Compare commits
5 Commits
master
...
07360d3f11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07360d3f11 | ||
| 8f28832432 | |||
|
|
4d1981420d | ||
|
|
14927b610d | ||
|
|
ae393f7c35 |
36
.env.example
36
.env.example
@@ -1,5 +1,12 @@
|
|||||||
# Discord bot token (from https://discord.com/developers/applications)
|
# Bot runtime profile: dev (economy + member tools) or economy (economy-only)
|
||||||
DISCORD_TOKEN=your-bot-token-here
|
BOT_PROFILE=dev
|
||||||
|
|
||||||
|
# Profile-specific Discord bot tokens (from https://discord.com/developers/applications)
|
||||||
|
DISCORD_TOKEN_DEV=your-dev-bot-token-here
|
||||||
|
DISCORD_TOKEN_ECONOMY=your-economy-bot-token-here
|
||||||
|
|
||||||
|
# Legacy fallback token (optional, backward compatibility)
|
||||||
|
DISCORD_TOKEN=
|
||||||
|
|
||||||
# Google Sheets spreadsheet ID (the long string in the sheet URL)
|
# Google Sheets spreadsheet ID (the long string in the sheet URL)
|
||||||
SHEET_ID=your-google-sheet-id-here
|
SHEET_ID=your-google-sheet-id-here
|
||||||
@@ -7,11 +14,21 @@ SHEET_ID=your-google-sheet-id-here
|
|||||||
# Path to Google service account credentials JSON
|
# Path to Google service account credentials JSON
|
||||||
GOOGLE_CREDS_PATH=credentials.json
|
GOOGLE_CREDS_PATH=credentials.json
|
||||||
|
|
||||||
# Guild (server) ID - right-click your server with dev mode on
|
# Profile-specific guild (server) IDs - right-click your server with dev mode on
|
||||||
GUILD_ID=your-guild-id-here
|
GUILD_ID_DEV=your-dev-guild-id-here
|
||||||
|
GUILD_ID_ECONOMY=your-economy-guild-id-here
|
||||||
|
|
||||||
# Channel ID where birthday announcements are posted (bot sends @here on the birthday at 09:00 Tallinn time)
|
# Legacy fallback guild ID (optional, backward compatibility)
|
||||||
BIRTHDAY_CHANNEL_ID=your-channel-id-here
|
GUILD_ID=
|
||||||
|
|
||||||
|
# Channel ID where birthday announcements are posted (dev profile)
|
||||||
|
BIRTHDAY_CHANNEL_ID_DEV=your-dev-birthday-channel-id-here
|
||||||
|
|
||||||
|
# Optional birthday channel for economy profile (normally unset for economy-only bot)
|
||||||
|
BIRTHDAY_CHANNEL_ID_ECONOMY=
|
||||||
|
|
||||||
|
# Legacy fallback birthday channel ID (optional, backward compatibility)
|
||||||
|
BIRTHDAY_CHANNEL_ID=
|
||||||
|
|
||||||
# How many days before a birthday the on-join check counts as "coming up"
|
# How many days before a birthday the on-join check counts as "coming up"
|
||||||
BIRTHDAY_WINDOW_DAYS=7
|
BIRTHDAY_WINDOW_DAYS=7
|
||||||
@@ -20,3 +37,10 @@ BIRTHDAY_WINDOW_DAYS=7
|
|||||||
PB_URL=http://127.0.0.1:8090
|
PB_URL=http://127.0.0.1:8090
|
||||||
PB_ADMIN_EMAIL=admin@example.com
|
PB_ADMIN_EMAIL=admin@example.com
|
||||||
PB_ADMIN_PASSWORD=your-pb-admin-password
|
PB_ADMIN_PASSWORD=your-pb-admin-password
|
||||||
|
|
||||||
|
# Profile-specific PocketBase collections
|
||||||
|
PB_ECONOMY_COLLECTION_DEV=economy_users_dev
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY=economy_users_prod
|
||||||
|
|
||||||
|
# Legacy fallback collection name (optional, backward compatibility)
|
||||||
|
PB_ECONOMY_COLLECTION=
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ pocketbase.exe
|
|||||||
pocketbase
|
pocketbase
|
||||||
pb_data/
|
pb_data/
|
||||||
pb_migrations/
|
pb_migrations/
|
||||||
|
logs/
|
||||||
|
|||||||
110
DEV_NOTES.md
110
DEV_NOTES.md
@@ -11,8 +11,6 @@
|
|||||||
| `sheets.py` | Google Sheets integration (member sync) |
|
| `sheets.py` | Google Sheets integration (member sync) |
|
||||||
| `member_sync.py` | Birthday/member sync background task |
|
| `member_sync.py` | Birthday/member sync background task |
|
||||||
| `config.py` | Environment variables (TOKEN, GUILD_ID, PB_URL, etc.) |
|
| `config.py` | Environment variables (TOKEN, GUILD_ID, PB_URL, etc.) |
|
||||||
| `migrate_to_pb.py` | One-time utility: migrate `data/economy.json` → PocketBase |
|
|
||||||
| `add_stats_fields.py` | Schema migration: add new fields to `economy_users` PocketBase collection |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,18 +21,19 @@ Checklist - do all of these, in order:
|
|||||||
1. **`economy.py`** - add the `do_<cmd>` async function with cooldown check, logic, `_commit`, and `_txn` logging
|
1. **`economy.py`** - add the `do_<cmd>` async function with cooldown check, logic, `_commit`, and `_txn` logging
|
||||||
2. **`economy.py`** - add the cooldown to `COOLDOWNS` dict if it has one
|
2. **`economy.py`** - add the cooldown to `COOLDOWNS` dict if it has one
|
||||||
3. **`economy.py`** - add the EXP reward to `EXP_REWARDS` dict
|
3. **`economy.py`** - add the EXP reward to `EXP_REWARDS` dict
|
||||||
3a. **PocketBase** - if the function stores new fields, add them as columns via `python scripts/add_stats_fields.py` (or manually in the PB admin UI at `http://127.0.0.1:8090/_/`). Fields not in the PB schema are silently dropped on PATCH.
|
4. **PocketBase** - if the function stores new fields, add them manually in the PB admin UI at `http://127.0.0.1:8090/_/`. Fields not in the PB schema are silently dropped on PATCH.
|
||||||
4. **`strings.py` `CMD`** - add the slash command description
|
5. **`strings.py` `CMD`** - add the slash command description
|
||||||
5. **`strings.py` `OPT`** - add any parameter descriptions
|
6. **`strings.py` `OPT`** - add any parameter descriptions
|
||||||
6. **`strings.py` `TITLE`** - add embed title(s) for success/fail states
|
7. **`strings.py` `TITLE`** - add embed title(s) for success/fail states
|
||||||
7. **`strings.py` `ERR`** - add any error messages (banned, cooldown uses `CD_MSG`, jailed uses `CD_MSG["jailed"]`)
|
8. **`strings.py` `ERR`** - add any error messages (banned, cooldown uses `CD_MSG`, jailed uses `CD_MSG["jailed"]`)
|
||||||
8. **`strings.py` `CD_MSG`** - add cooldown message if command has a cooldown
|
9. **`strings.py` `CD_MSG`** - add cooldown message if command has a cooldown
|
||||||
9. **`strings.py` `HELP_CATEGORIES["tipibot"]["fields"]`** - add the command to the help embed
|
10. **`strings.py` `HELP_CATEGORIES["tipibot"]["fields"]`** - add the command to the help embed
|
||||||
10. **`bot.py`** - implement the `cmd_<name>` function, handle all `res["reason"]` cases
|
11. **`bot.py`** - implement the `cmd_<name>` function, handle all `res["reason"]` cases
|
||||||
11. **`bot.py`** - call `_maybe_remind` if the command has a cooldown and reminders make sense
|
12. **`bot.py`** - call `_maybe_remind` if the command has a cooldown and reminders make sense
|
||||||
12. **`bot.py`** - call `_award_exp(interaction, economy.EXP_REWARDS["<cmd>"])` on success
|
13. **`bot.py`** - call `_award_exp(interaction, economy.EXP_REWARDS["<cmd>"])` on success
|
||||||
13. **`strings.py` `REMINDER_OPTS`** - add a reminder option if the command needs one
|
14. **`strings.py` `REMINDER_OPTS`** - add a reminder option if the command needs one
|
||||||
14. **`bot.py` `_maybe_remind`** - if the command has an item-modified cooldown, add an `elif` branch
|
15. **`bot.py` `_maybe_remind`** and **`_restore_reminders`** - if item-modified cooldown, add `elif` branch
|
||||||
|
16. **`bot.py` `_REMINDER_COOLDOWN_KEYS`** - add `"cmd": "last_cmd"` mapping if reminder-capable
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -50,6 +49,7 @@ Checklist:
|
|||||||
6. If the item modifies a cooldown:
|
6. If the item modifies a cooldown:
|
||||||
- **`economy.py`** - add the `if "item" in user["items"]` branch in the relevant `do_<cmd>` function
|
- **`economy.py`** - add the `if "item" in user["items"]` branch in the relevant `do_<cmd>` function
|
||||||
- **`bot.py` `_maybe_remind`** - add `elif cmd == "<cmd>" and "<item>" in items:` branch with the new delay
|
- **`bot.py` `_maybe_remind`** - add `elif cmd == "<cmd>" and "<item>" in items:` branch with the new delay
|
||||||
|
- **`bot.py` `_restore_reminders`** - add the same `elif` branch
|
||||||
- **`bot.py` `cmd_cooldowns`** - add the item annotation to the relevant status line
|
- **`bot.py` `cmd_cooldowns`** - add the item annotation to the relevant status line
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -65,8 +65,11 @@ Checklist:
|
|||||||
## Adding a New Admin Command
|
## Adding a New Admin Command
|
||||||
|
|
||||||
1. **`strings.py` `CMD`** - add `"[Admin] ..."` description
|
1. **`strings.py` `CMD`** - add `"[Admin] ..."` description
|
||||||
2. **`strings.py` `HELP_CATEGORIES["admin"]["fields"]`** - add the entry
|
2. **`strings.py` `OPT`** - add parameter descriptions
|
||||||
3. **`bot.py`** - add `@app_commands.default_permissions(manage_guild=True)` and `@app_commands.guild_only()`
|
3. **`strings.py` `ADMIN`** - add response and DM strings
|
||||||
|
4. **`strings.py` `HELP_CATEGORIES["admin"]["fields"]`** - add the entry
|
||||||
|
5. **`economy.py`** - add `do_admin_<name>` function
|
||||||
|
6. **`bot.py`** - add command with `@app_commands.default_permissions(manage_guild=True)` and `@app_commands.guild_only()`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -80,16 +83,46 @@ All economy state is stored in **PocketBase** (`economy_users` collection). `pb_
|
|||||||
|
|
||||||
| Command | Cooldown | Base Earn | Notes |
|
| Command | Cooldown | Base Earn | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `/daily` | 20h (18h w/ korvaklapid) | 150⬡ | ×streak multiplier, ×2 w/ lan_pass, +5% interest w/ gaming_laptop |
|
| `/daily` | 20h (18h w/ korvaklapid) | 150⬡ | ×streak multiplier, ×2 w/ lan_pass, +5% interest w/ gaming_laptop; prestige daily_plus adds +20% per level |
|
||||||
| `/work` | 1h (40min w/ monitor) | 15-75⬡ | ×1.5 w/ gaming_hiir, ×1.25 w/ reguleeritav_laud, ×3 30% chance w/ energiajook |
|
| `/work` | 1h (40min w/ monitor) | 15-75⬡ | ×1.5 w/ gaming_hiir, ×1.25 w/ reguleeritav_laud, ×3 30% chance w/ energiajook; prestige work_plus +20%/level |
|
||||||
| `/beg` | 5min (3min w/ hiirematt) | 10-40⬡ | ×2 w/ klaviatuur |
|
| `/beg` | 5min (3min w/ hiirematt) | 10-40⬡ | ×2 w/ klaviatuur |
|
||||||
| `/crime` | 2h | 200-500⬡ win | 60% success (75% w/ cat6), +30% w/ mikrofon; fail = fine + jail |
|
| `/crime` | 2h | 200-500⬡ win | 60% success (75% w/ cat6), +30% w/ mikrofon; fail = fine + jail |
|
||||||
| `/rob` | 2h | 10–25% of target | 45% success (60% w/ jellyfin); fail = fine; cannot rob TipiBOT |
|
| `/rob` | 2h | 10–25% of target | 45% success (60% w/ jellyfin); fail = fine; cannot rob TipiBOT directly |
|
||||||
| `/heist` | 4h personal + 1h global | 20–55% of house / n players | solo allowed, max 8; 35%+5%/player success (cap 65%); fail = 3h jail + ~15% balance fine |
|
| `/heist` | 4h personal + 1h global | 20–55% of house / n players | solo allowed, max 8; 35%+5%/player success (cap 65%); fail = 3h jail + ~15% balance fine |
|
||||||
| `/slots` | - | varies | pair=+0.5× bet; triple tiered: heart×4, fire×5, troll×7, cry×10, skull×15, karikas×25 (jackpot); ×1.5 w/ monitor_360; miss=lose bet; house edge ~5% |
|
| `/fish` | 2min (90s w/ ussipurk) | varies by fish rarity | Interactive minigame; catches go to inventory; sell with `/fishsell` |
|
||||||
|
| `/slots` | - | varies | pair=+0.5× bet; triple tiered; karikas jackpot ×25; ×1.5 w/ monitor_360; miss=lose bet |
|
||||||
| `/roulette` | - | 2× red/black, 14× green | 1/37 green chance |
|
| `/roulette` | - | 2× red/black, 14× green | 1/37 green chance |
|
||||||
| `/blackjack` | - | 1:1 win, 3:2 BJ, 2:1 double | Dealer stands on 17+; double down on first action only |
|
| `/blackjack` | - | 1:1 win, 3:2 BJ, 2:1 double | Dealer stands on 17+; double down on first action only |
|
||||||
|
|
||||||
|
### Fishing System
|
||||||
|
|
||||||
|
- `/fish` - interactive minigame: cast → wait 5–15s for bite → press button within 2s (3s w/ echolood) → keep or sell
|
||||||
|
- Fish stored in `fish_inventory` (list of `{fish_id, weight, value}` objects)
|
||||||
|
- `/fishbook` - paginated fish collection showing caught species and inventory counts
|
||||||
|
- `/fishsell` - sell all fish from inventory at once
|
||||||
|
- `fish_inventory` and `fish_book` **survive prestige resets**
|
||||||
|
- `kalavork` (T3, 5000⬡): bumps all caught fish up one rarity tier
|
||||||
|
- `ussipurk` (T2, 3500⬡): cooldown 2min → 90s
|
||||||
|
- `echolood` (T3, 8000⬡): bite window 2s → 3s
|
||||||
|
|
||||||
|
### Prestige System
|
||||||
|
|
||||||
|
- Requires level 30 (9000 EXP)
|
||||||
|
- Resets: balance, EXP, items, cooldowns, jail
|
||||||
|
- Preserves: fish_book, fish_inventory, lifetime stats, prestige_points, season_total_exp, prestige_upgrades
|
||||||
|
- Awards prestige_points = max(1, exp ÷ 1000) at time of prestige
|
||||||
|
- Each prestige increments `prestige_level` counter
|
||||||
|
- Prestige coin/exp multipliers apply to all earned values
|
||||||
|
|
||||||
|
**Prestige Shop** (`PRESTIGE_SHOP` in economy.py):
|
||||||
|
|
||||||
|
| Upgrade | Max Level | Cost/level | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `coin_mult` | 5 | 5 PP | +8% coin multiplier per level |
|
||||||
|
| `exp_mult` | 5 | 5 PP | +8% EXP multiplier per level |
|
||||||
|
| `daily_plus` | 3 | 7 PP | +20% daily base reward per level |
|
||||||
|
| `work_plus` | 3 | 7 PP | +20% work earnings per level |
|
||||||
|
|
||||||
### "all" Keyword
|
### "all" Keyword
|
||||||
Commands that accept a coin amount (`/give`, `/roulette`, `/rps`, `/slots`, `/blackjack`) accept `"all"` to mean the user's full current balance. Parsed by `_parse_amount(value, balance)` in `bot.py`.
|
Commands that accept a coin amount (`/give`, `/roulette`, `/rps`, `/slots`, `/blackjack`) accept `"all"` to mean the user's full current balance. Parsed by `_parse_amount(value, balance)` in `bot.py`.
|
||||||
|
|
||||||
@@ -102,13 +135,35 @@ Commands that accept a coin amount (`/give`, `/roulette`, `/rps`, `/slots`, `/bl
|
|||||||
|
|
||||||
### Jail
|
### Jail
|
||||||
- Normal duration: 30 minutes (`JAIL_DURATION`)
|
- Normal duration: 30 minutes (`JAIL_DURATION`)
|
||||||
- Heist fail duration: 3 hours (`HEIST_JAIL`) + fine 15% of balance (min 150⨡, max 1000⨡)
|
- Heist fail duration: 3 hours (`HEIST_JAIL`) + fine 15% of balance (min 150⬡, max 1000⬡)
|
||||||
- `gaming_tool`: prevents jail on crime fail
|
- `gaming_tool`: prevents jail on crime fail
|
||||||
- `/jailbreak`: 3 dice rolls, need doubles to escape free. On fail - bail = 20-30% of balance, min 350⬡. If balance < 350⬡, player stays jailed until timer.
|
- `/jailbreak`: 3 single-button dice rolls (both dice at once), need doubles to escape free. Animated reveal with TipiDICE emoji. On fail after 3 tries - bail = 20-30% of balance, min 350⬡. If balance < 350⬡, stay jailed until timer.
|
||||||
|
|
||||||
### EXP Rewards (from `EXP_REWARDS` in economy.py)
|
### EXP Rewards (from `EXP_REWARDS` in economy.py)
|
||||||
EXP is awarded on every successful command use. Level formula: `floor(sqrt(exp / 10))`, so Level 5 = 250 EXP, Level 10 = 1000, Level 20 = 4000, Level 30 = 9000.
|
EXP is awarded on every successful command use. Level formula: `floor(sqrt(exp / 10))`, so Level 5 = 250 EXP, Level 10 = 1000, Level 20 = 4000, Level 30 = 9000.
|
||||||
|
|
||||||
|
Fish EXP is awarded per catch (varies by rarity, defined in `FISH_CATALOGUE`). Prestige `exp_mult` upgrade applies to fish EXP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin Commands Reference
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `/pause` | Toggle maintenance mode - blocks all non-admin commands |
|
||||||
|
| `/admincoins @user <amount> <reason>` | Give/take coins (positive/negative). DMs user. |
|
||||||
|
| `/adminexp @user <amount> <reason>` | Give/take EXP (positive/negative). Auto-applies level roles on change. DMs user. |
|
||||||
|
| `/adminitem @user <item_id> <anna\|eemalda>` | Give or remove any shop item for free. DMs user. |
|
||||||
|
| `/adminjail @user <minutes> <reason>` | Manually jail a user. DMs user. |
|
||||||
|
| `/adminunjail @user` | Remove jail from a user. |
|
||||||
|
| `/adminban @user <reason>` | Ban from all economy commands. DMs user. |
|
||||||
|
| `/adminunban @user` | Lift economy ban. |
|
||||||
|
| `/adminreset @user <reason>` | Wipe balance, EXP, items, streak. DMs user. |
|
||||||
|
| `/adminview @user` | Full profile: balance, EXP/level, streak, prestige, fish stats, items, timestamps. |
|
||||||
|
| `/adminseason <top_n>` | End season: DM top N players, reset all EXP. |
|
||||||
|
|
||||||
|
All admin commands require **Manage Guild** permission and work in any channel (bypass pause and channel restrictions).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Role Hierarchy (Discord)
|
## Role Hierarchy (Discord)
|
||||||
@@ -130,6 +185,7 @@ Run `/economysetup` to auto-create all roles and set their positions. The comman
|
|||||||
Role assignment:
|
Role assignment:
|
||||||
- **ECONOMY** role: given automatically on first EXP award (i.e. first successful economy command)
|
- **ECONOMY** role: given automatically on first EXP award (i.e. first successful economy command)
|
||||||
- **Level roles**: given/swapped automatically on level-up; synced on `/rank`
|
- **Level roles**: given/swapped automatically on level-up; synced on `/rank`
|
||||||
|
- **`/adminexp`**: automatically re-applies level roles if level changes
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -138,8 +194,8 @@ Role assignment:
|
|||||||
| Tier | Level Required | Items |
|
| Tier | Level Required | Items |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| T1 | 0 (any) | gaming_hiir, hiirematt, korvaklapid, lan_pass, anticheat, energiajook, gaming_laptop |
|
| T1 | 0 (any) | gaming_hiir, hiirematt, korvaklapid, lan_pass, anticheat, energiajook, gaming_laptop |
|
||||||
| T2 | 10 | reguleeritav_laud, jellyfin, mikrofon, klaviatuur, monitor, cat6 |
|
| T2 | 10 | reguleeritav_laud, jellyfin, mikrofon, klaviatuur, monitor, cat6, **ussipurk** |
|
||||||
| T3 | 20 | monitor_360, karikas, gaming_tool |
|
| T3 | 20 | monitor_360, karikas, gaming_tool, **kalavork**, **echolood** |
|
||||||
|
|
||||||
Shop display is sorted by cost (ascending) within each tier.
|
Shop display is sorted by cost (ascending) within each tier.
|
||||||
The `SHOP_LEVEL_REQ` dict in `economy.py` controls per-item lock thresholds.
|
The `SHOP_LEVEL_REQ` dict in `economy.py` controls per-item lock thresholds.
|
||||||
@@ -162,6 +218,9 @@ The `SHOP_LEVEL_REQ` dict in `economy.py` controls per-item lock thresholds.
|
|||||||
| Cooldown messages | `CD_MSG["cmd"].format(ts=_cd_ts(...))` | Cooldown responses |
|
| Cooldown messages | `CD_MSG["cmd"].format(ts=_cd_ts(...))` | Cooldown responses |
|
||||||
| Shop UI | `SHOP_UI["key"]` | `_shop_embed` |
|
| Shop UI | `SHOP_UI["key"]` | `_shop_embed` |
|
||||||
| Item descriptions | `ITEM_DESCRIPTIONS["item_key"]` | `economy.SHOP[key]["description"]` |
|
| Item descriptions | `ITEM_DESCRIPTIONS["item_key"]` | `economy.SHOP[key]["description"]` |
|
||||||
|
| Fish UI | `FISH_UI["key"]` | `/fish`, `/fishbook`, `/fishsell` |
|
||||||
|
| Fish names | `FISH_NAMES["fish_id"]` | Fish display name |
|
||||||
|
| Admin responses | `ADMIN["key"]` | Admin command success/DM messages |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -177,6 +236,8 @@ The `SHOP_LEVEL_REQ` dict in `economy.py` controls per-item lock thresholds.
|
|||||||
| `LEVEL_ROLES` | `economy.py` | `[(min_level, "RoleName"), ...]` highest first |
|
| `LEVEL_ROLES` | `economy.py` | `[(min_level, "RoleName"), ...]` highest first |
|
||||||
| `ECONOMY_ROLE` | `economy.py` | Name of the base economy participation role |
|
| `ECONOMY_ROLE` | `economy.py` | Name of the base economy participation role |
|
||||||
| `EXP_REWARDS` | `economy.py` | EXP per command |
|
| `EXP_REWARDS` | `economy.py` | EXP per command |
|
||||||
|
| `FISH_CATALOGUE` | `economy.py` | All fish species (rarity, weight, coins, exp) |
|
||||||
|
| `PRESTIGE_SHOP` | `economy.py` | Prestige upgrade definitions |
|
||||||
| `HOUSE_ID` | `economy.py` | Bot's user ID (house account for /rob) |
|
| `HOUSE_ID` | `economy.py` | Bot's user ID (house account for /rob) |
|
||||||
| `MIN_BAIL` | `economy.py` | Minimum bail payment (350⬡) |
|
| `MIN_BAIL` | `economy.py` | Minimum bail payment (350⬡) |
|
||||||
| `COIN` | `economy.py` | The coin emoji string |
|
| `COIN` | `economy.py` | The coin emoji string |
|
||||||
@@ -193,3 +254,4 @@ The `SHOP_LEVEL_REQ` dict in `economy.py` controls per-item lock thresholds.
|
|||||||
- `anticheat` is consumable (2 uses) - only item that can be re-bought
|
- `anticheat` is consumable (2 uses) - only item that can be re-bought
|
||||||
- `karikas` (T3) is the only item that preserves a daily streak across missed days
|
- `karikas` (T3) is the only item that preserves a daily streak across missed days
|
||||||
- `reguleeritav_laud` (T2) stacks with `gaming_hiir`: combined ×1.5 × ×1.25 = ×1.875
|
- `reguleeritav_laud` (T2) stacks with `gaming_hiir`: combined ×1.5 × ×1.25 = ×1.875
|
||||||
|
- **Fishing** is a steady passive income; `kalavork` (T3) dramatically increases fish value by bumping rarity
|
||||||
|
|||||||
95
README.md
95
README.md
@@ -70,7 +70,7 @@ The economy system stores all player data in [PocketBase](https://pocketbase.io/
|
|||||||
1. Download `pocketbase.exe` (Windows) from https://pocketbase.io/docs/ and place it in the project root.
|
1. Download `pocketbase.exe` (Windows) from https://pocketbase.io/docs/ and place it in the project root.
|
||||||
2. Start PocketBase: `.\pocketbase.exe serve`
|
2. Start PocketBase: `.\pocketbase.exe serve`
|
||||||
3. Open the admin UI at http://127.0.0.1:8090/_/ and create a superuser account.
|
3. Open the admin UI at http://127.0.0.1:8090/_/ and create a superuser account.
|
||||||
4. Create a collection named `economy_users` - see `docs/POCKETBASE_SETUP.md` for the full schema.
|
4. Create two collections for profile separation: `economy_users_dev` and `economy_users_prod` - see `docs/POCKETBASE_SETUP.md` for schema notes.
|
||||||
5. Set `PB_URL`, `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD` in `.env`.
|
5. Set `PB_URL`, `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD` in `.env`.
|
||||||
6. **One-time data migration** (only if you have an existing `data/economy.json`): `python scripts/migrate_to_pb.py`
|
6. **One-time data migration** (only if you have an existing `data/economy.json`): `python scripts/migrate_to_pb.py`
|
||||||
|
|
||||||
@@ -88,15 +88,25 @@ cp .env.example .env
|
|||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `DISCORD_TOKEN` | Bot token from Discord Developer Portal |
|
| `BOT_PROFILE` | Runtime profile: `dev` or `economy` |
|
||||||
|
| `DISCORD_TOKEN_DEV` | Dev bot token from Discord Developer Portal |
|
||||||
|
| `DISCORD_TOKEN_ECONOMY` | Economy bot token from Discord Developer Portal |
|
||||||
|
| `DISCORD_TOKEN` | Legacy fallback token (optional) |
|
||||||
| `SHEET_ID` | ID from the Google Sheet URL |
|
| `SHEET_ID` | ID from the Google Sheet URL |
|
||||||
| `GOOGLE_CREDS_PATH` | Path to `credentials.json` (default: `credentials.json`) |
|
| `GOOGLE_CREDS_PATH` | Path to `credentials.json` (default: `credentials.json`) |
|
||||||
| `GUILD_ID` | Right-click server → Copy Server ID (Developer Mode on) |
|
| `GUILD_ID_DEV` | Dev bot guild ID |
|
||||||
| `BIRTHDAY_CHANNEL_ID` | Channel for birthday `@here` pings |
|
| `GUILD_ID_ECONOMY` | Economy bot guild ID |
|
||||||
|
| `GUILD_ID` | Legacy fallback guild ID (optional) |
|
||||||
|
| `BIRTHDAY_CHANNEL_ID_DEV` | Channel for birthday `@here` pings in dev profile |
|
||||||
|
| `BIRTHDAY_CHANNEL_ID_ECONOMY` | Optional birthday channel in economy profile |
|
||||||
|
| `BIRTHDAY_CHANNEL_ID` | Legacy fallback birthday channel ID (optional) |
|
||||||
| `BIRTHDAY_WINDOW_DAYS` | Days before birthday that on-join check flags it (default: 7) |
|
| `BIRTHDAY_WINDOW_DAYS` | Days before birthday that on-join check flags it (default: 7) |
|
||||||
| `PB_URL` | PocketBase base URL (default: `http://127.0.0.1:8090`) |
|
| `PB_URL` | PocketBase base URL (default: `http://127.0.0.1:8090`) |
|
||||||
| `PB_ADMIN_EMAIL` | PocketBase superuser e-mail |
|
| `PB_ADMIN_EMAIL` | PocketBase superuser e-mail |
|
||||||
| `PB_ADMIN_PASSWORD` | PocketBase superuser password |
|
| `PB_ADMIN_PASSWORD` | PocketBase superuser password |
|
||||||
|
| `PB_ECONOMY_COLLECTION_DEV` | PocketBase collection used by `BOT_PROFILE=dev` |
|
||||||
|
| `PB_ECONOMY_COLLECTION_ECONOMY` | PocketBase collection used by `BOT_PROFILE=economy` |
|
||||||
|
| `PB_ECONOMY_COLLECTION` | Legacy fallback collection (optional) |
|
||||||
|
|
||||||
### 6. Install & Run
|
### 6. Install & Run
|
||||||
|
|
||||||
@@ -110,7 +120,12 @@ pip install -r requirements.txt
|
|||||||
# Terminal 1 - keep running
|
# Terminal 1 - keep running
|
||||||
.\pocketbase.exe serve
|
.\pocketbase.exe serve
|
||||||
|
|
||||||
# Terminal 2
|
# Terminal 2 (dev bot)
|
||||||
|
set BOT_PROFILE=dev
|
||||||
|
python bot.py
|
||||||
|
|
||||||
|
# Terminal 3 (economy bot)
|
||||||
|
set BOT_PROFILE=economy
|
||||||
python bot.py
|
python bot.py
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -146,6 +161,8 @@ Admins (bot lacks permission to modify them) are silently skipped and still mark
|
|||||||
## Admin Commands
|
## Admin Commands
|
||||||
|
|
||||||
> These commands are hidden from users who don't have the required permission. Override per-role in **Server Settings → Integrations → Bot**.
|
> These commands are hidden from users who don't have the required permission. Override per-role in **Server Settings → Integrations → Bot**.
|
||||||
|
>
|
||||||
|
> Profile note: `/check`, `/member`, and `/birthdays` are available only when `BOT_PROFILE=dev`.
|
||||||
|
|
||||||
| Command | Permission | What it does |
|
| Command | Permission | What it does |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -154,16 +171,18 @@ Admins (bot lacks permission to modify them) are silently skipped and still mark
|
|||||||
| `/sync` | Manage Guild | Re-registers slash commands with Discord |
|
| `/sync` | Manage Guild | Re-registers slash commands with Discord |
|
||||||
| `/restart` | Manage Guild | Gracefully restarts the bot process; posts ✅ in the same channel when back up |
|
| `/restart` | Manage Guild | Gracefully restarts the bot process; posts ✅ in the same channel when back up |
|
||||||
| `/shutdown` | Manage Guild | Shuts the bot down cleanly without restarting |
|
| `/shutdown` | Manage Guild | Shuts the bot down cleanly without restarting |
|
||||||
| `/pause` | Manage Guild | Toggles maintenance mode — blocks all non-admin commands; calling again unpauses |
|
| `/pause` | Manage Guild | Toggles maintenance mode - blocks all non-admin commands; calling again unpauses |
|
||||||
| `/send #channel message` | Manage Guild | Sends a message to any channel as the bot |
|
| `/send #channel message` | Manage Guild | Sends a message to any channel as the bot |
|
||||||
| `/status` | Manage Guild | Bot uptime, RAM, CPU, latency, cache stats, economy user count |
|
| `/status` | Manage Guild | Bot uptime, RAM, CPU, latency, cache stats, economy user count |
|
||||||
| `/admincoins @user <kogus> <põhjus>` | Manage Guild | Give (positive) or take (negative) TipiCOINi. Balance floored at 0. User gets a DM with reason. |
|
| `/admincoins @user <kogus> <põhjus>` | Manage Guild | Give (positive) or take (negative) TipiCOINi. Balance floored at 0. User gets a DM with reason. |
|
||||||
|
| `/adminexp @user <kogus> <põhjus>` | Manage Guild | Give (positive) or take (negative) EXP. Level roles auto-updated. User gets a DM. |
|
||||||
|
| `/adminitem @user <ese> <anna\|eemalda>` | Manage Guild | Give or remove any shop item for free. `ese` is the internal item ID (e.g. `anticheat`). User gets a DM. |
|
||||||
| `/adminjail @user <minutid> <põhjus>` | Manage Guild | Manually jail a user for N minutes. User gets a DM. |
|
| `/adminjail @user <minutid> <põhjus>` | Manage Guild | Manually jail a user for N minutes. User gets a DM. |
|
||||||
| `/adminunjail @user` | Manage Guild | Release a user from jail immediately. |
|
| `/adminunjail @user` | Manage Guild | Release a user from jail immediately. |
|
||||||
| `/adminban @user <põhjus>` | Manage Guild | Ban a user from all economy commands. User gets a DM. |
|
| `/adminban @user <põhjus>` | Manage Guild | Ban a user from all economy commands. User gets a DM. |
|
||||||
| `/adminunban @user` | Manage Guild | Lift an economy ban. |
|
| `/adminunban @user` | Manage Guild | Lift an economy ban. |
|
||||||
| `/adminreset @user <põhjus>` | Manage Guild | Wipe a user's balance, items, and streak to zero. User gets a DM. |
|
| `/adminreset @user <põhjus>` | Manage Guild | Wipe a user's balance, items, and streak to zero. User gets a DM. |
|
||||||
| `/adminview @user` | Manage Guild | Inspect a user's full economy profile: balance, streak, items, jail status, ban status. |
|
| `/adminview @user` | Manage Guild | Full profile: balance, EXP/level, streak, prestige, fish stats, items, ban/jail, all timestamps. |
|
||||||
|
|
||||||
### `/check` output example
|
### `/check` output example
|
||||||
```
|
```
|
||||||
@@ -226,10 +245,11 @@ The house is listed at **#0** on the leaderboard. Players can attempt to rob it
|
|||||||
|
|
||||||
| Command | Cooldown | Base payout | Notes |
|
| Command | Cooldown | Base payout | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `/daily` | 20h | 150 ⬡ | Streak multiplier applied (see below). Kõrvaklapid reduces cooldown to 18h. LAN Pilet doubles the reward. Botikoobas adds 5% interest on your balance (capped at 500 ⬡/day). |
|
| `/daily` | 20h | 150 ⬡ | Streak multiplier applied (see below). Kõrvaklapid reduces cooldown to 18h. LAN Pilet doubles the reward. Botikoobas adds 5% interest on your balance (capped at 500 ⬡/day). Prestige daily_plus adds +20% base per upgrade level. |
|
||||||
| `/work` | 1h | 15–75 ⬡ | Random job flavour text. Mängurihiir +50%, Reguleeritav laud +25% (stacks). Red Bull: 30% chance of ×3. Ultralai monitor reduces cooldown to 40min. |
|
| `/work` | 1h | 15–75 ⬡ | Random job flavour text. Mängurihiir +50%, Reguleeritav laud +25% (stacks). Red Bull: 30% chance of ×3. Ultralai monitor reduces cooldown to 40min. Prestige work_plus adds +20% per upgrade level. |
|
||||||
| `/beg` | 5min | 10–40 ⬡ | XL hiirematt reduces cooldown to 3min. Mehhaaniline klaviatuur multiplies earnings ×2. |
|
| `/beg` | 5min | 10–40 ⬡ | XL hiirematt reduces cooldown to 3min. Mehhaaniline klaviatuur multiplies earnings ×2. |
|
||||||
| `/crime` | 2h | 200–500 ⬡ | 60% success rate (75% with CAT6). +30% earnings with Mikrofon on win. Fail = fine + 30min jail. Mänguritool skips jail on fail. |
|
| `/crime` | 2h | 200–500 ⬡ | 60% success rate (75% with CAT6). +30% earnings with Mikrofon on win. Fail = fine + 30min jail. Mänguritool skips jail on fail. |
|
||||||
|
| `/fish` | 2min | varies | Interactive minigame. Cast → wait for bite → press button within 2s → keep in inventory or sell immediately. Ussipurk reduces cooldown to 90s. |
|
||||||
|
|
||||||
### Daily streak
|
### Daily streak
|
||||||
|
|
||||||
@@ -254,10 +274,12 @@ Every successful economy action awards EXP:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `/daily` claimed | +50 |
|
| `/daily` claimed | +50 |
|
||||||
| `/work` completed | +25 |
|
| `/work` completed | +25 |
|
||||||
|
| `/heist` win | +25 |
|
||||||
| `/crime` success | +15 |
|
| `/crime` success | +15 |
|
||||||
| `/rob` success | +15 |
|
| `/rob` success | +15 |
|
||||||
| Gambling win (`/roulette`, `/slots`, `/blackjack`) | Scaled by bet: <10⬡ = 0, 10–99⬡ = +5, 100–999⬡ = +10, 1 000–9 999⬡ = +15, 10 000–99 999⬡ = +20, 100 000+⬡ = +25 |
|
| Gambling win (`/roulette`, `/slots`, `/blackjack`) | Scaled by bet: <10⬡ = 0, 10–99⬡ = +5, 100–999⬡ = +10, 1 000–9 999⬡ = +15, 10 000–99 999⬡ = +20, 100 000+⬡ = +25 |
|
||||||
| `/beg` completed | +5 |
|
| `/beg` completed | +5 |
|
||||||
|
| `/fish` catch | +3 to +15 (varies by rarity) |
|
||||||
|
|
||||||
**Level formula:** `level = floor(√(total_exp ÷ 10))`
|
**Level formula:** `level = floor(√(total_exp ÷ 10))`
|
||||||
|
|
||||||
@@ -314,10 +336,16 @@ The **ECONOMY** role is granted on your first EXP award (i.e. first successful e
|
|||||||
| `/rank [@user]` | EXP total, current level, progress bar to next level, leaderboard rank. |
|
| `/rank [@user]` | EXP total, current level, progress bar to next level, leaderboard rank. |
|
||||||
| `/stats [@user]` | Lifetime statistics: economy totals, work/beg counts, gambling records, crime/heist history, social totals, best streak. |
|
| `/stats [@user]` | Lifetime statistics: economy totals, work/beg counts, gambling records, crime/heist history, social totals, best streak. |
|
||||||
| `/cooldowns` | All cooldowns at a glance with live Discord timestamps. Shows jail timer if jailed. |
|
| `/cooldowns` | All cooldowns at a glance with live Discord timestamps. Shows jail timer if jailed. |
|
||||||
| `/leaderboard` | Paginated coin leaderboard (10/page). House pinned at #0. ◀/▶ to browse; 📍 **Mina** jumps to your page. Has a separate EXP/level tab. |
|
| `/leaderboard` | Paginated leaderboard with 6 tabs: 🪙 Coins, 📊 EXP, 🏆 Season EXP, 🔥 Prestige, 🎲 Wagered, 🎣 Fish caught. House pinned at #0 on coins tab. |
|
||||||
| `/shop` | Browse all items by tier. Shows owned status, Anticheat charges remaining, and level lock for T2/T3. |
|
| `/shop` | Browse all items by tier. Shows owned status, Anticheat charges remaining, and level lock for T2/T3. |
|
||||||
| `/buy <item>` | Purchase an item by name (partial match accepted). |
|
| `/buy <item>` | Purchase an item by name (partial match accepted). |
|
||||||
| `/reminders` | Toggle per-command DM notifications. **All reminders are on by default.** Bot DMs you the moment each cooldown expires. |
|
| `/reminders` | Toggle per-command DM notifications. Bot DMs you the moment each cooldown expires. |
|
||||||
|
| `/fish` | Interactive fishing minigame. Cast, wait for bite, pull, then keep or sell. 2min cooldown (90s with Ussipurk). |
|
||||||
|
| `/fishbook` | View your fish collection - all caught species, rarity, count, and current inventory amounts. |
|
||||||
|
| `/fishsell` | Sell all fish currently in your inventory at once. |
|
||||||
|
| `/prestige` | Reset your balance/EXP/items at level 30 in exchange for Prestige Points. Fishing collection preserved. |
|
||||||
|
| `/prestigeshop` | View available prestige upgrades and their current levels. |
|
||||||
|
| `/prestigebuy <upgrade>` | Purchase a prestige upgrade using Prestige Points. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -326,7 +354,7 @@ The **ECONOMY** role is granted on your first EXP award (i.e. first successful e
|
|||||||
`/crime` fail (without Mänguritool) jails you for **30 minutes**. While jailed, `/work`, `/beg`, `/crime`, `/rob`, and `/give` are blocked.
|
`/crime` fail (without Mänguritool) jails you for **30 minutes**. While jailed, `/work`, `/beg`, `/crime`, `/rob`, and `/give` are blocked.
|
||||||
|
|
||||||
#### `/jailbreak`
|
#### `/jailbreak`
|
||||||
Roll two dice - matching values (doubles) free you instantly. **3 attempts** per jail sentence. If all 3 fail you pay bail:
|
Press the roll button - both dice are rolled simultaneously with an animated reveal. **3 attempts** per sentence. Matching values (doubles) = free instantly. If all 3 fail you pay bail:
|
||||||
|
|
||||||
- **20–30% of your current balance** (scales with wealth)
|
- **20–30% of your current balance** (scales with wealth)
|
||||||
- **Minimum 350 ⬡** - if your balance is below this you stay jailed until the timer runs out
|
- **Minimum 350 ⬡** - if your balance is below this you stay jailed until the timer runs out
|
||||||
@@ -335,6 +363,41 @@ Cooldowns and jail release times display as live Discord relative timestamps.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Prestige
|
||||||
|
|
||||||
|
Once you reach **level 30** (9 000 EXP), you can `/prestige`. This resets your balance, EXP, and items in exchange for **Prestige Points** (PP = floor(exp ÷ 1000), min 1).
|
||||||
|
|
||||||
|
**What survives a prestige reset:** fish book, fish inventory, lifetime economy stats, prestige upgrades, season EXP.
|
||||||
|
|
||||||
|
Spend PP in `/prestigeshop`:
|
||||||
|
|
||||||
|
| Upgrade | Max Level | Cost/level | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Coin multiplier | 5 | 5 PP | +8% to all coin earnings per level |
|
||||||
|
| EXP multiplier | 5 | 5 PP | +8% to all EXP per level |
|
||||||
|
| Daily bonus | 3 | 7 PP | +20% to daily base payout per level |
|
||||||
|
| Work bonus | 3 | 7 PP | +20% to work earnings per level |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fishing
|
||||||
|
|
||||||
|
`/fish` is an interactive minigame with a **2-minute cooldown** (90s with Ussipurk):
|
||||||
|
|
||||||
|
1. Cast the line - a button appears
|
||||||
|
2. Wait 5–15 seconds for the bite
|
||||||
|
3. Press **TÕMBA!** within the 2s window (3s with Echolood)
|
||||||
|
4. Choose: **sell immediately** or **keep in inventory**
|
||||||
|
|
||||||
|
Caught fish go to your `fish_inventory` and persist through prestige resets.
|
||||||
|
|
||||||
|
| Command | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `/fishbook` | View your fish collection: caught species, rarity, count, how many in inventory. |
|
||||||
|
| `/fishsell` | Sell all fish in your inventory at once. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Shop items
|
### Shop items
|
||||||
|
|
||||||
All items are **permanent** once purchased **except Anticheat**, which expires after 2 uses and can be repurchased.
|
All items are **permanent** once purchased **except Anticheat**, which expires after 2 uses and can be repurchased.
|
||||||
@@ -362,6 +425,12 @@ All items are **permanent** once purchased **except Anticheat**, which expires a
|
|||||||
| CAT6 netikaabel | 3 500 ⬡ | `/crime` success rate 60% → 75% |
|
| CAT6 netikaabel | 3 500 ⬡ | `/crime` success rate 60% → 75% |
|
||||||
| Jellyfin server | 4 000 ⬡ | `/rob` success rate 45% → 60% |
|
| Jellyfin server | 4 000 ⬡ | `/rob` success rate 45% → 60% |
|
||||||
|
|
||||||
|
#### Tier 2 - level 10 required (TipiHUSTLER+) - continued
|
||||||
|
|
||||||
|
| Item | Cost | Effect |
|
||||||
|
|---|---|---|
|
||||||
|
| Ussipurk | 3 500 ⬡ | `/fish` cooldown 2min → 90s |
|
||||||
|
|
||||||
#### Tier 3 - level 20 required (TipiCHAD+)
|
#### Tier 3 - level 20 required (TipiCHAD+)
|
||||||
|
|
||||||
| Item | Cost | Effect |
|
| Item | Cost | Effect |
|
||||||
@@ -369,6 +438,8 @@ All items are **permanent** once purchased **except Anticheat**, which expires a
|
|||||||
| TipiLAN trofee | 6 000 ⬡ | Daily streak survives missed days |
|
| TipiLAN trofee | 6 000 ⬡ | Daily streak survives missed days |
|
||||||
| 360hz monitor | 7 500 ⬡ | Slots jackpot 10× → 15×, triple 4× → 6× |
|
| 360hz monitor | 7 500 ⬡ | Slots jackpot 10× → 15×, triple 4× → 6× |
|
||||||
| Mänguritool | 9 000 ⬡ | `/crime` fail never sends you to jail |
|
| Mänguritool | 9 000 ⬡ | `/crime` fail never sends you to jail |
|
||||||
|
| Kalavõrk | 5 000 ⬡ | All fish caught are bumped up one rarity tier |
|
||||||
|
| Echolood | 8 000 ⬡ | Fishing bite window 2s → 3s |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
50
config.py
50
config.py
@@ -3,14 +3,58 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
BOT_PROFILE = os.getenv("BOT_PROFILE", "dev").strip().lower() or "dev"
|
||||||
|
if BOT_PROFILE not in {"dev", "economy"}:
|
||||||
|
raise SystemExit("BOT_PROFILE must be either 'dev' or 'economy'.")
|
||||||
|
|
||||||
|
|
||||||
|
def _env_int(name: str, default: int) -> int:
|
||||||
|
raw = os.getenv(name)
|
||||||
|
if raw is None or not raw.strip():
|
||||||
|
return default
|
||||||
|
return int(raw)
|
||||||
|
|
||||||
|
|
||||||
|
_LEGACY_DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
|
||||||
|
DISCORD_TOKEN_DEV = os.getenv("DISCORD_TOKEN_DEV", "")
|
||||||
|
DISCORD_TOKEN_ECONOMY = os.getenv("DISCORD_TOKEN_ECONOMY", "")
|
||||||
|
DISCORD_TOKEN = (
|
||||||
|
DISCORD_TOKEN_ECONOMY if BOT_PROFILE == "economy" else DISCORD_TOKEN_DEV
|
||||||
|
) or _LEGACY_DISCORD_TOKEN
|
||||||
|
|
||||||
SHEET_ID = os.getenv("SHEET_ID")
|
SHEET_ID = os.getenv("SHEET_ID")
|
||||||
GOOGLE_CREDS_PATH = os.getenv("GOOGLE_CREDS_PATH", "credentials.json")
|
GOOGLE_CREDS_PATH = os.getenv("GOOGLE_CREDS_PATH", "credentials.json")
|
||||||
GUILD_ID = int(os.getenv("GUILD_ID", "0"))
|
|
||||||
BIRTHDAY_CHANNEL_ID = int(os.getenv("BIRTHDAY_CHANNEL_ID", "0"))
|
_LEGACY_GUILD_ID = _env_int("GUILD_ID", 0)
|
||||||
|
GUILD_ID_DEV = _env_int("GUILD_ID_DEV", _LEGACY_GUILD_ID)
|
||||||
|
GUILD_ID_ECONOMY = _env_int("GUILD_ID_ECONOMY", _LEGACY_GUILD_ID)
|
||||||
|
GUILD_ID = GUILD_ID_ECONOMY if BOT_PROFILE == "economy" else GUILD_ID_DEV
|
||||||
|
|
||||||
|
_LEGACY_BIRTHDAY_CHANNEL_ID = _env_int("BIRTHDAY_CHANNEL_ID", 0)
|
||||||
|
BIRTHDAY_CHANNEL_ID_DEV = _env_int("BIRTHDAY_CHANNEL_ID_DEV", _LEGACY_BIRTHDAY_CHANNEL_ID)
|
||||||
|
BIRTHDAY_CHANNEL_ID_ECONOMY = _env_int("BIRTHDAY_CHANNEL_ID_ECONOMY", 0)
|
||||||
|
BIRTHDAY_CHANNEL_ID = (
|
||||||
|
BIRTHDAY_CHANNEL_ID_ECONOMY
|
||||||
|
if BOT_PROFILE == "economy"
|
||||||
|
else BIRTHDAY_CHANNEL_ID_DEV
|
||||||
|
)
|
||||||
|
|
||||||
BIRTHDAY_WINDOW_DAYS = int(os.getenv("BIRTHDAY_WINDOW_DAYS", "7"))
|
BIRTHDAY_WINDOW_DAYS = int(os.getenv("BIRTHDAY_WINDOW_DAYS", "7"))
|
||||||
BASE_ROLE_IDS: list[int] = [1478304631930228779, 1478302278862766190]
|
BASE_ROLE_IDS: list[int] = [1478304631930228779, 1478302278862766190]
|
||||||
|
|
||||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
||||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
||||||
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
||||||
|
|
||||||
|
_LEGACY_PB_COLLECTION = os.getenv("PB_ECONOMY_COLLECTION", "").strip()
|
||||||
|
PB_ECONOMY_COLLECTION_DEV = (
|
||||||
|
os.getenv("PB_ECONOMY_COLLECTION_DEV", "").strip()
|
||||||
|
or (_LEGACY_PB_COLLECTION if _LEGACY_PB_COLLECTION else "economy_users_dev")
|
||||||
|
)
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY = (
|
||||||
|
os.getenv("PB_ECONOMY_COLLECTION_ECONOMY", "").strip()
|
||||||
|
or (_LEGACY_PB_COLLECTION if _LEGACY_PB_COLLECTION else "economy_users_prod")
|
||||||
|
)
|
||||||
|
PB_ECONOMY_COLLECTION = (
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY if BOT_PROFILE == "economy" else PB_ECONOMY_COLLECTION_DEV
|
||||||
|
)
|
||||||
|
|||||||
314
dev_member_commands.py
Normal file
314
dev_member_commands.py
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import sheets
|
||||||
|
import strings as S
|
||||||
|
from member_sync import announce_birthday, sync_member
|
||||||
|
|
||||||
|
|
||||||
|
class BirthdayPages(discord.ui.View):
|
||||||
|
def __init__(self, pages: list[discord.Embed], start: int = 0):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self.pages = pages
|
||||||
|
self.current = start
|
||||||
|
self._update_buttons()
|
||||||
|
|
||||||
|
def _update_buttons(self):
|
||||||
|
self.prev_button.disabled = self.current == 0
|
||||||
|
self.next_button.disabled = self.current >= len(self.pages) - 1
|
||||||
|
|
||||||
|
@discord.ui.button(label="◀", style=discord.ButtonStyle.secondary)
|
||||||
|
async def prev_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.current -= 1
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self.pages[self.current], view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="▶", style=discord.ButtonStyle.secondary)
|
||||||
|
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.current += 1
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self.pages[self.current], view=self)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_birthday_pages(
|
||||||
|
guild: discord.Guild | None = None,
|
||||||
|
) -> tuple[list[discord.Embed], int]:
|
||||||
|
"""Build 12 monthly embeds (one per calendar month).
|
||||||
|
|
||||||
|
Returns (pages, start_index) where start_index is the current month.
|
||||||
|
"""
|
||||||
|
rows = sheets.get_cache()
|
||||||
|
today = datetime.date.today()
|
||||||
|
|
||||||
|
by_month: dict[int, list[tuple[int, str, int | None]]] = {m: [] for m in range(1, 13)}
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
name = str(row.get("Nimi", "")).strip()
|
||||||
|
bday_str = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
if not name or not bday_str or bday_str.strip().lower() in ("-", "x", "n/a", "none", "ei"):
|
||||||
|
continue
|
||||||
|
bday = None
|
||||||
|
for fmt in ["%d/%m/%Y", "%Y-%m-%d", "%m-%d"]:
|
||||||
|
try:
|
||||||
|
bday = datetime.datetime.strptime(bday_str, fmt).date()
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if bday is None:
|
||||||
|
continue
|
||||||
|
raw_uid = str(row.get("User ID", "")).strip()
|
||||||
|
try:
|
||||||
|
uid = int(raw_uid) if raw_uid and raw_uid not in ("0", "-") else None
|
||||||
|
except ValueError:
|
||||||
|
uid = None
|
||||||
|
by_month[bday.month].append((bday.day, name, uid))
|
||||||
|
|
||||||
|
pages: list[discord.Embed] = []
|
||||||
|
for month in range(1, 13):
|
||||||
|
entries = sorted(by_month[month], key=lambda x: x[0])
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"🎂 {S.BIRTHDAY_MONTHS[month - 1]}",
|
||||||
|
color=0xF4A261,
|
||||||
|
)
|
||||||
|
if not entries:
|
||||||
|
embed.description = S.BIRTHDAY_UI["no_entries"]
|
||||||
|
else:
|
||||||
|
lines = []
|
||||||
|
for day, name, uid in entries:
|
||||||
|
try:
|
||||||
|
this_year = datetime.date(today.year, month, day)
|
||||||
|
except ValueError:
|
||||||
|
this_year = datetime.date(today.year, month, day - 1)
|
||||||
|
next_bday = this_year if this_year >= today else this_year.replace(year=today.year + 1)
|
||||||
|
days_until = (next_bday - today).days
|
||||||
|
if days_until == 0:
|
||||||
|
when = S.BIRTHDAY_UI["today"]
|
||||||
|
elif days_until == 1:
|
||||||
|
when = S.BIRTHDAY_UI["tomorrow"]
|
||||||
|
else:
|
||||||
|
when = S.BIRTHDAY_UI["in_days"].format(days=days_until)
|
||||||
|
display = name
|
||||||
|
if guild and uid:
|
||||||
|
member = guild.get_member(uid)
|
||||||
|
if member:
|
||||||
|
display = member.mention
|
||||||
|
lines.append(f"{display} - {day:02d}/{month:02d} · {when}")
|
||||||
|
embed.description = "\n".join(lines)
|
||||||
|
embed.set_footer(text=S.BIRTHDAY_UI["footer"].format(month=month, month_name=S.BIRTHDAY_MONTHS[month - 1]))
|
||||||
|
pages.append(embed)
|
||||||
|
|
||||||
|
return pages, today.month - 1
|
||||||
|
|
||||||
|
|
||||||
|
def _sheet_stats(rows: list[dict]) -> str:
|
||||||
|
"""Return a formatted string with sheet completeness statistics."""
|
||||||
|
total = len(rows)
|
||||||
|
missing_uid = []
|
||||||
|
missing_discord = []
|
||||||
|
missing_birthday = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
name = str(row.get("Nimi", "")).strip() or S.CHECK_UI["no_name"]
|
||||||
|
uid = str(row.get("User ID", "")).strip()
|
||||||
|
discord_name = str(row.get("Discord", "")).strip()
|
||||||
|
bday = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
|
||||||
|
if not uid or uid == "0":
|
||||||
|
missing_uid.append(name)
|
||||||
|
if not discord_name:
|
||||||
|
missing_discord.append(name)
|
||||||
|
if not bday:
|
||||||
|
missing_birthday.append(name)
|
||||||
|
|
||||||
|
lines = [S.CHECK_UI["sheet_stats_header"].format(total=total)]
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
def stat_line(label: str, missing: list[str]) -> str:
|
||||||
|
count = len(missing)
|
||||||
|
if count == 0:
|
||||||
|
return S.CHECK_UI["stat_ok"].format(label=label)
|
||||||
|
names = ", ".join(missing[:5])
|
||||||
|
more = S.CHECK_UI["stat_more"].format(count=count - 5) if count > 5 else ""
|
||||||
|
return S.CHECK_UI["stat_warn"].format(label=label, count=count, names=names, more=more)
|
||||||
|
|
||||||
|
lines.append(stat_line(S.CHECK_UI["stat_uid"], missing_uid))
|
||||||
|
lines.append(stat_line(S.CHECK_UI["stat_discord"], missing_discord))
|
||||||
|
lines.append(stat_line(S.CHECK_UI["stat_bday"], missing_birthday))
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def register_dev_member_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
has_announced_today: Callable[[int], bool],
|
||||||
|
mark_announced_today: Callable[[int], None],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="birthdays", description=S.CMD["birthdays"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_birthdays(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
sheets.refresh()
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(S.ERR["sheet_error"].format(error=e), ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
pages, start = _build_birthday_pages(guild=interaction.guild)
|
||||||
|
await interaction.followup.send(embed=pages[start], view=BirthdayPages(pages, start=start))
|
||||||
|
|
||||||
|
@tree.command(name="check", description=S.CMD["check"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_roles=True)
|
||||||
|
async def cmd_check(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
|
||||||
|
guild = interaction.guild
|
||||||
|
if guild is None:
|
||||||
|
await interaction.followup.send(S.ERR["guild_only"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = sheets.refresh()
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(S.ERR["sheet_error"].format(error=e), ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
ids_filled = 0
|
||||||
|
for row in data:
|
||||||
|
uid = str(row.get("User ID", "")).strip()
|
||||||
|
if uid and uid not in ("0", "-"):
|
||||||
|
continue
|
||||||
|
discord_name = str(row.get("Discord", "")).strip()
|
||||||
|
if not discord_name:
|
||||||
|
continue
|
||||||
|
guild_member = discord.utils.find(
|
||||||
|
lambda m, n=discord_name: m.name.lower() == n.lower(),
|
||||||
|
guild.members,
|
||||||
|
)
|
||||||
|
if guild_member:
|
||||||
|
sheets.set_user_id(discord_name, guild_member.id)
|
||||||
|
ids_filled += 1
|
||||||
|
|
||||||
|
data = sheets.get_cache()
|
||||||
|
|
||||||
|
changed_count = 0
|
||||||
|
not_found = 0
|
||||||
|
already_ok = 0
|
||||||
|
errors_total = 0
|
||||||
|
birthday_pings = 0
|
||||||
|
details: list[str] = []
|
||||||
|
sync_updates: list[tuple[int, bool]] = []
|
||||||
|
|
||||||
|
for member in guild.members:
|
||||||
|
if member.bot:
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = await sync_member(member, guild)
|
||||||
|
|
||||||
|
if result.not_found:
|
||||||
|
not_found += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
sync_updates.append((member.id, result.synced))
|
||||||
|
|
||||||
|
if result.errors:
|
||||||
|
errors_total += len(result.errors)
|
||||||
|
for err in result.errors:
|
||||||
|
details.append(S.CHECK_UI["detail_error"].format(error=err))
|
||||||
|
|
||||||
|
if result.changed:
|
||||||
|
changed_count += 1
|
||||||
|
parts = []
|
||||||
|
if result.nickname_changed:
|
||||||
|
parts.append(S.CHECK_UI["detail_nickname"])
|
||||||
|
if result.roles_added:
|
||||||
|
parts.append(S.CHECK_UI["detail_roles_added"].format(roles=", ".join(result.roles_added)))
|
||||||
|
details.append(S.CHECK_UI["detail_changed"].format(name=member.display_name, parts=", ".join(parts)))
|
||||||
|
else:
|
||||||
|
already_ok += 1
|
||||||
|
|
||||||
|
if result.birthday_soon and not has_announced_today(member.id):
|
||||||
|
birthday_pings += 1
|
||||||
|
await announce_birthday(member, bot)
|
||||||
|
mark_announced_today(member.id)
|
||||||
|
|
||||||
|
if sync_updates:
|
||||||
|
try:
|
||||||
|
sheets.batch_set_synced(sync_updates)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("/check batch_set_synced failed: %s", e)
|
||||||
|
|
||||||
|
summary_lines = [
|
||||||
|
S.CHECK_UI["done"],
|
||||||
|
S.CHECK_UI["already_ok"].format(count=already_ok),
|
||||||
|
S.CHECK_UI["fixed"].format(count=changed_count),
|
||||||
|
S.CHECK_UI["not_found"].format(count=not_found),
|
||||||
|
S.CHECK_UI["bday_pings"].format(count=birthday_pings),
|
||||||
|
]
|
||||||
|
if errors_total:
|
||||||
|
summary_lines.append(S.CHECK_UI["errors"].format(count=errors_total))
|
||||||
|
|
||||||
|
summary = "\n".join(summary_lines)
|
||||||
|
|
||||||
|
if details:
|
||||||
|
detail_text = "\n".join(details[:20])
|
||||||
|
summary += f"\n\n{S.CHECK_UI['details_header']}\n{detail_text}"
|
||||||
|
if len(details) > 20:
|
||||||
|
summary += "\n" + S.CHECK_UI["details_more"].format(count=len(details) - 20)
|
||||||
|
|
||||||
|
stats = _sheet_stats(data)
|
||||||
|
id_note = S.CHECK_UI["ids_filled"].format(count=ids_filled) if ids_filled else ""
|
||||||
|
summary = id_note + "\n" + summary + "\n\n" + stats
|
||||||
|
|
||||||
|
await interaction.followup.send(summary.strip(), ephemeral=True)
|
||||||
|
log.info(
|
||||||
|
"/check - OK=%d, Fixed=%d, NotFound=%d, IDs=%d, Errors=%d",
|
||||||
|
already_ok,
|
||||||
|
changed_count,
|
||||||
|
not_found,
|
||||||
|
ids_filled,
|
||||||
|
errors_total,
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="member", description=S.CMD["member"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_roles=True)
|
||||||
|
async def cmd_member(interaction: discord.Interaction, user: discord.Member):
|
||||||
|
row = sheets.find_member(user.id, user.name)
|
||||||
|
if row is None:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["member_not_found"].format(name=user.display_name),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(title=S.MEMBER_UI["title"].format(name=user.display_name), color=user.color)
|
||||||
|
|
||||||
|
bday_str = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
if bday_str and bday_str.lower() not in ("-", "x", "n/a", "none", "ei"):
|
||||||
|
for fmt in ["%d/%m/%Y", "%Y-%m-%d"]:
|
||||||
|
try:
|
||||||
|
bday = datetime.datetime.strptime(bday_str, fmt).date()
|
||||||
|
if 1920 <= bday.year <= datetime.date.today().year:
|
||||||
|
today = datetime.date.today()
|
||||||
|
age = today.year - bday.year - ((today.month, today.day) < (bday.month, bday.day))
|
||||||
|
embed.add_field(name=S.MEMBER_UI["age_field"], value=S.MEMBER_UI["age_val"].format(age=age), inline=True)
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for sheet_key, label in S.MEMBER_FIELDS:
|
||||||
|
val = str(row.get(sheet_key, "")).strip()
|
||||||
|
if val:
|
||||||
|
embed.add_field(name=label, value=val, inline=True)
|
||||||
|
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
93
dev_member_runtime.py
Normal file
93
dev_member_runtime.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
import config
|
||||||
|
import sheets
|
||||||
|
from member_sync import announce_birthday, is_birthday_today, sync_member
|
||||||
|
|
||||||
|
|
||||||
|
async def run_birthday_daily(
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
has_announced_today: Callable[[int], bool],
|
||||||
|
mark_announced_today: Callable[[int], None],
|
||||||
|
) -> None:
|
||||||
|
"""Announce birthdays in the configured guild for users whose birthday is today."""
|
||||||
|
guild = bot.get_guild(config.GUILD_ID)
|
||||||
|
if guild is None:
|
||||||
|
log.warning("Birthday task: guild %s not found", config.GUILD_ID)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = sheets.refresh()
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Birthday task: sheet refresh failed: %s", e)
|
||||||
|
data = sheets.get_cache()
|
||||||
|
|
||||||
|
announced = 0
|
||||||
|
for row in data:
|
||||||
|
bday_str = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
if not is_birthday_today(bday_str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
member = None
|
||||||
|
raw_id = str(row.get("User ID", "")).strip()
|
||||||
|
if raw_id:
|
||||||
|
try:
|
||||||
|
member = guild.get_member(int(raw_id))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if member is None:
|
||||||
|
discord_name = str(row.get("Discord", "")).strip()
|
||||||
|
if discord_name:
|
||||||
|
member = discord.utils.find(
|
||||||
|
lambda m, n=discord_name: m.name.lower() == n.lower(),
|
||||||
|
guild.members,
|
||||||
|
)
|
||||||
|
if member and not has_announced_today(member.id):
|
||||||
|
await announce_birthday(member, bot)
|
||||||
|
mark_announced_today(member.id)
|
||||||
|
announced += 1
|
||||||
|
|
||||||
|
log.info("Sünnipäeva ülesanne: teavitati %d liiget", announced)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_member_join(
|
||||||
|
member: discord.Member,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
has_announced_today: Callable[[int], bool],
|
||||||
|
mark_announced_today: Callable[[int], None],
|
||||||
|
log_sync_result: Callable[[discord.Member, object], None],
|
||||||
|
) -> None:
|
||||||
|
"""Sync a newly joined member against sheet data and trigger birthday notice if needed."""
|
||||||
|
log.info("Member joined: %s (ID: %s)", member, member.id)
|
||||||
|
|
||||||
|
if not sheets.get_cache():
|
||||||
|
sheets.refresh()
|
||||||
|
|
||||||
|
result = await sync_member(member, member.guild)
|
||||||
|
|
||||||
|
if result.not_found:
|
||||||
|
try:
|
||||||
|
sheets.add_new_member_row(member.name, member.id)
|
||||||
|
log.info(
|
||||||
|
" → %s not in sheet, added new row (Discord=%s, ID=%s)",
|
||||||
|
member,
|
||||||
|
member.name,
|
||||||
|
member.id,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(" → Failed to add sheet row for %s: %s", member, e)
|
||||||
|
return
|
||||||
|
|
||||||
|
log_sync_result(member, result)
|
||||||
|
sheets.set_synced(member.id, result.synced)
|
||||||
|
|
||||||
|
if result.birthday_soon and not has_announced_today(member.id):
|
||||||
|
await announce_birthday(member, bot)
|
||||||
|
mark_announced_today(member.id)
|
||||||
@@ -13,9 +13,14 @@ Create your admin account on first launch via the Admin UI.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Create the `economy_users` collection
|
## 2. Create economy collections for both bot profiles
|
||||||
|
|
||||||
In the Admin UI → **Collections** → **New collection** → name it exactly `economy_users`.
|
In the Admin UI → **Collections** → **New collection** and create:
|
||||||
|
|
||||||
|
- `economy_users_dev`
|
||||||
|
- `economy_users_prod`
|
||||||
|
|
||||||
|
Use the same schema for both collections.
|
||||||
|
|
||||||
Add the following fields:
|
Add the following fields:
|
||||||
|
|
||||||
@@ -51,6 +56,8 @@ Add to your `.env`:
|
|||||||
PB_URL=http://127.0.0.1:8090
|
PB_URL=http://127.0.0.1:8090
|
||||||
PB_ADMIN_EMAIL=your-admin@email.com
|
PB_ADMIN_EMAIL=your-admin@email.com
|
||||||
PB_ADMIN_PASSWORD=your-admin-password
|
PB_ADMIN_PASSWORD=your-admin-password
|
||||||
|
PB_ECONOMY_COLLECTION_DEV=economy_users_dev
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY=economy_users_prod
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
513
economy.py
513
economy.py
@@ -30,7 +30,10 @@ def _txn(event: str, **fields) -> None:
|
|||||||
# To use your custom Discord emoji replace COIN with the full tag, e.g.:
|
# To use your custom Discord emoji replace COIN with the full tag, e.g.:
|
||||||
# COIN = "<:tipicoin:1234567890123456789>"
|
# COIN = "<:tipicoin:1234567890123456789>"
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
COIN = "<:TipiCOIN:1483000209188589628>"
|
COIN = "<:TipiCOIN:1483000209188589628>"
|
||||||
|
PP_EMOJI = "<:TipiFIRE:1483431381668335687>"
|
||||||
|
PRESTIGE_ROLE = "TipiPRESTIGE"
|
||||||
|
PRESTIGE_MIN_LEVEL = 30 # minimum level required to prestige
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Shop catalogue
|
# Shop catalogue
|
||||||
@@ -141,13 +144,32 @@ SHOP: dict[str, ShopItem] = {
|
|||||||
"cost": 9000,
|
"cost": 9000,
|
||||||
"description": strings.ITEM_DESCRIPTIONS["gaming_tool"],
|
"description": strings.ITEM_DESCRIPTIONS["gaming_tool"],
|
||||||
},
|
},
|
||||||
|
# ----- Fishing items -----
|
||||||
|
"ussipurk": {
|
||||||
|
"name": "Ussipurk",
|
||||||
|
"emoji": "🪣",
|
||||||
|
"cost": 3500,
|
||||||
|
"description": strings.ITEM_DESCRIPTIONS["ussipurk"],
|
||||||
|
},
|
||||||
|
"kalavork": {
|
||||||
|
"name": "Kalavõrk",
|
||||||
|
"emoji": "🪝",
|
||||||
|
"cost": 5000,
|
||||||
|
"description": strings.ITEM_DESCRIPTIONS["kalavork"],
|
||||||
|
},
|
||||||
|
"echolood": {
|
||||||
|
"name": "Echolood",
|
||||||
|
"emoji": "📡",
|
||||||
|
"cost": 8000,
|
||||||
|
"description": strings.ITEM_DESCRIPTIONS["echolood"],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Tier grouping (used by /shop pagination)
|
# Tier grouping (used by /shop pagination)
|
||||||
SHOP_TIERS: dict[int, list[str]] = {
|
SHOP_TIERS: dict[int, list[str]] = {
|
||||||
1: ["gaming_hiir", "hiirematt", "korvaklapid", "lan_pass", "energiajook", "anticheat", "gaming_laptop"],
|
1: ["gaming_hiir", "hiirematt", "korvaklapid", "lan_pass", "energiajook", "anticheat", "gaming_laptop"],
|
||||||
2: ["reguleeritav_laud", "jellyfin", "mikrofon", "klaviatuur", "monitor", "cat6"],
|
2: ["reguleeritav_laud", "jellyfin", "mikrofon", "klaviatuur", "monitor", "cat6", "ussipurk"],
|
||||||
3: ["monitor_360", "karikas", "gaming_tool"],
|
3: ["monitor_360", "karikas", "gaming_tool", "kalavork", "echolood"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Minimum level required to purchase Tier 2 / Tier 3 shop items
|
# Minimum level required to purchase Tier 2 / Tier 3 shop items
|
||||||
@@ -158,11 +180,104 @@ SHOP_LEVEL_REQ: dict[str, int] = {
|
|||||||
"klaviatuur": 10,
|
"klaviatuur": 10,
|
||||||
"monitor": 10,
|
"monitor": 10,
|
||||||
"cat6": 10,
|
"cat6": 10,
|
||||||
|
"ussipurk": 10,
|
||||||
"monitor_360": 20,
|
"monitor_360": 20,
|
||||||
"karikas": 20,
|
"karikas": 20,
|
||||||
"gaming_tool": 20,
|
"gaming_tool": 20,
|
||||||
|
"kalavork": 20,
|
||||||
|
"echolood": 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Prestige shop catalogue
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class PrestigeItem(TypedDict):
|
||||||
|
emoji: str
|
||||||
|
max_level: int
|
||||||
|
pp_cost: int
|
||||||
|
effect: float
|
||||||
|
|
||||||
|
|
||||||
|
PRESTIGE_SHOP: dict[str, PrestigeItem] = {
|
||||||
|
"coin_mult": {
|
||||||
|
"emoji": "<:TipiCOIN:1483000209188589628>",
|
||||||
|
"max_level": 5,
|
||||||
|
"pp_cost": 5,
|
||||||
|
"effect": 0.08,
|
||||||
|
},
|
||||||
|
"exp_mult": {
|
||||||
|
"emoji": "✨",
|
||||||
|
"max_level": 5,
|
||||||
|
"pp_cost": 5,
|
||||||
|
"effect": 0.08,
|
||||||
|
},
|
||||||
|
"daily_plus": {
|
||||||
|
"emoji": "📅",
|
||||||
|
"max_level": 3,
|
||||||
|
"pp_cost": 7,
|
||||||
|
"effect": 0.20,
|
||||||
|
},
|
||||||
|
"work_plus": {
|
||||||
|
"emoji": "💼",
|
||||||
|
"max_level": 3,
|
||||||
|
"pp_cost": 7,
|
||||||
|
"effect": 0.20,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fish catalogue
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FISH_CATALOGUE: dict[str, dict] = {
|
||||||
|
# id: { rarity, weight=(min_g, max_g), coins=(min, max), exp }
|
||||||
|
"sarj": {"rarity": "common", "weight": (50, 500), "coins": (3, 18), "exp": 3},
|
||||||
|
"ahven": {"rarity": "common", "weight": (80, 700), "coins": (5, 22), "exp": 3},
|
||||||
|
"koger": {"rarity": "common", "weight": (100, 800), "coins": (5, 20), "exp": 3},
|
||||||
|
"viidikas": {"rarity": "common", "weight": (10, 120), "coins": (2, 8), "exp": 2},
|
||||||
|
"latikas": {"rarity": "uncommon", "weight": (300, 2500), "coins": (20, 70), "exp": 6},
|
||||||
|
"karpkala": {"rarity": "uncommon", "weight": (500, 4000), "coins": (25, 80), "exp": 7},
|
||||||
|
"linask": {"rarity": "uncommon", "weight": (200, 2000), "coins": (18, 60), "exp": 6},
|
||||||
|
"haug": {"rarity": "rare", "weight": (500, 6000), "coins": (50, 180), "exp": 10},
|
||||||
|
"angerjas": {"rarity": "rare", "weight": (200, 1800), "coins": (40, 120), "exp": 10},
|
||||||
|
"siig": {"rarity": "rare", "weight": (200, 2000), "coins": (45, 130), "exp": 10},
|
||||||
|
"forell": {"rarity": "epic", "weight": (400, 4500), "coins": (100, 280), "exp": 15},
|
||||||
|
"koha": {"rarity": "epic", "weight": (600, 7000), "coins": (120, 300), "exp": 15},
|
||||||
|
"tougjas": {"rarity": "epic", "weight": (400, 4000), "coins": (90, 250), "exp": 14},
|
||||||
|
"lohe": {"rarity": "legendary","weight": (1500, 12000), "coins": (250, 700), "exp": 25},
|
||||||
|
"vimb": {"rarity": "legendary","weight": (200, 1200), "coins": (200, 600), "exp": 25},
|
||||||
|
}
|
||||||
|
|
||||||
|
FISH_RARITY_WEIGHTS: dict[str, int] = {
|
||||||
|
"junk": 15,
|
||||||
|
"common": 45,
|
||||||
|
"uncommon": 22,
|
||||||
|
"rare": 12,
|
||||||
|
"epic": 5,
|
||||||
|
"legendary": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def roll_fish(rarity_bump: bool = False) -> tuple[str, int]:
|
||||||
|
"""Roll a random fish. Returns (fish_id, weight_grams) or ('junk', 0).
|
||||||
|
rarity_bump=True (kalavork item) shifts each catch one tier up.
|
||||||
|
"""
|
||||||
|
rarity_pool = list(FISH_RARITY_WEIGHTS.keys())
|
||||||
|
weights = list(FISH_RARITY_WEIGHTS.values())
|
||||||
|
chosen_rarity = random.choices(rarity_pool, weights=weights)[0]
|
||||||
|
if chosen_rarity == "junk":
|
||||||
|
return ("junk", 0)
|
||||||
|
if rarity_bump:
|
||||||
|
order = ["common", "uncommon", "rare", "epic", "legendary"]
|
||||||
|
idx = order.index(chosen_rarity) if chosen_rarity in order else 0
|
||||||
|
chosen_rarity = order[min(idx + 1, len(order) - 1)]
|
||||||
|
fish_of_rarity = [k for k, v in FISH_CATALOGUE.items() if v["rarity"] == chosen_rarity]
|
||||||
|
if not fish_of_rarity:
|
||||||
|
return ("junk", 0)
|
||||||
|
fish_id = random.choice(fish_of_rarity)
|
||||||
|
fish = FISH_CATALOGUE[fish_id]
|
||||||
|
weight = random.randint(fish["weight"][0], fish["weight"][1])
|
||||||
|
return (fish_id, weight)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# EXP / Level system
|
# EXP / Level system
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -227,6 +342,7 @@ COOLDOWNS: dict[str, timedelta] = {
|
|||||||
"beg": timedelta(minutes=5),
|
"beg": timedelta(minutes=5),
|
||||||
"crime": timedelta(hours=2),
|
"crime": timedelta(hours=2),
|
||||||
"rob": timedelta(hours=2),
|
"rob": timedelta(hours=2),
|
||||||
|
"fish": timedelta(minutes=2),
|
||||||
}
|
}
|
||||||
|
|
||||||
JAIL_DURATION = timedelta(minutes=30)
|
JAIL_DURATION = timedelta(minutes=30)
|
||||||
@@ -272,6 +388,16 @@ class UserData(TypedDict, total=False):
|
|||||||
total_received: int
|
total_received: int
|
||||||
best_daily_streak: int
|
best_daily_streak: int
|
||||||
heist_global_cd_until: float
|
heist_global_cd_until: float
|
||||||
|
# Prestige system
|
||||||
|
prestige_level: int
|
||||||
|
prestige_points: int
|
||||||
|
season_total_exp: int # cumulative EXP this season (survives prestige resets)
|
||||||
|
prestige_upgrades: dict # {upgrade_id: level}
|
||||||
|
# Fishing system
|
||||||
|
last_fish: str | None
|
||||||
|
fish_book: dict # {fish_id: times_caught}
|
||||||
|
total_fish_caught: int
|
||||||
|
fish_inventory: list # [{fish_id, weight, value}] - survives prestige
|
||||||
|
|
||||||
|
|
||||||
def _default_user() -> UserData:
|
def _default_user() -> UserData:
|
||||||
@@ -312,6 +438,16 @@ def _default_user() -> UserData:
|
|||||||
"total_received": 0,
|
"total_received": 0,
|
||||||
"best_daily_streak": 0,
|
"best_daily_streak": 0,
|
||||||
"heist_global_cd_until": 0.0,
|
"heist_global_cd_until": 0.0,
|
||||||
|
# ── Prestige ─────────────────────────────────────────────────────────
|
||||||
|
"prestige_level": 0,
|
||||||
|
"prestige_points": 0,
|
||||||
|
"season_total_exp": 0,
|
||||||
|
"prestige_upgrades": {},
|
||||||
|
# ── Fishing ──────────────────────────────────────────────────────────
|
||||||
|
"last_fish": None,
|
||||||
|
"fish_book": {},
|
||||||
|
"total_fish_caught": 0,
|
||||||
|
"fish_inventory": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -388,33 +524,6 @@ async def get_all_users_raw() -> dict[str, "UserData"]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def migrate_anticheat_uses() -> int:
|
|
||||||
"""One-time migration: users who own anticheat but have no item_uses entry get 2 uses."""
|
|
||||||
records = await pb_client.list_all_records()
|
|
||||||
changed = 0
|
|
||||||
for record in records:
|
|
||||||
items = record.get("items") or []
|
|
||||||
item_uses = record.get("item_uses") or {}
|
|
||||||
if "anticheat" in items and "anticheat" not in item_uses:
|
|
||||||
item_uses["anticheat"] = 2
|
|
||||||
await pb_client.update_record(record["id"], {"item_uses": item_uses})
|
|
||||||
changed += 1
|
|
||||||
return changed
|
|
||||||
|
|
||||||
|
|
||||||
async def migrate_reminders_default() -> int:
|
|
||||||
"""One-time migration: enable all reminders for users who have an empty list."""
|
|
||||||
_ALL_REMINDERS = ["daily", "work", "beg", "crime", "rob"]
|
|
||||||
records = await pb_client.list_all_records()
|
|
||||||
changed = 0
|
|
||||||
for record in records:
|
|
||||||
reminders = record.get("reminders")
|
|
||||||
if not reminders:
|
|
||||||
await pb_client.update_record(record["id"], {"reminders": _ALL_REMINDERS})
|
|
||||||
changed += 1
|
|
||||||
return changed
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Internal helpers
|
# Internal helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -498,6 +607,17 @@ async def get_leaderboard(top_n: int | None = 10) -> list[tuple[str, int]]:
|
|||||||
return result if top_n is None else result[:top_n]
|
return result if top_n is None else result[:top_n]
|
||||||
|
|
||||||
|
|
||||||
|
def _prestige_mult(user: UserData) -> tuple[float, float]:
|
||||||
|
"""Return (coin_mult, exp_mult) based on prestige upgrades. Both ≥1.0."""
|
||||||
|
upgrades: dict = user.get("prestige_upgrades") or {} # type: ignore[assignment]
|
||||||
|
coin_level = upgrades.get("coin_mult", 0)
|
||||||
|
exp_level = upgrades.get("exp_mult", 0)
|
||||||
|
return (
|
||||||
|
1.0 + coin_level * PRESTIGE_SHOP["coin_mult"]["effect"],
|
||||||
|
1.0 + exp_level * PRESTIGE_SHOP["exp_mult"]["effect"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_leaderboard_exp(top_n: int | None = 10) -> list[tuple[str, int, int]]:
|
async def get_leaderboard_exp(top_n: int | None = 10) -> list[tuple[str, int, int]]:
|
||||||
"""Return top_n (user_id_str, exp, level) sorted by EXP descending."""
|
"""Return top_n (user_id_str, exp, level) sorted by EXP descending."""
|
||||||
records = await pb_client.list_all_records()
|
records = await pb_client.list_all_records()
|
||||||
@@ -511,15 +631,18 @@ async def get_leaderboard_exp(top_n: int | None = 10) -> list[tuple[str, int, in
|
|||||||
|
|
||||||
|
|
||||||
async def award_exp(user_id: int, amount: int) -> dict:
|
async def award_exp(user_id: int, amount: int) -> dict:
|
||||||
"""Add EXP to a user. Returns old_level, new_level, total exp."""
|
"""Add EXP to a user. Applies prestige exp_mult. Returns old_level, new_level, total exp."""
|
||||||
user = await get_user(user_id)
|
user = await get_user(user_id)
|
||||||
|
_, exp_mult = _prestige_mult(user)
|
||||||
|
gained = max(1, int(amount * exp_mult))
|
||||||
old_exp = user.get("exp", 0)
|
old_exp = user.get("exp", 0)
|
||||||
new_exp = old_exp + amount
|
new_exp = old_exp + gained
|
||||||
old_level = get_level(old_exp)
|
old_level = get_level(old_exp)
|
||||||
new_level = get_level(new_exp)
|
new_level = get_level(new_exp)
|
||||||
user["exp"] = new_exp
|
user["exp"] = new_exp
|
||||||
|
user["season_total_exp"] = user.get("season_total_exp", 0) + gained
|
||||||
await _commit(user_id, user)
|
await _commit(user_id, user)
|
||||||
return {"old_level": old_level, "new_level": new_level, "exp": new_exp}
|
return {"old_level": old_level, "new_level": new_level, "exp": new_exp, "gained": gained}
|
||||||
|
|
||||||
|
|
||||||
async def do_season_reset(top_n: int = 10) -> list[tuple[str, int, int]]:
|
async def do_season_reset(top_n: int = 10) -> list[tuple[str, int, int]]:
|
||||||
@@ -541,8 +664,10 @@ async def do_season_reset(top_n: int = 10) -> list[tuple[str, int, int]]:
|
|||||||
"last_beg": None,
|
"last_beg": None,
|
||||||
"last_crime": None,
|
"last_crime": None,
|
||||||
"last_rob": None,
|
"last_rob": None,
|
||||||
|
"last_fish": None,
|
||||||
"daily_streak": 0,
|
"daily_streak": 0,
|
||||||
"last_streak_date": None,
|
"last_streak_date": None,
|
||||||
|
"season_total_exp": 0,
|
||||||
}
|
}
|
||||||
for record in records:
|
for record in records:
|
||||||
await pb_client.update_record(record["id"], reset_fields)
|
await pb_client.update_record(record["id"], reset_fields)
|
||||||
@@ -605,8 +730,13 @@ async def do_daily(user_id: int) -> dict:
|
|||||||
vip = "lan_pass" in user["items"]
|
vip = "lan_pass" in user["items"]
|
||||||
vip_mult = 2.0 if vip else 1.0
|
vip_mult = 2.0 if vip else 1.0
|
||||||
|
|
||||||
base = 150
|
daily_plus_level = (user.get("prestige_upgrades") or {}).get("daily_plus", 0)
|
||||||
|
base = int(150 * (1.0 + daily_plus_level * PRESTIGE_SHOP["daily_plus"]["effect"]))
|
||||||
earned = int(base * streak_mult * vip_mult)
|
earned = int(base * streak_mult * vip_mult)
|
||||||
|
if "korvaklapid" in user["items"]:
|
||||||
|
earned += 25
|
||||||
|
coin_mult, _ = _prestige_mult(user)
|
||||||
|
earned = int(earned * coin_mult)
|
||||||
|
|
||||||
# Investor interest (capped at 500/day to prevent runaway wealth)
|
# Investor interest (capped at 500/day to prevent runaway wealth)
|
||||||
interest = 0
|
interest = 0
|
||||||
@@ -661,7 +791,10 @@ async def do_work(user_id: int) -> dict:
|
|||||||
if "energiajook" in user["items"] and random.random() < 0.30:
|
if "energiajook" in user["items"] and random.random() < 0.30:
|
||||||
lucky = True
|
lucky = True
|
||||||
|
|
||||||
earned = int(base * job_mult * worker_mult * desk_mult * (3.0 if lucky else 1.0))
|
work_plus_level = (user.get("prestige_upgrades") or {}).get("work_plus", 0)
|
||||||
|
work_plus_mult = 1.0 + work_plus_level * PRESTIGE_SHOP["work_plus"]["effect"]
|
||||||
|
coin_mult, _ = _prestige_mult(user)
|
||||||
|
earned = int(base * job_mult * worker_mult * desk_mult * (3.0 if lucky else 1.0) * work_plus_mult * coin_mult)
|
||||||
user["balance"] += earned
|
user["balance"] += earned
|
||||||
user["last_work"] = _now().isoformat()
|
user["last_work"] = _now().isoformat()
|
||||||
user["work_count"] = user.get("work_count", 0) + 1
|
user["work_count"] = user.get("work_count", 0) + 1
|
||||||
@@ -699,7 +832,8 @@ async def do_beg(user_id: int) -> dict:
|
|||||||
|
|
||||||
jailed = bool(_is_jailed(user))
|
jailed = bool(_is_jailed(user))
|
||||||
beg_mult = 2 if "klaviatuur" in user["items"] else 1
|
beg_mult = 2 if "klaviatuur" in user["items"] else 1
|
||||||
earned = random.randint(10, 40) * beg_mult
|
coin_mult, _ = _prestige_mult(user)
|
||||||
|
earned = int(random.randint(10, 40) * beg_mult * coin_mult)
|
||||||
user["balance"] += earned
|
user["balance"] += earned
|
||||||
user["last_beg"] = _now().isoformat()
|
user["last_beg"] = _now().isoformat()
|
||||||
user["beg_count"] = user.get("beg_count", 0) + 1
|
user["beg_count"] = user.get("beg_count", 0) + 1
|
||||||
@@ -718,6 +852,262 @@ async def do_beg(user_id: int) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# /fish
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
async def do_fish_start(user_id: int) -> dict:
|
||||||
|
"""Check cooldown + jail, set cooldown. Call before starting the fishing minigame."""
|
||||||
|
user = await get_user(user_id)
|
||||||
|
if user.get("eco_banned"):
|
||||||
|
return {"ok": False, "reason": "banned"}
|
||||||
|
if jail := _is_jailed(user):
|
||||||
|
return {"ok": False, "reason": "jailed", "remaining": jail}
|
||||||
|
|
||||||
|
fish_cd = timedelta(seconds=90) if "ussipurk" in user["items"] else COOLDOWNS["fish"]
|
||||||
|
if cd := _cooldown_remaining(user, "fish", override_cd=fish_cd):
|
||||||
|
return {"ok": False, "reason": "cooldown", "remaining": cd}
|
||||||
|
|
||||||
|
user["last_fish"] = _now().isoformat()
|
||||||
|
await _commit(user_id, user)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
async def do_fish_resolve(user_id: int, fish_id: str, weight: int) -> dict:
|
||||||
|
"""Add catch to inventory + update fish_book. Returns catch info incl. pre-calculated value."""
|
||||||
|
user = await get_user(user_id)
|
||||||
|
|
||||||
|
if fish_id == "junk":
|
||||||
|
_txn("FISH_JUNK", user=user_id)
|
||||||
|
return {"ok": True, "type": "junk", "coins": 0, "exp": 0}
|
||||||
|
|
||||||
|
if fish_id not in FISH_CATALOGUE:
|
||||||
|
return {"ok": False, "reason": "invalid_fish"}
|
||||||
|
|
||||||
|
fish = FISH_CATALOGUE[fish_id]
|
||||||
|
min_c, max_c = fish["coins"]
|
||||||
|
w_min, w_max = fish["weight"]
|
||||||
|
weight_ratio = (weight - w_min) / max(1, w_max - w_min)
|
||||||
|
base_coins = int(min_c + weight_ratio * (max_c - min_c))
|
||||||
|
coin_mult, _ = _prestige_mult(user)
|
||||||
|
value = int(base_coins * coin_mult)
|
||||||
|
exp = fish["exp"]
|
||||||
|
|
||||||
|
book: dict = user.get("fish_book") or {}
|
||||||
|
prev_count = book.get(fish_id, 0)
|
||||||
|
book[fish_id] = prev_count + 1
|
||||||
|
user["fish_book"] = book
|
||||||
|
user["total_fish_caught"] = user.get("total_fish_caught", 0) + 1
|
||||||
|
|
||||||
|
inv: list = list(user.get("fish_inventory") or [])
|
||||||
|
inv.append({"fish_id": fish_id, "weight": weight, "value": value})
|
||||||
|
user["fish_inventory"] = inv
|
||||||
|
|
||||||
|
await _commit(user_id, user)
|
||||||
|
_txn("FISH", user=user_id, fish=fish_id, weight=weight, value=value)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"type": "fish",
|
||||||
|
"fish_id": fish_id,
|
||||||
|
"weight": weight,
|
||||||
|
"value": value,
|
||||||
|
"exp": exp,
|
||||||
|
"is_new": prev_count == 0,
|
||||||
|
"total_caught": book[fish_id],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def do_fish_sell(user_id: int, indices: list[int] | None = None) -> dict:
|
||||||
|
"""Sell fish from inventory. indices=None sells all. Returns coins earned."""
|
||||||
|
user = await get_user(user_id)
|
||||||
|
inv: list = list(user.get("fish_inventory") or [])
|
||||||
|
if not inv:
|
||||||
|
return {"ok": False, "reason": "empty"}
|
||||||
|
|
||||||
|
if indices is None:
|
||||||
|
to_sell = inv
|
||||||
|
remaining = []
|
||||||
|
else:
|
||||||
|
to_sell = [inv[i] for i in sorted(set(indices)) if 0 <= i < len(inv)]
|
||||||
|
keep_idx = set(range(len(inv))) - set(indices)
|
||||||
|
remaining = [inv[i] for i in sorted(keep_idx)]
|
||||||
|
|
||||||
|
if not to_sell:
|
||||||
|
return {"ok": False, "reason": "empty"}
|
||||||
|
|
||||||
|
total_coins = sum(entry["value"] for entry in to_sell)
|
||||||
|
user["fish_inventory"] = remaining
|
||||||
|
user["balance"] = user.get("balance", 0) + total_coins
|
||||||
|
user["lifetime_earned"] = user.get("lifetime_earned", 0) + total_coins
|
||||||
|
user["peak_balance"] = max(user.get("peak_balance", 0), user["balance"])
|
||||||
|
await _commit(user_id, user)
|
||||||
|
_txn("FISH_SELL", user=user_id, count=len(to_sell), coins=f"+{total_coins}", bal=user["balance"])
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"coins": total_coins,
|
||||||
|
"count": len(to_sell),
|
||||||
|
"balance": user["balance"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def do_fishbook(user_id: int) -> dict:
|
||||||
|
"""Return the user's fish book data including per-species inventory counts."""
|
||||||
|
user = await get_user(user_id)
|
||||||
|
book: dict = user.get("fish_book") or {}
|
||||||
|
inv: list = user.get("fish_inventory") or []
|
||||||
|
inv_counts: dict[str, int] = {}
|
||||||
|
for entry in inv:
|
||||||
|
fid = entry.get("fish_id", "")
|
||||||
|
inv_counts[fid] = inv_counts.get(fid, 0) + 1
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"book": book,
|
||||||
|
"inv_counts": inv_counts,
|
||||||
|
"total_fish_caught": user.get("total_fish_caught", 0),
|
||||||
|
"unique_caught": len(book),
|
||||||
|
"total_species": len(FISH_CATALOGUE),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# /prestige
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
async def do_prestige(user_id: int) -> dict:
|
||||||
|
"""Prestige: requires level 30, earns PP, resets balance/exp/items/cooldowns."""
|
||||||
|
user = await get_user(user_id)
|
||||||
|
if user.get("eco_banned"):
|
||||||
|
return {"ok": False, "reason": "banned"}
|
||||||
|
|
||||||
|
exp = user.get("exp", 0)
|
||||||
|
level = get_level(exp)
|
||||||
|
if level < PRESTIGE_MIN_LEVEL:
|
||||||
|
return {"ok": False, "reason": "level_too_low", "level": level, "required": PRESTIGE_MIN_LEVEL}
|
||||||
|
|
||||||
|
pp_earned = max(1, exp // 1000)
|
||||||
|
new_prestige_level = user.get("prestige_level", 0) + 1
|
||||||
|
|
||||||
|
# Preserve: fish_book, fish_inventory, lifetime stats, prestige_points, season_total_exp, prestige_upgrades
|
||||||
|
user["balance"] = 0
|
||||||
|
user["exp"] = 0
|
||||||
|
user["items"] = []
|
||||||
|
user["item_uses"] = {}
|
||||||
|
user["last_daily"] = None
|
||||||
|
user["last_work"] = None
|
||||||
|
user["last_beg"] = None
|
||||||
|
user["last_crime"] = None
|
||||||
|
user["last_rob"] = None
|
||||||
|
user["last_fish"] = None
|
||||||
|
user["last_heist"] = None
|
||||||
|
user["daily_streak"] = 0
|
||||||
|
user["last_streak_date"] = None
|
||||||
|
user["jailed_until"] = None
|
||||||
|
user["jailbreak_used"] = False
|
||||||
|
user["prestige_level"] = new_prestige_level
|
||||||
|
user["prestige_points"] = user.get("prestige_points", 0) + pp_earned
|
||||||
|
|
||||||
|
await _commit(user_id, user)
|
||||||
|
_txn("PRESTIGE", user=user_id, pp_earned=pp_earned, prestige=new_prestige_level, old_exp=exp)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"pp_earned": pp_earned,
|
||||||
|
"prestige_level": new_prestige_level,
|
||||||
|
"prestige_points": user["prestige_points"],
|
||||||
|
"old_exp": exp,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def do_prestige_buy(user_id: int, upgrade_id: str) -> dict:
|
||||||
|
"""Spend PP to buy a prestige upgrade level."""
|
||||||
|
if upgrade_id not in PRESTIGE_SHOP:
|
||||||
|
return {"ok": False, "reason": "not_found"}
|
||||||
|
|
||||||
|
user = await get_user(user_id)
|
||||||
|
if user.get("eco_banned"):
|
||||||
|
return {"ok": False, "reason": "banned"}
|
||||||
|
|
||||||
|
upgrade = PRESTIGE_SHOP[upgrade_id]
|
||||||
|
upgrades: dict = user.get("prestige_upgrades") or {}
|
||||||
|
current_level = upgrades.get(upgrade_id, 0)
|
||||||
|
|
||||||
|
if current_level >= upgrade["max_level"]:
|
||||||
|
return {"ok": False, "reason": "maxed", "max": upgrade["max_level"]}
|
||||||
|
|
||||||
|
pp = user.get("prestige_points", 0)
|
||||||
|
cost = upgrade["pp_cost"]
|
||||||
|
if pp < cost:
|
||||||
|
return {"ok": False, "reason": "insufficient_pp", "have": pp, "need": cost}
|
||||||
|
|
||||||
|
upgrades[upgrade_id] = current_level + 1
|
||||||
|
user["prestige_upgrades"] = upgrades
|
||||||
|
user["prestige_points"] = pp - cost
|
||||||
|
|
||||||
|
await _commit(user_id, user)
|
||||||
|
_txn("PRESTIGE_BUY", user=user_id, upgrade=upgrade_id,
|
||||||
|
new_level=upgrades[upgrade_id], pp_left=user["prestige_points"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"upgrade_id": upgrade_id,
|
||||||
|
"new_level": upgrades[upgrade_id],
|
||||||
|
"max_level": upgrade["max_level"],
|
||||||
|
"pp_remaining": user["prestige_points"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Extended leaderboards
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
async def get_leaderboard_season_exp(top_n: int | None = 10) -> list[tuple[str, int, int]]:
|
||||||
|
"""Return (user_id, season_total_exp, prestige_level) sorted by season EXP."""
|
||||||
|
records = await pb_client.list_all_records()
|
||||||
|
result = sorted(
|
||||||
|
(
|
||||||
|
(r["user_id"], r.get("season_total_exp", 0), r.get("prestige_level", 0))
|
||||||
|
for r in records if r.get("user_id")
|
||||||
|
),
|
||||||
|
key=lambda x: x[1],
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
return result if top_n is None else result[:top_n]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_leaderboard_prestige(top_n: int | None = 10) -> list[tuple[str, int, int]]:
|
||||||
|
"""Return (user_id, prestige_level, prestige_points) sorted by prestige_level then PP."""
|
||||||
|
records = await pb_client.list_all_records()
|
||||||
|
result = sorted(
|
||||||
|
(
|
||||||
|
(r["user_id"], r.get("prestige_level", 0), r.get("prestige_points", 0))
|
||||||
|
for r in records if r.get("user_id")
|
||||||
|
),
|
||||||
|
key=lambda x: (x[1], x[2]),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
return result if top_n is None else result[:top_n]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_leaderboard_wagered(top_n: int | None = 10) -> list[tuple[str, int]]:
|
||||||
|
"""Return (user_id, total_wagered) sorted descending."""
|
||||||
|
records = await pb_client.list_all_records()
|
||||||
|
result = sorted(
|
||||||
|
((r["user_id"], r.get("total_wagered", 0)) for r in records if r.get("user_id")),
|
||||||
|
key=lambda x: x[1],
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
return result if top_n is None else result[:top_n]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_leaderboard_fish(top_n: int | None = 10) -> list[tuple[str, int]]:
|
||||||
|
"""Return (user_id, total_fish_caught) sorted descending."""
|
||||||
|
records = await pb_client.list_all_records()
|
||||||
|
result = sorted(
|
||||||
|
((r["user_id"], r.get("total_fish_caught", 0)) for r in records if r.get("user_id")),
|
||||||
|
key=lambda x: x[1],
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
return result if top_n is None else result[:top_n]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# /crime
|
# /crime
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1171,6 +1561,57 @@ async def do_admin_inspect(target_id: int) -> dict:
|
|||||||
return {"ok": True, "data": dict(user)}
|
return {"ok": True, "data": dict(user)}
|
||||||
|
|
||||||
|
|
||||||
|
async def do_admin_exp(target_id: int, amount: int, admin_id: int, reason: str) -> dict:
|
||||||
|
"""Give (positive) or take (negative) EXP from a user. EXP is floored at 0."""
|
||||||
|
user = await get_user(target_id)
|
||||||
|
old_exp = user.get("exp", 0)
|
||||||
|
old_level = get_level(old_exp)
|
||||||
|
user["exp"] = max(0, old_exp + amount)
|
||||||
|
user["season_total_exp"] = max(0, user.get("season_total_exp", 0) + amount)
|
||||||
|
new_level = get_level(user["exp"])
|
||||||
|
await _commit(target_id, user)
|
||||||
|
verb = f"+{amount}" if amount >= 0 else str(amount)
|
||||||
|
_txn("ADMIN_EXP", admin=admin_id, target=target_id, amount=verb, reason=reason, exp=user["exp"])
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"exp": user["exp"],
|
||||||
|
"change": amount,
|
||||||
|
"old_level": old_level,
|
||||||
|
"new_level": new_level,
|
||||||
|
"level_changed": new_level != old_level,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def do_admin_item(target_id: int, item_id: str, action: str, admin_id: int) -> dict:
|
||||||
|
"""Give or remove an item. action='give'|'remove'. Returns ok/reason."""
|
||||||
|
if item_id not in SHOP:
|
||||||
|
return {"ok": False, "reason": "invalid_item"}
|
||||||
|
user = await get_user(target_id)
|
||||||
|
items: list = list(user.get("items") or [])
|
||||||
|
item_uses: dict = dict(user.get("item_uses") or {})
|
||||||
|
if action == "give":
|
||||||
|
if item_id not in items:
|
||||||
|
items.append(item_id)
|
||||||
|
if item_id == "anticheat":
|
||||||
|
item_uses["anticheat"] = 2
|
||||||
|
user["items"] = items
|
||||||
|
user["item_uses"] = item_uses
|
||||||
|
await _commit(target_id, user)
|
||||||
|
_txn("ADMIN_ITEM_GIVE", admin=admin_id, target=target_id, item=item_id)
|
||||||
|
return {"ok": True, "action": "given", "item_id": item_id}
|
||||||
|
elif action == "remove":
|
||||||
|
if item_id not in items:
|
||||||
|
return {"ok": False, "reason": "not_owned"}
|
||||||
|
items.remove(item_id)
|
||||||
|
item_uses.pop(item_id, None)
|
||||||
|
user["items"] = items
|
||||||
|
user["item_uses"] = item_uses
|
||||||
|
await _commit(target_id, user)
|
||||||
|
_txn("ADMIN_ITEM_REMOVE", admin=admin_id, target=target_id, item=item_id)
|
||||||
|
return {"ok": True, "action": "removed", "item_id": item_id}
|
||||||
|
return {"ok": False, "reason": "invalid_action"}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# /reminders
|
# /reminders
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
310
economy_admin_commands.py
Normal file
310
economy_admin_commands.py
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
async def _dm_user(bot: discord.Client, user_id: int, msg: str) -> None:
|
||||||
|
"""Best-effort DM to a user."""
|
||||||
|
try:
|
||||||
|
user = bot.get_user(user_id) or await bot.fetch_user(user_id)
|
||||||
|
await user.send(msg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_admin_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
cd_ts: Callable[[datetime.timedelta], str],
|
||||||
|
apply_level_role: Callable[[discord.Member, int, int], Awaitable[None]],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="adminseason", description=S.CMD["adminseason"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
@app_commands.describe(top_n=S.OPT["adminseason_top_n"])
|
||||||
|
async def cmd_adminseason(interaction: discord.Interaction, top_n: int = 10):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
top = await economy.do_season_reset(top_n)
|
||||||
|
guild = interaction.guild
|
||||||
|
|
||||||
|
if guild:
|
||||||
|
all_role_names = {name for _, name in economy.LEVEL_ROLES}
|
||||||
|
for role_name in all_role_names:
|
||||||
|
role = discord.utils.find(lambda r: r.name == role_name, guild.roles)
|
||||||
|
if not role:
|
||||||
|
continue
|
||||||
|
for member in list(role.members):
|
||||||
|
try:
|
||||||
|
await member.remove_roles(role, reason="Season reset")
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
|
medals = ["🥇", "🥈", "🥉"]
|
||||||
|
lines = []
|
||||||
|
for i, (uid, exp, lvl) in enumerate(top):
|
||||||
|
member = guild.get_member(int(uid)) if guild else None
|
||||||
|
name = member.display_name if member else S.LEADERBOARD_UI["unknown_user"].format(uid=uid)
|
||||||
|
prefix = medals[i] if i < 3 else f"**{i + 1}.**"
|
||||||
|
lines.append(S.SEASON["entry"].format(prefix=prefix, name=name, exp=exp, level=lvl))
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["adminseason"],
|
||||||
|
description=(S.SEASON["top_header"] + "\n" + "\n".join(lines)) if lines else S.SEASON["no_players"],
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
embed.set_footer(text=S.SEASON["footer"])
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=False)
|
||||||
|
await interaction.followup.send(S.SEASON["done"], ephemeral=True)
|
||||||
|
log.info("/adminseason triggered by %s (top_n=%d)", interaction.user, top_n)
|
||||||
|
|
||||||
|
@tree.command(name="admincoins", description=S.CMD["admincoins"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
kogus=S.OPT["admincoins_kogus"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_admincoins(interaction: discord.Interaction, kasutaja: discord.Member, kogus: int, põhjus: str):
|
||||||
|
if kogus == 0:
|
||||||
|
await interaction.response.send_message(S.ERR["admincoins_zero"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_admin_coins(kasutaja.id, kogus, interaction.user.id, põhjus)
|
||||||
|
verb = f"+{kogus}" if kogus > 0 else str(kogus)
|
||||||
|
emoji = "💰" if kogus > 0 else "💸"
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["coins_done"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=kasutaja.display_name,
|
||||||
|
verb=verb,
|
||||||
|
coin=economy.COIN,
|
||||||
|
balance=f"{res['balance']:,}",
|
||||||
|
reason=põhjus,
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(
|
||||||
|
bot,
|
||||||
|
kasutaja.id,
|
||||||
|
S.ADMIN["coins_dm"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
verb=verb,
|
||||||
|
coin=economy.COIN,
|
||||||
|
reason=põhjus,
|
||||||
|
balance=f"{res['balance']:,}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
log.info("ADMINCOINS %s → %s (%s) by %s", verb, kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminjail", description=S.CMD["adminjail"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
minutid=S.OPT["adminjail_minutid"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminjail(interaction: discord.Interaction, kasutaja: discord.Member, minutid: int, põhjus: str):
|
||||||
|
if minutid <= 0:
|
||||||
|
await interaction.response.send_message(S.ERR["positive_duration"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await economy.do_admin_jail(kasutaja.id, minutid, interaction.user.id, põhjus)
|
||||||
|
until_ts = cd_ts(datetime.timedelta(minutes=minutid))
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["jail_done"].format(name=kasutaja.display_name, minutes=minutid, ts=until_ts, reason=põhjus),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(
|
||||||
|
bot,
|
||||||
|
kasutaja.id,
|
||||||
|
S.ADMIN["jail_dm"].format(minutes=minutid, reason=põhjus, ts=until_ts),
|
||||||
|
)
|
||||||
|
log.info("ADMINJAIL %s %dmin (%s) by %s", kasutaja, minutid, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminunjail", description=S.CMD["adminunjail"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminunjail(interaction: discord.Interaction, kasutaja: discord.Member):
|
||||||
|
await economy.do_admin_unjail(kasutaja.id, interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["unjail_done"].format(name=kasutaja.display_name), ephemeral=True
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["unjail_dm"])
|
||||||
|
log.info("ADMINUNJAIL %s by %s", kasutaja, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminban", description=S.CMD["adminban"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminban(interaction: discord.Interaction, kasutaja: discord.Member, põhjus: str):
|
||||||
|
if bot.user and kasutaja.id == bot.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["admin_ban_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await economy.do_admin_ban(kasutaja.id, interaction.user.id, põhjus)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["ban_done"].format(name=kasutaja.display_name, reason=põhjus),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["ban_dm"].format(reason=põhjus))
|
||||||
|
log.info("ADMINBAN %s (%s) by %s", kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminunban", description=S.CMD["adminunban"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminunban(interaction: discord.Interaction, kasutaja: discord.Member):
|
||||||
|
await economy.do_admin_unban(kasutaja.id, interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["unban_done"].format(name=kasutaja.display_name), ephemeral=True
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["unban_dm"])
|
||||||
|
log.info("ADMINUNBAN %s by %s", kasutaja, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminreset", description=S.CMD["adminreset"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminreset(interaction: discord.Interaction, kasutaja: discord.Member, põhjus: str):
|
||||||
|
if bot.user and kasutaja.id == bot.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["admin_reset_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await economy.do_admin_reset(kasutaja.id, interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["reset_done"].format(name=kasutaja.display_name, reason=põhjus),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["reset_dm"].format(reason=põhjus))
|
||||||
|
log.info("ADMINRESET %s (%s) by %s", kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminview", description=S.CMD["adminview"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminview(interaction: discord.Interaction, kasutaja: discord.Member):
|
||||||
|
res = await economy.do_admin_inspect(kasutaja.id)
|
||||||
|
data = res["data"]
|
||||||
|
items_str = ", ".join(data.get("items", [])) or "-"
|
||||||
|
uses = data.get("item_uses", {})
|
||||||
|
uses_str = ", ".join(f"{k}:{v}" for k, v in uses.items()) if uses else "-"
|
||||||
|
jailed = data.get("jailed_until") or "-"
|
||||||
|
banned = S.ADMINVIEW_UI["banned_yes"] if data.get("eco_banned") else S.ADMINVIEW_UI["banned_no"]
|
||||||
|
exp = data.get("exp", 0)
|
||||||
|
level = economy.get_level(exp)
|
||||||
|
prestige_lvl = data.get("prestige_level", 0)
|
||||||
|
prestige_pp = data.get("prestige_points", 0)
|
||||||
|
total_fish = data.get("total_fish_caught", 0)
|
||||||
|
inv_fish = len(data.get("fish_inventory") or [])
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.ADMINVIEW_UI["title"].format(name=kasutaja.display_name),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_balance"], value=f"{data.get('balance', 0):,} {economy.COIN}", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_exp"], value=S.ADMINVIEW_UI["exp_val"].format(exp=f"{exp:,}", level=level), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_streak"], value=str(data.get("daily_streak", 0)), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_banned"], value=banned, inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_jailed"], value=jailed, inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_prestige"], value=S.ADMINVIEW_UI["prestige_val"].format(level=prestige_lvl, pp=prestige_pp), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_fish"], value=S.ADMINVIEW_UI["fish_val"].format(caught=total_fish, inv=inv_fish), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_items"], value=items_str, inline=False)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_uses"], value=uses_str, inline=False)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_daily"], value=data.get("last_daily") or "-", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_work"], value=data.get("last_work") or "-", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_crime"], value=data.get("last_crime") or "-", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_fish"], value=data.get("last_fish") or "-", inline=True)
|
||||||
|
embed.set_footer(text=S.ADMINVIEW_UI["footer"].format(uid=kasutaja.id))
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
log.info("ADMINVIEW %s by %s", kasutaja, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminexp", description=S.CMD["adminexp"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
kogus=S.OPT["adminexp_kogus"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminexp(interaction: discord.Interaction, kasutaja: discord.Member, kogus: int, põhjus: str):
|
||||||
|
if kogus == 0:
|
||||||
|
await interaction.response.send_message(S.ERR["admincoins_zero"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_admin_exp(kasutaja.id, kogus, interaction.user.id, põhjus)
|
||||||
|
verb = f"+{kogus}" if kogus > 0 else str(kogus)
|
||||||
|
emoji = "📈" if kogus > 0 else "📉"
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["exp_done"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=kasutaja.display_name,
|
||||||
|
verb=verb,
|
||||||
|
exp=f"{res['exp']:,}",
|
||||||
|
level=res["new_level"],
|
||||||
|
reason=põhjus,
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(
|
||||||
|
bot,
|
||||||
|
kasutaja.id,
|
||||||
|
S.ADMIN["exp_dm"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
verb=verb,
|
||||||
|
reason=põhjus,
|
||||||
|
exp=f"{res['exp']:,}",
|
||||||
|
level=res["new_level"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if res["level_changed"]:
|
||||||
|
member = interaction.guild.get_member(kasutaja.id) if interaction.guild else None
|
||||||
|
if member:
|
||||||
|
await apply_level_role(member, res["new_level"], res["old_level"])
|
||||||
|
log.info("ADMINEXP %s → %s (%s) by %s", verb, kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminitem", description=S.CMD["adminitem"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
ese=S.OPT["adminitem_ese"],
|
||||||
|
tegevus=S.OPT["adminitem_tegevus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminitem(interaction: discord.Interaction, kasutaja: discord.Member, ese: str, tegevus: str):
|
||||||
|
action = tegevus.strip().lower()
|
||||||
|
if action not in ("anna", "eemalda"):
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_invalid"].format(item_id=tegevus), ephemeral=True)
|
||||||
|
return
|
||||||
|
action_key = "give" if action == "anna" else "remove"
|
||||||
|
res = await economy.do_admin_item(kasutaja.id, ese, action_key, interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "invalid_item":
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_invalid"].format(item_id=ese), ephemeral=True)
|
||||||
|
elif res["reason"] == "not_owned":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["item_not_owned"].format(name=kasutaja.display_name, item_id=ese),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(S.ERR["generic_error"].format(error=res["reason"]), ephemeral=True)
|
||||||
|
return
|
||||||
|
item_name = economy.SHOP[ese]["name"]
|
||||||
|
if action_key == "give":
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_given"].format(item=item_name, name=kasutaja.display_name), ephemeral=True)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["item_dm_given"].format(item=item_name))
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_removed"].format(item=item_name, name=kasutaja.display_name), ephemeral=True)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["item_dm_removed"].format(item=item_name))
|
||||||
|
log.info("ADMINITEM %s %s %s by %s", action_key, ese, kasutaja, interaction.user)
|
||||||
883
economy_extra_commands.py
Normal file
883
economy_extra_commands.py
Normal file
@@ -0,0 +1,883 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from collections.abc import Awaitable, Callable, MutableSet
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_extra_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
coin: Callable[[int], str],
|
||||||
|
cd_ts: Callable[[datetime.timedelta], str],
|
||||||
|
parse_amount: Callable[[str, int], tuple[int | None, str | None]],
|
||||||
|
ensure_level_role: Callable[[discord.Member, int], Awaitable[None]],
|
||||||
|
active_games: MutableSet[int],
|
||||||
|
) -> None:
|
||||||
|
active_heist = None
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# /heist - multiplayer group robbery of the house
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
_HEIST_JOIN_WINDOW = 300 # seconds players have to join
|
||||||
|
_HEIST_MIN_PLAYERS = 2
|
||||||
|
_HEIST_GLOBAL_CD = 14400 # seconds between heist events server-wide (4h)
|
||||||
|
_HEIST_MAX_PLAYERS = 8
|
||||||
|
_HEIST_BASE_CHANCE = 0.35 # 35% solo
|
||||||
|
_HEIST_CHANCE_STEP = 0.05 # +5% per extra player
|
||||||
|
_HEIST_MAX_CHANCE = 0.65 # cap at 65%
|
||||||
|
|
||||||
|
def _build_heist_story(participants: list[discord.Member], success: bool) -> list[str]:
|
||||||
|
"""Return a list of story lines for the heist narrative reveal."""
|
||||||
|
story = S.HEIST_STORY
|
||||||
|
leader = participants[0].display_name
|
||||||
|
if len(participants) == 1:
|
||||||
|
names = f"**{leader}**"
|
||||||
|
elif len(participants) == 2:
|
||||||
|
names = S.HEIST_UI["names_duo"].format(
|
||||||
|
a=participants[0].display_name,
|
||||||
|
b=participants[1].display_name,
|
||||||
|
)
|
||||||
|
elif len(participants) <= 4:
|
||||||
|
names = S.HEIST_UI["names_sep"].join(f"**{p.display_name}**" for p in participants)
|
||||||
|
else:
|
||||||
|
names = S.HEIST_UI["names_crew"].format(leader=participants[0].display_name)
|
||||||
|
|
||||||
|
vehicle = random.choice(story["vehicles"])
|
||||||
|
approach = random.choice(["sneaky", "loud"])
|
||||||
|
non_leaders = participants[1:] if len(participants) > 1 else participants
|
||||||
|
|
||||||
|
def fill(tmpl: str) -> str:
|
||||||
|
picked = random.choice(non_leaders).display_name
|
||||||
|
return tmpl.format(
|
||||||
|
leader=f"**{leader}**",
|
||||||
|
member=f"**{picked}**",
|
||||||
|
names=names,
|
||||||
|
vehicle=vehicle,
|
||||||
|
)
|
||||||
|
|
||||||
|
getaway_pool = "getaway_success" if success else "getaway_fail"
|
||||||
|
|
||||||
|
return [
|
||||||
|
fill(random.choice(story["arrival"])),
|
||||||
|
fill(random.choice(story[f"entry_{approach}"])),
|
||||||
|
fill(random.choice(story["inside"])),
|
||||||
|
fill(random.choice(story["vault"])),
|
||||||
|
fill(random.choice(story["vault_open"])),
|
||||||
|
fill(random.choice(story["police_inbound"])),
|
||||||
|
fill(random.choice(story[getaway_pool])),
|
||||||
|
fill(random.choice(story["escape_success" if success else "escape_fail"])),
|
||||||
|
]
|
||||||
|
|
||||||
|
class HeistLobbyView(discord.ui.View):
|
||||||
|
def __init__(self, organizer: discord.Member, organizer_has_jellyfin: bool = False):
|
||||||
|
super().__init__(timeout=_HEIST_JOIN_WINDOW)
|
||||||
|
self.organizer = organizer
|
||||||
|
self.participants: list[discord.Member] = [organizer]
|
||||||
|
self.message: discord.Message | None = None
|
||||||
|
self.resolved = False
|
||||||
|
self.jellyfin_holders: int = 1 if organizer_has_jellyfin else 0
|
||||||
|
|
||||||
|
def _chance(self) -> float:
|
||||||
|
n = len(self.participants)
|
||||||
|
base = min(_HEIST_BASE_CHANCE + _HEIST_CHANCE_STEP * (n - 1), _HEIST_MAX_CHANCE)
|
||||||
|
jelly_bonus = 0.05 if self.jellyfin_holders > 0 else 0.0
|
||||||
|
return min(base + jelly_bonus, _HEIST_MAX_CHANCE)
|
||||||
|
|
||||||
|
def _lobby_embed(self) -> discord.Embed:
|
||||||
|
names = "\n".join(f"• {p.display_name}" for p in self.participants)
|
||||||
|
desc = S.HEIST_UI["lobby_desc"].format(
|
||||||
|
n=len(self.participants),
|
||||||
|
max=_HEIST_MAX_PLAYERS,
|
||||||
|
names=names,
|
||||||
|
chance=int(self._chance() * 100),
|
||||||
|
ts=int(self._timeout_expiry()),
|
||||||
|
)
|
||||||
|
return discord.Embed(title=S.TITLE["heist_lobby"], description=desc, color=0xE67E22)
|
||||||
|
|
||||||
|
def _timeout_expiry(self) -> float:
|
||||||
|
return time.time() + (self.timeout or 0)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.HEIST_UI["btn_join"], style=discord.ButtonStyle.danger)
|
||||||
|
async def join(self, interaction: discord.Interaction, _: discord.ui.Button):
|
||||||
|
if any(p.id == interaction.user.id for p in self.participants):
|
||||||
|
await interaction.response.send_message(S.HEIST_UI["already_joined"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if len(self.participants) >= _HEIST_MAX_PLAYERS:
|
||||||
|
await interaction.response.send_message(S.ERR["heist_full"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if interaction.user.id in active_games:
|
||||||
|
await interaction.response.send_message(S.ERR["already_in_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_heist_check(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])), ephemeral=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["heist"].format(ts=cd_ts(res["remaining"])), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.participants.append(interaction.user)
|
||||||
|
active_games.add(interaction.user.id)
|
||||||
|
joiner_data = await economy.get_user(interaction.user.id)
|
||||||
|
if "jellyfin" in joiner_data.get("items", []):
|
||||||
|
self.jellyfin_holders += 1
|
||||||
|
await interaction.response.edit_message(embed=self._lobby_embed())
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.HEIST_UI["btn_start"], style=discord.ButtonStyle.success)
|
||||||
|
async def start_now(self, interaction: discord.Interaction, _: discord.ui.Button):
|
||||||
|
if interaction.user.id != self.organizer.id:
|
||||||
|
await interaction.response.send_message(S.HEIST_UI["only_organizer"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if len(self.participants) < _HEIST_MIN_PLAYERS:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["heist_min_players"].format(min=_HEIST_MIN_PLAYERS), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await self._resolve(interaction)
|
||||||
|
|
||||||
|
async def _resolve(self, interaction: discord.Interaction | None = None) -> None:
|
||||||
|
nonlocal active_heist
|
||||||
|
if self.resolved:
|
||||||
|
return
|
||||||
|
self.resolved = True
|
||||||
|
active_heist = None
|
||||||
|
self.stop()
|
||||||
|
self.clear_items()
|
||||||
|
|
||||||
|
for p in self.participants:
|
||||||
|
active_games.discard(p.id)
|
||||||
|
|
||||||
|
n = len(self.participants)
|
||||||
|
channel = interaction.channel if interaction else self.message.channel if self.message else None
|
||||||
|
|
||||||
|
if n < _HEIST_MIN_PLAYERS:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["heist_cancel"],
|
||||||
|
description=S.HEIST_UI["cancel_desc"].format(min=_HEIST_MIN_PLAYERS),
|
||||||
|
color=0x99AAB5,
|
||||||
|
)
|
||||||
|
if interaction and not interaction.response.is_done():
|
||||||
|
await interaction.response.edit_message(embed=embed, view=self)
|
||||||
|
elif self.message:
|
||||||
|
try:
|
||||||
|
await self.message.edit(embed=embed, view=self)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
success = random.random() < self._chance()
|
||||||
|
story_lines = _build_heist_story(self.participants, success)
|
||||||
|
|
||||||
|
lobby_done = discord.Embed(
|
||||||
|
title=S.HEIST_UI["started_title"],
|
||||||
|
description=S.HEIST_UI["started_desc"].format(n=n),
|
||||||
|
color=0x99AAB5,
|
||||||
|
)
|
||||||
|
if interaction and not interaction.response.is_done():
|
||||||
|
await interaction.response.edit_message(embed=lobby_done, view=self)
|
||||||
|
elif self.message:
|
||||||
|
try:
|
||||||
|
await self.message.edit(embed=lobby_done, view=self)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
story_embed = discord.Embed(title=S.HEIST_UI["story_title"], description="", color=0xE67E22)
|
||||||
|
story_msg = await channel.send(embed=story_embed)
|
||||||
|
accumulated = ""
|
||||||
|
for i, line in enumerate(story_lines):
|
||||||
|
await asyncio.sleep(random.uniform(3.0, 4.5))
|
||||||
|
accumulated += ("\n\n" if i > 0 else "") + line
|
||||||
|
story_embed.description = accumulated
|
||||||
|
try:
|
||||||
|
await story_msg.edit(embed=story_embed)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
|
res = await economy.do_heist_resolve([p.id for p in self.participants], success)
|
||||||
|
payout_each = res["payout_each"]
|
||||||
|
names_str = "\n".join(f"• {p.display_name}" for p in self.participants)
|
||||||
|
guild = interaction.guild if interaction else self.message.guild if self.message else None
|
||||||
|
|
||||||
|
if success:
|
||||||
|
result_desc = S.HEIST_UI["win_desc"].format(names=names_str, payout=coin(payout_each))
|
||||||
|
result_embed = discord.Embed(
|
||||||
|
title=S.TITLE["heist_win"],
|
||||||
|
description=result_desc,
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
for p in self.participants:
|
||||||
|
exp_res = await economy.award_exp(p.id, economy.EXP_REWARDS["heist_win"])
|
||||||
|
if exp_res["old_level"] != exp_res["new_level"] and guild:
|
||||||
|
gm = guild.get_member(p.id)
|
||||||
|
if gm:
|
||||||
|
asyncio.create_task(ensure_level_role(gm, exp_res["new_level"]))
|
||||||
|
else:
|
||||||
|
result_desc = S.HEIST_UI["fail_desc"].format(names=names_str)
|
||||||
|
result_embed = discord.Embed(
|
||||||
|
title=S.TITLE["heist_fail"],
|
||||||
|
description=result_desc,
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
|
||||||
|
await economy.set_heist_global_cd(time.time() + _HEIST_GLOBAL_CD)
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
await channel.send(embed=result_embed)
|
||||||
|
elif self.message:
|
||||||
|
try:
|
||||||
|
await self.message.channel.send(embed=result_embed)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_timeout(self) -> None:
|
||||||
|
await self._resolve()
|
||||||
|
|
||||||
|
@tree.command(name="heist", description=S.CMD["heist"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_heist(interaction: discord.Interaction):
|
||||||
|
nonlocal active_heist
|
||||||
|
if active_heist is not None:
|
||||||
|
await interaction.response.send_message(S.ERR["heist_active"], ephemeral=True)
|
||||||
|
return
|
||||||
|
heist_cd = await economy.get_heist_global_cd()
|
||||||
|
if time.time() < heist_cd:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["heist_global"].format(
|
||||||
|
ts=cd_ts(datetime.timedelta(seconds=heist_cd - time.time()))
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if interaction.user.id in active_games:
|
||||||
|
await interaction.response.send_message(S.ERR["already_in_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_heist_check(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
organizer_data = await economy.get_user(interaction.user.id)
|
||||||
|
view = HeistLobbyView(interaction.user, "jellyfin" in organizer_data.get("items", []))
|
||||||
|
active_heist = view
|
||||||
|
active_games.add(interaction.user.id)
|
||||||
|
await interaction.response.send_message(embed=view._lobby_embed(), view=view)
|
||||||
|
view.message = await interaction.original_response()
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# /jailbreak - Monopoly-style dice escape
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
_DICE_EMOJI = [
|
||||||
|
"<:TipiYKS:1483103190491856916>",
|
||||||
|
"<:TipiKAKS:1483103215841972404>",
|
||||||
|
"<:TipiKOLM:1483103217846980781>",
|
||||||
|
"<:TipiNELI:1483103237585240114>",
|
||||||
|
"<:TipiVIIS:1483103239036469289>",
|
||||||
|
"<:TipiKUUS:1483103253163020348>",
|
||||||
|
]
|
||||||
|
|
||||||
|
class JailbreakView(discord.ui.View):
|
||||||
|
MAX_TRIES = 3
|
||||||
|
|
||||||
|
def __init__(self, user_id: int):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self.user_id = user_id
|
||||||
|
self.tries = 0
|
||||||
|
self._rolling = False
|
||||||
|
self._add_roll_btn()
|
||||||
|
|
||||||
|
def _add_roll_btn(self):
|
||||||
|
self.clear_items()
|
||||||
|
btn = discord.ui.Button(
|
||||||
|
label=S.JAILBREAK_UI["btn_roll"].format(try_=self.tries + 1, max=self.MAX_TRIES),
|
||||||
|
style=discord.ButtonStyle.primary,
|
||||||
|
)
|
||||||
|
btn.callback = self._on_roll
|
||||||
|
self.add_item(btn)
|
||||||
|
|
||||||
|
async def _on_roll(self, interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if self._rolling:
|
||||||
|
await interaction.response.defer()
|
||||||
|
return
|
||||||
|
self._rolling = True
|
||||||
|
|
||||||
|
self.clear_items()
|
||||||
|
rolling_embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak"],
|
||||||
|
description=S.JAILBREAK_UI["rolling_desc"],
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
await interaction.response.edit_message(embed=rolling_embed, view=self)
|
||||||
|
|
||||||
|
d1 = random.randint(1, 6)
|
||||||
|
d2 = random.randint(1, 6)
|
||||||
|
e1, e2 = _DICE_EMOJI[d1 - 1], _DICE_EMOJI[d2 - 1]
|
||||||
|
double = d1 == d2
|
||||||
|
self.tries += 1
|
||||||
|
tries_left = self.MAX_TRIES - self.tries
|
||||||
|
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
self._rolling = False
|
||||||
|
|
||||||
|
if double:
|
||||||
|
await economy.do_jail_free(self.user_id)
|
||||||
|
self.stop()
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_free"],
|
||||||
|
description=S.JAILBREAK_UI["free_desc"].format(d1=e1, d2=e2),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
elif tries_left == 0:
|
||||||
|
self.stop()
|
||||||
|
user_data = await economy.get_user(self.user_id)
|
||||||
|
bal = user_data["balance"]
|
||||||
|
if bal >= economy.MIN_BAIL:
|
||||||
|
min_fine = max(economy.MIN_BAIL, int(bal * 0.20))
|
||||||
|
max_fine = max(economy.MIN_BAIL, int(bal * 0.30))
|
||||||
|
desc = S.JAILBREAK_UI["fail_bail_offer"].format(
|
||||||
|
d1=e1,
|
||||||
|
d2=e2,
|
||||||
|
min=coin(min_fine),
|
||||||
|
max=coin(max_fine),
|
||||||
|
bal=coin(bal),
|
||||||
|
)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_fail"],
|
||||||
|
description=desc,
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=BailView(self.user_id))
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_fail"],
|
||||||
|
description=S.JAILBREAK_UI["fail_broke_desc"].format(
|
||||||
|
d1=e1,
|
||||||
|
d2=e2,
|
||||||
|
balance=coin(bal),
|
||||||
|
),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=None)
|
||||||
|
else:
|
||||||
|
self._add_roll_btn()
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_miss"].format(tries=self.tries, max=self.MAX_TRIES),
|
||||||
|
description=S.JAILBREAK_UI["miss_desc"].format(d1=e1, d2=e2, left=tries_left),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
|
||||||
|
class BailView(discord.ui.View):
|
||||||
|
def __init__(self, user_id: int):
|
||||||
|
super().__init__(timeout=60)
|
||||||
|
self.user_id = user_id
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.JAILBREAK_UI["bail_btn"], style=discord.ButtonStyle.danger)
|
||||||
|
async def pay_bail(self, interaction: discord.Interaction, _: discord.ui.Button):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_bail(self.user_id)
|
||||||
|
self.clear_items()
|
||||||
|
self.stop()
|
||||||
|
if not res["ok"] and res.get("reason") == "broke":
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_bail"],
|
||||||
|
description=S.JAILBREAK_UI["bail_broke_desc"].format(
|
||||||
|
min=coin(economy.MIN_BAIL),
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_bail"],
|
||||||
|
description=S.JAILBREAK_UI["bail_paid_desc"].format(
|
||||||
|
fine=coin(res["fine"]),
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await interaction.response.edit_message(embed=embed, view=self)
|
||||||
|
|
||||||
|
@tree.command(name="jailbreak", description=S.CMD["jailbreak"])
|
||||||
|
async def cmd_jailbreak(interaction: discord.Interaction):
|
||||||
|
user_data = await economy.get_user(interaction.user.id)
|
||||||
|
remaining = economy._is_jailed(user_data)
|
||||||
|
if not remaining:
|
||||||
|
await interaction.response.send_message(S.ERR["not_jailed"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if user_data.get("jailbreak_used", False):
|
||||||
|
bal = user_data["balance"]
|
||||||
|
min_fine = max(economy.MIN_BAIL, int(bal * 0.20))
|
||||||
|
max_fine = max(economy.MIN_BAIL, int(bal * 0.30))
|
||||||
|
if bal >= economy.MIN_BAIL:
|
||||||
|
desc = S.JAILBREAK_UI["already_bail"].format(
|
||||||
|
min=coin(min_fine),
|
||||||
|
max=coin(max_fine),
|
||||||
|
bal=coin(bal),
|
||||||
|
ts=cd_ts(remaining),
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_bail"],
|
||||||
|
description=desc,
|
||||||
|
color=0xED4245,
|
||||||
|
),
|
||||||
|
view=BailView(interaction.user.id),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
desc = S.JAILBREAK_UI["already_broke"].format(
|
||||||
|
min=coin(economy.MIN_BAIL),
|
||||||
|
bal=coin(bal),
|
||||||
|
ts=cd_ts(remaining),
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_bail"],
|
||||||
|
description=desc,
|
||||||
|
color=0xED4245,
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await economy.set_jailbreak_used(interaction.user.id)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak"],
|
||||||
|
description=S.JAILBREAK_UI["intro_desc"].format(
|
||||||
|
ts=cd_ts(remaining),
|
||||||
|
tries=JailbreakView.MAX_TRIES,
|
||||||
|
),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=embed,
|
||||||
|
view=JailbreakView(interaction.user.id),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="give", description=S.CMD["give"])
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["give_kasutaja"], summa=S.OPT["give_summa"])
|
||||||
|
async def cmd_give(interaction: discord.Interaction, kasutaja: discord.Member, summa: str):
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
summa_int, err = parse_amount(summa, data["balance"])
|
||||||
|
if err:
|
||||||
|
await interaction.response.send_message(err, ephemeral=True)
|
||||||
|
return
|
||||||
|
if summa_int is None or summa_int <= 0:
|
||||||
|
await interaction.response.send_message(S.ERR["positive_amount"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if kasutaja.id == interaction.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["give_self"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if kasutaja.bot:
|
||||||
|
await interaction.response.send_message(S.ERR["give_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await economy.do_give(interaction.user.id, kasutaja.id, summa_int)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["give_jailed"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["broke"].format(bal=coin(data["balance"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{economy.COIN} {S.TITLE['give']}",
|
||||||
|
description=S.GIVE_UI["desc"].format(
|
||||||
|
giver=interaction.user.display_name,
|
||||||
|
amount=coin(summa_int),
|
||||||
|
receiver=kasutaja.display_name,
|
||||||
|
),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
|
||||||
|
class LeaderboardView(discord.ui.View):
|
||||||
|
PER_PAGE = 10
|
||||||
|
|
||||||
|
def __init__(self, data: dict, guild: discord.Guild | None, bot_user: discord.ClientUser | None):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self.data = data
|
||||||
|
self.guild = guild
|
||||||
|
self.bot_user = bot_user
|
||||||
|
self.page = 0
|
||||||
|
self.mode = "coins"
|
||||||
|
self.max_page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
|
||||||
|
def _current_list(self) -> list:
|
||||||
|
return self.data.get(self.mode, [])
|
||||||
|
|
||||||
|
def _update_buttons(self):
|
||||||
|
current = self._current_list()
|
||||||
|
self.max_page = max(0, (len(current) - 1) // self.PER_PAGE) if current else 0
|
||||||
|
self.prev_btn.disabled = self.page == 0
|
||||||
|
self.next_btn.disabled = self.page >= self.max_page
|
||||||
|
for m, btn in [
|
||||||
|
("coins", self.coins_btn),
|
||||||
|
("exp", self.exp_btn),
|
||||||
|
("season", self.season_btn),
|
||||||
|
("prestige", self.prestige_btn),
|
||||||
|
("wagered", self.wagered_btn),
|
||||||
|
("fish", self.fish_btn),
|
||||||
|
]:
|
||||||
|
btn.style = discord.ButtonStyle.primary if m == self.mode else discord.ButtonStyle.secondary
|
||||||
|
|
||||||
|
def _name(self, uid: str, highlight_uid: int | None = None) -> str:
|
||||||
|
if self.guild:
|
||||||
|
member = self.guild.get_member(int(uid))
|
||||||
|
name = member.display_name if member else S.LEADERBOARD_UI["unknown_user"].format(uid=uid)
|
||||||
|
else:
|
||||||
|
name = S.LEADERBOARD_UI["unknown_user"].format(uid=uid)
|
||||||
|
if highlight_uid and int(uid) == highlight_uid:
|
||||||
|
name = f"**› {name} ‹**"
|
||||||
|
return name
|
||||||
|
|
||||||
|
def _make_embed(self, highlight_uid: int | None = None) -> discord.Embed:
|
||||||
|
title_map = {
|
||||||
|
"coins": f"{economy.COIN} {S.TITLE['leaderboard_coins']}",
|
||||||
|
"exp": S.TITLE["leaderboard_exp"],
|
||||||
|
"season": S.TITLE["leaderboard_season"],
|
||||||
|
"prestige": S.TITLE["leaderboard_prestige"],
|
||||||
|
"wagered": S.TITLE["leaderboard_wagered"],
|
||||||
|
"fish": S.TITLE["leaderboard_fish"],
|
||||||
|
}
|
||||||
|
color_map = {"coins": 0xF4C430, "wagered": 0xED4245, "fish": 0x57F287}
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=title_map.get(self.mode, "Edetabel"),
|
||||||
|
color=color_map.get(self.mode, 0x5865F2),
|
||||||
|
)
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
if self.mode == "coins" and self.page == 0 and self.data.get("house_entry"):
|
||||||
|
_, bal = self.data["house_entry"]
|
||||||
|
house_name = self.bot_user.display_name if self.bot_user else S.LEADERBOARD_UI["house_default_name"]
|
||||||
|
lines.append(S.LEADERBOARD_UI["house_entry"].format(name=house_name, balance=coin(bal)))
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
start = self.page * self.PER_PAGE
|
||||||
|
medals = ["🥇", "🥈", "🥉"]
|
||||||
|
current = self._current_list()
|
||||||
|
slice_ = current[start : start + self.PER_PAGE]
|
||||||
|
|
||||||
|
if not slice_:
|
||||||
|
lines.append(S.LEADERBOARD_UI["no_entries"])
|
||||||
|
else:
|
||||||
|
for i, entry in enumerate(slice_):
|
||||||
|
rank = start + i
|
||||||
|
uid = entry[0]
|
||||||
|
prefix = medals[rank] if rank < 3 else f"**{rank + 1}.**"
|
||||||
|
name = self._name(uid, highlight_uid)
|
||||||
|
if self.mode == "coins":
|
||||||
|
lines.append(f"{prefix} {name} - {coin(entry[1])}")
|
||||||
|
elif self.mode == "exp":
|
||||||
|
lines.append(
|
||||||
|
S.LEADERBOARD_UI["exp_entry"].format(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
exp=entry[1],
|
||||||
|
level=entry[2],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif self.mode == "season":
|
||||||
|
lines.append(
|
||||||
|
S.LEADERBOARD_UI["season_entry"].format(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
exp=entry[1],
|
||||||
|
prestige=entry[2],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif self.mode == "prestige":
|
||||||
|
lines.append(
|
||||||
|
S.LEADERBOARD_UI["prestige_entry"].format(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
prestige=entry[1],
|
||||||
|
pp=entry[2],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif self.mode == "wagered":
|
||||||
|
lines.append(
|
||||||
|
S.LEADERBOARD_UI["wagered_entry"].format(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
wagered=coin(entry[1]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif self.mode == "fish":
|
||||||
|
lines.append(
|
||||||
|
S.LEADERBOARD_UI["fish_entry"].format(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
caught=entry[1],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
total = self.max_page + 1
|
||||||
|
embed.description = "\n".join(lines)
|
||||||
|
embed.set_footer(
|
||||||
|
text=S.LEADERBOARD_UI["footer"].format(
|
||||||
|
page=self.page + 1,
|
||||||
|
total=total,
|
||||||
|
count=len(current),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
@discord.ui.button(label="◄", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def prev_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.page -= 1
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="►", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def next_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.page += 1
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_find_me"], style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def find_me_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
uid = interaction.user.id
|
||||||
|
for i, entry in enumerate(self._current_list()):
|
||||||
|
if int(entry[0]) == uid:
|
||||||
|
self.page = i // self.PER_PAGE
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(
|
||||||
|
embed=self._make_embed(highlight_uid=uid),
|
||||||
|
view=self,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await interaction.response.send_message(S.ERR["not_in_leaderboard"], ephemeral=True)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_coins"], style=discord.ButtonStyle.primary, row=1)
|
||||||
|
async def coins_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "coins"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_exp"], style=discord.ButtonStyle.secondary, row=1)
|
||||||
|
async def exp_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "exp"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_season"], style=discord.ButtonStyle.secondary, row=1)
|
||||||
|
async def season_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "season"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_prestige"], style=discord.ButtonStyle.secondary, row=1)
|
||||||
|
async def prestige_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "prestige"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_wagered"], style=discord.ButtonStyle.secondary, row=1)
|
||||||
|
async def wagered_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "wagered"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_fish"], style=discord.ButtonStyle.secondary, row=2)
|
||||||
|
async def fish_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "fish"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
for child in self.children:
|
||||||
|
child.disabled = True
|
||||||
|
|
||||||
|
@tree.command(name="leaderboard", description=S.CMD["leaderboard"])
|
||||||
|
async def cmd_leaderboard(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
coins_raw, exp_raw, season_raw, prestige_raw, wagered_raw, fish_raw = await asyncio.gather(
|
||||||
|
economy.get_leaderboard(top_n=None),
|
||||||
|
economy.get_leaderboard_exp(top_n=None),
|
||||||
|
economy.get_leaderboard_season_exp(top_n=None),
|
||||||
|
economy.get_leaderboard_prestige(top_n=None),
|
||||||
|
economy.get_leaderboard_wagered(top_n=None),
|
||||||
|
economy.get_leaderboard_fish(top_n=None),
|
||||||
|
)
|
||||||
|
|
||||||
|
house_entry = None
|
||||||
|
regular = []
|
||||||
|
bot_id = bot.user.id if bot.user else None
|
||||||
|
for uid, bal in coins_raw:
|
||||||
|
if bot_id and int(uid) == bot_id:
|
||||||
|
house_entry = (uid, bal)
|
||||||
|
else:
|
||||||
|
regular.append((uid, bal))
|
||||||
|
|
||||||
|
def _no_bot(entries: list) -> list:
|
||||||
|
return [e for e in entries if not (bot_id and int(e[0]) == bot_id)]
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"coins": regular,
|
||||||
|
"exp": _no_bot(exp_raw),
|
||||||
|
"season": _no_bot(season_raw),
|
||||||
|
"prestige": _no_bot(prestige_raw),
|
||||||
|
"wagered": _no_bot(wagered_raw),
|
||||||
|
"fish": _no_bot(fish_raw),
|
||||||
|
"house_entry": house_entry,
|
||||||
|
}
|
||||||
|
view = LeaderboardView(data, interaction.guild, bot.user)
|
||||||
|
await interaction.followup.send(embed=view._make_embed(), view=view)
|
||||||
|
|
||||||
|
def _shop_embed(tier: int, user_data: dict) -> discord.Embed:
|
||||||
|
owned = set(user_data.get("items", []))
|
||||||
|
item_uses = user_data.get("item_uses", {})
|
||||||
|
tier_names = {1: S.SHOP_UI["tier_1"], 2: S.SHOP_UI["tier_2"], 3: S.SHOP_UI["tier_3"]}
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{economy.COIN} TipiBOTi pood · {tier_names[tier]}",
|
||||||
|
description=S.SHOP_UI["desc"].format(bal=coin(user_data["balance"])),
|
||||||
|
color=[0x57F287, 0xF4C430, 0xED4245][tier - 1],
|
||||||
|
)
|
||||||
|
for item_id in sorted(economy.SHOP_TIERS[tier], key=lambda k: economy.SHOP[k]["cost"]):
|
||||||
|
item = economy.SHOP[item_id]
|
||||||
|
anticheat_uses = item_uses.get("anticheat", 0) if item_id == "anticheat" else 0
|
||||||
|
min_lvl = economy.SHOP_LEVEL_REQ.get(item_id, 0)
|
||||||
|
user_lvl = economy.get_level(user_data.get("exp", 0))
|
||||||
|
if item_id in owned and not (item_id == "anticheat" and anticheat_uses <= 0):
|
||||||
|
if item_id == "anticheat":
|
||||||
|
key = "owned_uses_1" if anticheat_uses == 1 else "owned_uses_n"
|
||||||
|
status = S.SHOP_UI[key].format(uses=anticheat_uses)
|
||||||
|
else:
|
||||||
|
status = S.SHOP_UI["owned"]
|
||||||
|
elif min_lvl > 0 and user_lvl < min_lvl:
|
||||||
|
status = S.SHOP_UI["locked"].format(min_lvl=min_lvl, user_lvl=user_lvl)
|
||||||
|
else:
|
||||||
|
status = f"{item['cost']} {economy.COIN}"
|
||||||
|
embed.add_field(
|
||||||
|
name=f"{item['emoji']} {item['name']} · {status}",
|
||||||
|
value=item["description"],
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
class ShopView(discord.ui.View):
|
||||||
|
def __init__(self, user_data: dict, tier: int = 1):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self._user_data = user_data
|
||||||
|
self._tier = tier
|
||||||
|
self._update_buttons()
|
||||||
|
|
||||||
|
def _update_buttons(self):
|
||||||
|
self.clear_items()
|
||||||
|
for t, label in [(1, S.SHOP_BTN[1]), (2, S.SHOP_BTN[2]), (3, S.SHOP_BTN[3])]:
|
||||||
|
btn = discord.ui.Button(
|
||||||
|
label=label,
|
||||||
|
style=discord.ButtonStyle.primary if t == self._tier else discord.ButtonStyle.secondary,
|
||||||
|
custom_id=f"shop_tier_{t}",
|
||||||
|
)
|
||||||
|
btn.callback = self._make_callback(t)
|
||||||
|
self.add_item(btn)
|
||||||
|
|
||||||
|
def _make_callback(self, tier: int):
|
||||||
|
async def callback(interaction: discord.Interaction):
|
||||||
|
self._tier = tier
|
||||||
|
self._update_buttons()
|
||||||
|
self._user_data = await economy.get_user(interaction.user.id)
|
||||||
|
await interaction.response.edit_message(
|
||||||
|
embed=_shop_embed(self._tier, self._user_data),
|
||||||
|
view=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
return callback
|
||||||
|
|
||||||
|
@tree.command(name="shop", description=S.CMD["shop"])
|
||||||
|
async def cmd_shop(interaction: discord.Interaction):
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=_shop_embed(1, data),
|
||||||
|
view=ShopView(data, tier=1),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="buy", description=S.CMD["buy"])
|
||||||
|
@app_commands.describe(ese=S.OPT["buy_ese"])
|
||||||
|
@app_commands.choices(
|
||||||
|
ese=[
|
||||||
|
app_commands.Choice(name=f"{v['name']} ({v['cost']} TipiCOINi)", value=k)
|
||||||
|
for k, v in economy.SHOP.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
async def cmd_buy(interaction: discord.Interaction, ese: app_commands.Choice[str]):
|
||||||
|
res = await economy.do_buy(interaction.user.id, ese.value)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "owned":
|
||||||
|
await interaction.response.send_message(S.ERR["item_owned"], ephemeral=True)
|
||||||
|
elif res["reason"] == "level_required":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["item_level_req"].format(
|
||||||
|
min_level=res["min_level"],
|
||||||
|
user_level=res["user_level"],
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "insufficient":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["broke_need"].format(need=coin(res["need"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(S.ERR["item_not_found"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
item = res["item"]
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.BUY_UI["title"].format(emoji=item["emoji"], name=item["name"]),
|
||||||
|
description=S.BUY_UI["desc"].format(
|
||||||
|
description=item["description"],
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
380
economy_fish_commands.py
Normal file
380
economy_fish_commands.py
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
from collections.abc import Awaitable, Callable, MutableSet
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_fish_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
coin: Callable[[int], str],
|
||||||
|
cd_ts: Callable,
|
||||||
|
check_cmd_rate: Callable[[discord.Interaction], Awaitable[bool]],
|
||||||
|
maybe_remind: Callable[[int, str], Awaitable[None]],
|
||||||
|
award_exp: Callable[[discord.Interaction, int], Awaitable[None]],
|
||||||
|
active_games: MutableSet[int],
|
||||||
|
) -> None:
|
||||||
|
class FishCatchView(discord.ui.View):
|
||||||
|
"""Shown after a successful pull - lets user sell or keep the fish."""
|
||||||
|
|
||||||
|
def __init__(self, user_id: int, res: dict, fish_id: str, weight: int):
|
||||||
|
super().__init__(timeout=60)
|
||||||
|
self.user_id = user_id
|
||||||
|
self._res = res
|
||||||
|
self._fish_id = fish_id
|
||||||
|
self._weight = weight
|
||||||
|
self._done = False
|
||||||
|
|
||||||
|
def _catch_embed(self, color: int = 0x57F287) -> discord.Embed:
|
||||||
|
rarity = economy.FISH_CATALOGUE[self._fish_id]["rarity"]
|
||||||
|
emoji = S.FISH_RARITY_EMOJI[rarity]
|
||||||
|
fish_name = S.FISH_NAMES[self._fish_id]
|
||||||
|
desc = S.FISH_UI["catch_desc"].format(
|
||||||
|
name=fish_name,
|
||||||
|
weight=self._weight,
|
||||||
|
exp=self._res["exp"],
|
||||||
|
value=coin(self._res["value"]),
|
||||||
|
)
|
||||||
|
if self._res.get("is_new"):
|
||||||
|
desc += S.FISH_UI["new_fish"]
|
||||||
|
return discord.Embed(title=f"{emoji} {fish_name}!", description=desc, color=color)
|
||||||
|
|
||||||
|
@discord.ui.button(label="", style=discord.ButtonStyle.success)
|
||||||
|
async def sell_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if self._done:
|
||||||
|
await interaction.response.defer()
|
||||||
|
return
|
||||||
|
self._done = True
|
||||||
|
self.stop()
|
||||||
|
for child in self.children:
|
||||||
|
child.disabled = True
|
||||||
|
sell_res = await economy.do_fish_sell(self.user_id, [-1])
|
||||||
|
rarity = economy.FISH_CATALOGUE[self._fish_id]["rarity"]
|
||||||
|
emoji = S.FISH_RARITY_EMOJI[rarity]
|
||||||
|
fish_name = S.FISH_NAMES[self._fish_id]
|
||||||
|
desc = S.FISH_UI["catch_sold"].format(
|
||||||
|
name=fish_name,
|
||||||
|
weight=self._weight,
|
||||||
|
coins=coin(sell_res["coins"]),
|
||||||
|
exp=self._res["exp"],
|
||||||
|
balance=coin(sell_res["balance"]),
|
||||||
|
)
|
||||||
|
if self._res.get("is_new"):
|
||||||
|
desc += S.FISH_UI["new_fish"]
|
||||||
|
embed = discord.Embed(title=f"{emoji} {fish_name}!", description=desc, color=0x57F287)
|
||||||
|
await interaction.response.edit_message(embed=embed, view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="", style=discord.ButtonStyle.secondary)
|
||||||
|
async def keep_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if self._done:
|
||||||
|
await interaction.response.defer()
|
||||||
|
return
|
||||||
|
self._done = True
|
||||||
|
self.stop()
|
||||||
|
for child in self.children:
|
||||||
|
child.disabled = True
|
||||||
|
rarity = economy.FISH_CATALOGUE[self._fish_id]["rarity"]
|
||||||
|
emoji = S.FISH_RARITY_EMOJI[rarity]
|
||||||
|
fish_name = S.FISH_NAMES[self._fish_id]
|
||||||
|
desc = S.FISH_UI["catch_kept"].format(
|
||||||
|
name=fish_name,
|
||||||
|
weight=self._weight,
|
||||||
|
exp=self._res["exp"],
|
||||||
|
)
|
||||||
|
if self._res.get("is_new"):
|
||||||
|
desc += S.FISH_UI["new_fish"]
|
||||||
|
embed = discord.Embed(title=f"{emoji} {fish_name}!", description=desc, color=0x5865F2)
|
||||||
|
await interaction.response.edit_message(embed=embed, view=self)
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
for child in self.children:
|
||||||
|
child.disabled = True
|
||||||
|
|
||||||
|
class FishingView(discord.ui.View):
|
||||||
|
BITE_WINDOW = 2.0
|
||||||
|
|
||||||
|
def __init__(self, user_id: int, fish_id: str, weight: int):
|
||||||
|
super().__init__(timeout=40)
|
||||||
|
self.user_id = user_id
|
||||||
|
self._fish_id = fish_id
|
||||||
|
self._weight = weight
|
||||||
|
self._clicked = False
|
||||||
|
self._bite_active = False
|
||||||
|
self._msg: discord.Message | None = None
|
||||||
|
|
||||||
|
self.pull_btn = discord.ui.Button(
|
||||||
|
label=S.FISH_UI["btn_wait"],
|
||||||
|
style=discord.ButtonStyle.secondary,
|
||||||
|
disabled=True,
|
||||||
|
)
|
||||||
|
self.pull_btn.callback = self._pull
|
||||||
|
self.add_item(self.pull_btn)
|
||||||
|
|
||||||
|
async def start(self, msg: discord.Message) -> None:
|
||||||
|
self._msg = msg
|
||||||
|
wait = random.uniform(5, 15)
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
if self._clicked or self.is_finished():
|
||||||
|
return
|
||||||
|
self._bite_active = True
|
||||||
|
self.pull_btn.disabled = False
|
||||||
|
self.pull_btn.label = S.FISH_UI["btn_bite"]
|
||||||
|
self.pull_btn.style = discord.ButtonStyle.success
|
||||||
|
try:
|
||||||
|
await msg.edit(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title=S.TITLE["fish_bite"],
|
||||||
|
description=S.FISH_UI["bite_desc"],
|
||||||
|
color=0xED4245,
|
||||||
|
),
|
||||||
|
view=self,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(self.BITE_WINDOW)
|
||||||
|
if not self._clicked:
|
||||||
|
self.stop()
|
||||||
|
active_games.discard(self.user_id)
|
||||||
|
self.pull_btn.disabled = True
|
||||||
|
try:
|
||||||
|
await msg.edit(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title=S.TITLE["fish_escape"],
|
||||||
|
description=S.FISH_UI["escape_desc"],
|
||||||
|
color=0x99AAB5,
|
||||||
|
),
|
||||||
|
view=self,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _pull(self, interaction: discord.Interaction) -> None:
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if not self._bite_active:
|
||||||
|
await interaction.response.send_message(S.FISH_UI["too_early"], ephemeral=True)
|
||||||
|
return
|
||||||
|
self._clicked = True
|
||||||
|
self.stop()
|
||||||
|
active_games.discard(self.user_id)
|
||||||
|
self.pull_btn.disabled = True
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
|
if self._fish_id == "junk":
|
||||||
|
junk_text = random.choice(S.FISH_JUNK_LINES)
|
||||||
|
user_data = await economy.get_user(interaction.user.id)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fish_junk"],
|
||||||
|
description=S.FISH_UI["junk_desc"].format(
|
||||||
|
text=junk_text,
|
||||||
|
balance=coin(user_data.get("balance", 0)),
|
||||||
|
),
|
||||||
|
color=0x99AAB5,
|
||||||
|
)
|
||||||
|
await self._msg.edit(embed=embed, view=self)
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await economy.do_fish_resolve(self.user_id, self._fish_id, self._weight)
|
||||||
|
if not res["ok"]:
|
||||||
|
await self._msg.edit(
|
||||||
|
embed=discord.Embed(title=S.ERR["generic_error"], color=0xED4245),
|
||||||
|
view=self,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
catch_view = FishCatchView(self.user_id, res, self._fish_id, self._weight)
|
||||||
|
catch_view.sell_btn.label = S.FISH_UI["btn_sell"]
|
||||||
|
catch_view.keep_btn.label = S.FISH_UI["btn_keep"]
|
||||||
|
await self._msg.edit(embed=catch_view._catch_embed(), view=catch_view)
|
||||||
|
if res.get("exp", 0) > 0:
|
||||||
|
asyncio.create_task(award_exp(interaction, res["exp"]))
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
for child in self.children:
|
||||||
|
child.disabled = True
|
||||||
|
active_games.discard(self.user_id)
|
||||||
|
|
||||||
|
@tree.command(name="fish", description=S.CMD["fish"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_fish(interaction: discord.Interaction):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
if interaction.user.id in active_games:
|
||||||
|
await interaction.response.send_message(S.ERR["game_in_progress"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await economy.do_fish_start(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["fish"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
user_data = await economy.get_user(interaction.user.id)
|
||||||
|
rarity_bump = "kalavork" in user_data.get("items", [])
|
||||||
|
has_echolood = "echolood" in user_data.get("items", [])
|
||||||
|
fish_id, weight = economy.roll_fish(rarity_bump=rarity_bump)
|
||||||
|
|
||||||
|
active_games.add(interaction.user.id)
|
||||||
|
view = FishingView(interaction.user.id, fish_id, weight)
|
||||||
|
if has_echolood:
|
||||||
|
view.BITE_WINDOW = 3.0
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fish_cast"],
|
||||||
|
description=S.FISH_UI["cast_desc"],
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, view=view)
|
||||||
|
msg = await interaction.original_response()
|
||||||
|
asyncio.create_task(view.start(msg))
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "fish"))
|
||||||
|
|
||||||
|
@tree.command(name="fishbook", description=S.CMD["fishbook"])
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["fishbook_kasutaja"])
|
||||||
|
async def cmd_fishbook(interaction: discord.Interaction, kasutaja: discord.Member | None = None):
|
||||||
|
target = kasutaja or interaction.user
|
||||||
|
res = await economy.do_fishbook(target.id)
|
||||||
|
book: dict = res["book"]
|
||||||
|
total = res["total_species"]
|
||||||
|
caught_count = res["unique_caught"]
|
||||||
|
|
||||||
|
if not book:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fishbook"],
|
||||||
|
description=S.FISH_UI["book_empty"],
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
inv_counts: dict = res.get("inv_counts", {})
|
||||||
|
all_fish = list(economy.FISH_CATALOGUE.items())
|
||||||
|
lines = [S.FISH_UI["book_caught"].format(caught=caught_count, total=total)]
|
||||||
|
for fish_id, fish_data in all_fish:
|
||||||
|
rarity = fish_data["rarity"]
|
||||||
|
emoji = S.FISH_RARITY_EMOJI[rarity]
|
||||||
|
rarity_name = S.FISH_RARITY_NAMES[rarity]
|
||||||
|
count = book.get(fish_id, 0)
|
||||||
|
if count > 0:
|
||||||
|
n_inv = inv_counts.get(fish_id, 0)
|
||||||
|
inv_str = S.FISH_UI["book_inv"].format(n=n_inv) if n_inv > 0 else ""
|
||||||
|
lines.append(
|
||||||
|
S.FISH_UI["book_yes"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=S.FISH_NAMES[fish_id],
|
||||||
|
rarity=rarity_name,
|
||||||
|
count=count,
|
||||||
|
inv=inv_str,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(S.FISH_UI["book_no"].format(rarity=rarity_name))
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fishbook"].replace("Kalakogu", f"{target.display_name} kalakogu"),
|
||||||
|
description="\n".join(lines),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.set_footer(
|
||||||
|
text=S.FISH_UI["book_footer"].format(
|
||||||
|
page=1,
|
||||||
|
total_pages=1,
|
||||||
|
caught=caught_count,
|
||||||
|
total=total,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="fishsell", description=S.CMD["fishsell"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_fishsell(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
user_data = await economy.get_user(interaction.user.id)
|
||||||
|
inv: list = user_data.get("fish_inventory") or []
|
||||||
|
if not inv:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fishbook"],
|
||||||
|
description=S.FISH_UI["inv_empty"],
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
total_value = sum(e["value"] for e in inv)
|
||||||
|
lines = [S.FISH_UI["inv_header"].format(count=len(inv), total_value=coin(total_value))]
|
||||||
|
for entry in inv:
|
||||||
|
fid = entry.get("fish_id", "")
|
||||||
|
rarity = economy.FISH_CATALOGUE.get(fid, {}).get("rarity", "common")
|
||||||
|
emoji = S.FISH_RARITY_EMOJI.get(rarity, "🐟")
|
||||||
|
name = S.FISH_NAMES.get(fid, fid)
|
||||||
|
lines.append(
|
||||||
|
S.FISH_UI["inv_entry"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=name,
|
||||||
|
weight=entry["weight"],
|
||||||
|
value=coin(entry["value"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fishbook"],
|
||||||
|
description="\n".join(lines),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
|
||||||
|
sell_all_btn = discord.ui.Button(
|
||||||
|
label=S.FISH_UI["btn_sell"] + f" ({coin(total_value)})",
|
||||||
|
style=discord.ButtonStyle.success,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _sell_all(btn_interaction: discord.Interaction):
|
||||||
|
if btn_interaction.user.id != interaction.user.id:
|
||||||
|
await btn_interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
sell_view.stop()
|
||||||
|
for child in sell_view.children:
|
||||||
|
child.disabled = True
|
||||||
|
res = await economy.do_fish_sell(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
await btn_interaction.response.edit_message(
|
||||||
|
embed=discord.Embed(description=S.FISH_UI["inv_none"], color=0x99AAB5),
|
||||||
|
view=sell_view,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
sold_embed = discord.Embed(
|
||||||
|
title=S.TITLE["fishbook"],
|
||||||
|
description=S.FISH_UI["inv_sold_all"].format(
|
||||||
|
count=res["count"],
|
||||||
|
coins=coin(res["coins"]),
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await btn_interaction.response.edit_message(embed=sold_embed, view=sell_view)
|
||||||
|
|
||||||
|
sell_all_btn.callback = _sell_all
|
||||||
|
sell_view = discord.ui.View(timeout=60)
|
||||||
|
sell_view.add_item(sell_all_btn)
|
||||||
|
await interaction.followup.send(embed=embed, view=sell_view, ephemeral=True)
|
||||||
1132
economy_games_commands.py
Normal file
1132
economy_games_commands.py
Normal file
File diff suppressed because it is too large
Load Diff
257
economy_income_commands.py
Normal file
257
economy_income_commands.py
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_income_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
coin: Callable[[int], str],
|
||||||
|
cd_ts: Callable[[datetime.timedelta], str],
|
||||||
|
check_cmd_rate: Callable[[discord.Interaction], Awaitable[bool]],
|
||||||
|
maybe_remind: Callable[[int, str], Awaitable[None]],
|
||||||
|
award_exp: Callable[[discord.Interaction, int], Awaitable[None]],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="daily", description=S.CMD["daily"])
|
||||||
|
async def cmd_daily(interaction: discord.Interaction):
|
||||||
|
res = await economy.do_daily(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["daily"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
streak = res["streak"]
|
||||||
|
streak_str = f"🔥 {streak}p" + (
|
||||||
|
" (+200%)"
|
||||||
|
if res["streak_mult"] >= 3.0
|
||||||
|
else " (+100%)"
|
||||||
|
if res["streak_mult"] >= 2.0
|
||||||
|
else " (+50%)"
|
||||||
|
if res["streak_mult"] >= 1.5
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
lines = [S.DAILY_UI["earned"].format(earned=coin(res["earned"]))]
|
||||||
|
if res["interest"]:
|
||||||
|
lines.append(S.DAILY_UI["interest"].format(interest=coin(res["interest"])))
|
||||||
|
if res["vip"]:
|
||||||
|
lines.append(S.DAILY_UI["vip"])
|
||||||
|
lines.append(S.DAILY_UI["footer"].format(streak_str=streak_str, balance=coin(res["balance"])))
|
||||||
|
|
||||||
|
embed = discord.Embed(title=S.TITLE["daily"], description="\n".join(lines), color=0xF4C430)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "daily"))
|
||||||
|
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["daily"]))
|
||||||
|
|
||||||
|
@tree.command(name="work", description=S.CMD["work"])
|
||||||
|
async def cmd_work(interaction: discord.Interaction):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
res = await economy.do_work(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["work"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
desc = S.WORK_UI["desc"].format(job=res["job"], earned=coin(res["earned"]))
|
||||||
|
if res["lucky"]:
|
||||||
|
desc += S.WORK_UI["redbull"]
|
||||||
|
if res["hiir"]:
|
||||||
|
desc += S.WORK_UI["hiir"]
|
||||||
|
if res["laud"]:
|
||||||
|
desc += S.WORK_UI["laud"]
|
||||||
|
desc += S.WORK_UI["balance"].format(balance=coin(res["balance"]))
|
||||||
|
embed = discord.Embed(title=S.TITLE["work"], description=desc, color=0x57F287)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "work"))
|
||||||
|
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["work"]))
|
||||||
|
|
||||||
|
@tree.command(name="beg", description=S.CMD["beg"])
|
||||||
|
async def cmd_beg(interaction: discord.Interaction):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
res = await economy.do_beg(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["beg"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if res["jailed"]:
|
||||||
|
title = "🔒 " + S.TITLE["beg"]
|
||||||
|
color = 0xE67E22
|
||||||
|
else:
|
||||||
|
title = S.TITLE["beg"]
|
||||||
|
color = 0x99AAB5
|
||||||
|
beg_lines = [S.BEG_UI["desc"].format(text=res["text"], earned=coin(res["earned"]))]
|
||||||
|
if res["klaviatuur"]:
|
||||||
|
beg_lines.append(S.BEG_UI["klaviatuur"])
|
||||||
|
beg_lines.append(S.BEG_UI["balance"].format(balance=coin(res["balance"])))
|
||||||
|
embed = discord.Embed(title=title, description="\n".join(beg_lines), color=color)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "beg"))
|
||||||
|
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["beg"]))
|
||||||
|
|
||||||
|
@tree.command(name="crime", description=S.CMD["crime"])
|
||||||
|
async def cmd_crime(interaction: discord.Interaction):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
res = await economy.do_crime(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["crime"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if res["success"]:
|
||||||
|
crime_lines = [S.CRIME_UI["win_desc"].format(text=res["text"], earned=coin(res["earned"]))]
|
||||||
|
if res["mikrofon"]:
|
||||||
|
crime_lines.append(S.CRIME_UI["mikrofon"].lstrip("\n"))
|
||||||
|
crime_lines.append(S.CRIME_UI["balance"].lstrip("\n").format(balance=coin(res["balance"])))
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["crime_win"],
|
||||||
|
description="\n".join(crime_lines),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
jail_part = (
|
||||||
|
S.CRIME_UI["fail_jailed"].format(ts=cd_ts(economy.JAIL_DURATION))
|
||||||
|
if res.get("jailed")
|
||||||
|
else S.CRIME_UI["fail_shield"]
|
||||||
|
)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["crime_fail"],
|
||||||
|
description=S.CRIME_UI["fail_base"].format(text=res["text"], fine=coin(res["fine"]))
|
||||||
|
+ jail_part
|
||||||
|
+ S.CRIME_UI["balance"].format(balance=coin(res["balance"])),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "crime"))
|
||||||
|
if res["success"]:
|
||||||
|
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["crime_win"]))
|
||||||
|
|
||||||
|
@tree.command(name="rob", description=S.CMD["rob"])
|
||||||
|
async def cmd_rob(interaction: discord.Interaction, sihtmärk: discord.Member):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
if sihtmärk.id == interaction.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["rob_self"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if sihtmärk.bot and (bot.user is None or sihtmärk.id != bot.user.id):
|
||||||
|
await interaction.response.send_message(S.ERR["rob_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if bot.user and sihtmärk.id == bot.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["rob_house_blocked"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await economy.do_rob(interaction.user.id, sihtmärk.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["rob"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "broke":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["rob_too_poor"].format(name=sihtmärk.display_name),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "target_jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["rob_target_jailed"].format(name=sihtmärk.display_name),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if res["success"]:
|
||||||
|
if res.get("jackpot"):
|
||||||
|
desc = S.ROB_UI["jackpot_desc"].format(stolen=coin(res["stolen"]), balance=coin(res["balance"]))
|
||||||
|
color = 0xF4C430
|
||||||
|
else:
|
||||||
|
desc = S.ROB_UI["win_desc"].format(
|
||||||
|
stolen=coin(res["stolen"]),
|
||||||
|
name=sihtmärk.display_name,
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
)
|
||||||
|
color = 0x57F287
|
||||||
|
embed = discord.Embed(title=S.TITLE["rob_win"], description=desc, color=color)
|
||||||
|
elif res["reason"] == "valvur":
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["rob_anticheat"],
|
||||||
|
description=S.ROB_UI["anticheat_desc"].format(
|
||||||
|
name=sihtmärk.display_name,
|
||||||
|
fine=coin(res["fine"]),
|
||||||
|
),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
target_data = await economy.get_user(sihtmärk.id)
|
||||||
|
if "anticheat" not in target_data.get("items", []):
|
||||||
|
try:
|
||||||
|
await sihtmärk.send(S.ROB_UI["anticheat_worn"])
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["rob_fail"],
|
||||||
|
description=S.ROB_UI["fail_desc"].format(
|
||||||
|
fine=coin(res["fine"]),
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "rob"))
|
||||||
|
if res["success"]:
|
||||||
|
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["rob_win"]))
|
||||||
|
try:
|
||||||
|
await sihtmärk.send(
|
||||||
|
S.ROB_UI["victim_dm"].format(
|
||||||
|
robber=interaction.user.display_name,
|
||||||
|
stolen=coin(res["stolen"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
249
economy_prestige_commands.py
Normal file
249
economy_prestige_commands.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_prestige_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
check_cmd_rate: Callable[[discord.Interaction], Awaitable[bool]],
|
||||||
|
ensure_level_role: Callable[[discord.Member, int], Awaitable[None]],
|
||||||
|
) -> None:
|
||||||
|
class PrestigeView(discord.ui.View):
|
||||||
|
def __init__(self, user_id: int, tab: str = "status"):
|
||||||
|
super().__init__(timeout=60)
|
||||||
|
self.user_id = user_id
|
||||||
|
self.tab = tab
|
||||||
|
|
||||||
|
async def _rebuild(self, data: dict):
|
||||||
|
self.clear_items()
|
||||||
|
pp = data.get("prestige_points", 0)
|
||||||
|
upgrades: dict = data.get("prestige_upgrades") or {}
|
||||||
|
exp = data.get("exp", 0)
|
||||||
|
level = economy.get_level(exp)
|
||||||
|
|
||||||
|
for tab_id, label in (("status", S.PRESTIGE_UI["btn_tab_status"]), ("shop", S.PRESTIGE_UI["btn_tab_shop"])):
|
||||||
|
btn = discord.ui.Button(
|
||||||
|
label=label,
|
||||||
|
style=discord.ButtonStyle.primary if tab_id == self.tab else discord.ButtonStyle.secondary,
|
||||||
|
disabled=(tab_id == self.tab),
|
||||||
|
row=0,
|
||||||
|
)
|
||||||
|
btn.callback = self._switch_tab(tab_id)
|
||||||
|
self.add_item(btn)
|
||||||
|
|
||||||
|
if self.tab == "status" and level >= economy.PRESTIGE_MIN_LEVEL:
|
||||||
|
confirm_btn = discord.ui.Button(
|
||||||
|
label=S.PRESTIGE_UI["btn_confirm"],
|
||||||
|
style=discord.ButtonStyle.danger,
|
||||||
|
row=1,
|
||||||
|
)
|
||||||
|
confirm_btn.callback = self._do_prestige()
|
||||||
|
cancel_btn = discord.ui.Button(
|
||||||
|
label=S.PRESTIGE_UI["btn_cancel"],
|
||||||
|
style=discord.ButtonStyle.secondary,
|
||||||
|
row=1,
|
||||||
|
)
|
||||||
|
cancel_btn.callback = self._do_cancel()
|
||||||
|
self.add_item(confirm_btn)
|
||||||
|
self.add_item(cancel_btn)
|
||||||
|
elif self.tab == "shop":
|
||||||
|
for uid, item in economy.PRESTIGE_SHOP.items():
|
||||||
|
cur_level = upgrades.get(uid, 0)
|
||||||
|
if cur_level >= item["max_level"]:
|
||||||
|
continue
|
||||||
|
cost = item["pp_cost"]
|
||||||
|
btn = discord.ui.Button(
|
||||||
|
label=S.PRESTIGE_UI["btn_buy_upgrade"].format(emoji=item["emoji"], name=S.PRESTIGE_SHOP_NAMES[uid], cost=cost),
|
||||||
|
style=discord.ButtonStyle.success if pp >= cost else discord.ButtonStyle.secondary,
|
||||||
|
disabled=(pp < cost),
|
||||||
|
row=1,
|
||||||
|
)
|
||||||
|
btn.callback = self._buy_upgrade(uid)
|
||||||
|
self.add_item(btn)
|
||||||
|
|
||||||
|
def _build_status_embed(self, data: dict) -> discord.Embed:
|
||||||
|
exp = data.get("exp", 0)
|
||||||
|
level = economy.get_level(exp)
|
||||||
|
pp = data.get("prestige_points", 0)
|
||||||
|
p_level = data.get("prestige_level", 0)
|
||||||
|
if level >= economy.PRESTIGE_MIN_LEVEL:
|
||||||
|
pp_preview = max(1, exp // 1000)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_confirm"],
|
||||||
|
description=S.PRESTIGE_UI["confirm_desc"].format(level=level, exp=exp, pp=pp_preview),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_too_low"],
|
||||||
|
description=S.PRESTIGE_UI["too_low_desc"].format(level=level, required=economy.PRESTIGE_MIN_LEVEL),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
if p_level > 0:
|
||||||
|
embed.set_footer(text=S.PRESTIGE_UI["status_footer"].format(level=p_level, pp=pp))
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _build_shop_embed(self, data: dict) -> discord.Embed:
|
||||||
|
pp = data.get("prestige_points", 0)
|
||||||
|
upgrades: dict = data.get("prestige_upgrades") or {}
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_shop"],
|
||||||
|
description=S.PRESTIGE_UI["shop_desc"].format(pp=pp),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
for uid, item in economy.PRESTIGE_SHOP.items():
|
||||||
|
cur_level = upgrades.get(uid, 0)
|
||||||
|
name = f"{item['emoji']} {S.PRESTIGE_SHOP_NAMES[uid]}"
|
||||||
|
cost_str = S.PRESTIGE_UI["shop_maxed"] if cur_level >= item["max_level"] else S.PRESTIGE_UI["shop_cost_fmt"].format(cost=item["pp_cost"])
|
||||||
|
level_str = S.PRESTIGE_UI["shop_level_fmt"].format(cur=cur_level, max=item["max_level"])
|
||||||
|
embed.add_field(name=f"{name} · {level_str} · {cost_str}", value=S.PRESTIGE_SHOP_DESCRIPTIONS[uid], inline=False)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _switch_tab(self, tab_id: str):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
self.tab = tab_id
|
||||||
|
await interaction.response.defer()
|
||||||
|
data = await economy.get_user(self.user_id)
|
||||||
|
await self._rebuild(data)
|
||||||
|
embed = self._build_status_embed(data) if tab_id == "status" else self._build_shop_embed(data)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
def _do_prestige(self):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await interaction.response.defer()
|
||||||
|
res = await economy.do_prestige(self.user_id)
|
||||||
|
self.clear_items()
|
||||||
|
if not res["ok"]:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_too_low"],
|
||||||
|
description=S.PRESTIGE_UI["too_low_desc"].format(level=res.get("level", 0), required=res.get("required", 30)),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_success"].format(level=res["prestige_level"]),
|
||||||
|
description=S.PRESTIGE_UI["success_desc"].format(pp=res["pp_earned"], level=res["prestige_level"], total_pp=res["prestige_points"]),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
if res.get("ok") and interaction.guild:
|
||||||
|
member = interaction.guild.get_member(self.user_id)
|
||||||
|
if member:
|
||||||
|
asyncio.create_task(ensure_level_role(member, 1))
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
def _do_cancel(self):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
self.clear_items()
|
||||||
|
await interaction.response.edit_message(view=self)
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
def _buy_upgrade(self, upgrade_id: str):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await interaction.response.defer()
|
||||||
|
res = await economy.do_prestige_buy(self.user_id, upgrade_id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "insufficient_pp":
|
||||||
|
err = S.PRESTIGE_UI["buy_no_pp"].format(have=res["have"], need=res["need"])
|
||||||
|
elif res["reason"] == "maxed":
|
||||||
|
err = S.PRESTIGE_UI["buy_maxed"]
|
||||||
|
else:
|
||||||
|
err = S.ERR["generic_error"].format(error=res["reason"])
|
||||||
|
await interaction.followup.send(err, ephemeral=True)
|
||||||
|
return
|
||||||
|
data = await economy.get_user(self.user_id)
|
||||||
|
await self._rebuild(data)
|
||||||
|
embed = self._build_shop_embed(data)
|
||||||
|
item = economy.PRESTIGE_SHOP[res["upgrade_id"]]
|
||||||
|
name = f"{item['emoji']} {S.PRESTIGE_SHOP_NAMES[res['upgrade_id']]}"
|
||||||
|
embed.description = S.PRESTIGE_UI["buy_success_desc"].format(
|
||||||
|
name=name,
|
||||||
|
new_level=res["new_level"],
|
||||||
|
max_level=item["max_level"],
|
||||||
|
pp=res["pp_remaining"],
|
||||||
|
) + "\n\n" + (embed.description or "")
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
self.clear_items()
|
||||||
|
|
||||||
|
@tree.command(name="prestige", description=S.CMD["prestige"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_prestige(interaction: discord.Interaction):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
if data.get("eco_banned"):
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
return
|
||||||
|
view = PrestigeView(interaction.user.id)
|
||||||
|
await view._rebuild(data)
|
||||||
|
embed = view._build_status_embed(data)
|
||||||
|
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="prestigeshop", description=S.CMD["prestigeshop"])
|
||||||
|
async def cmd_prestigeshop(interaction: discord.Interaction):
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
view = PrestigeView(interaction.user.id, tab="shop")
|
||||||
|
await view._rebuild(data)
|
||||||
|
embed = view._build_shop_embed(data)
|
||||||
|
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="prestigebuy", description=S.CMD["prestigebuy"])
|
||||||
|
@app_commands.describe(upgrade=S.OPT["prestigebuy_upgrade"])
|
||||||
|
async def cmd_prestigebuy(interaction: discord.Interaction, upgrade: str):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
res = await economy.do_prestige_buy(interaction.user.id, upgrade.strip().lower())
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "not_found":
|
||||||
|
await interaction.response.send_message(S.PRESTIGE_UI["buy_not_found"], ephemeral=True)
|
||||||
|
elif res["reason"] == "maxed":
|
||||||
|
await interaction.response.send_message(S.PRESTIGE_UI["buy_maxed"], ephemeral=True)
|
||||||
|
elif res["reason"] == "insufficient_pp":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.PRESTIGE_UI["buy_no_pp"].format(have=res["have"], need=res["need"]),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
item = economy.PRESTIGE_SHOP[res["upgrade_id"]]
|
||||||
|
name = f"{item['emoji']} {S.PRESTIGE_SHOP_NAMES[res['upgrade_id']]}"
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_buy_ok"],
|
||||||
|
description=S.PRESTIGE_UI["buy_success_desc"].format(
|
||||||
|
name=name,
|
||||||
|
new_level=res["new_level"],
|
||||||
|
max_level=res["max_level"],
|
||||||
|
pp=res["pp_remaining"],
|
||||||
|
),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
545
economy_profile_commands.py
Normal file
545
economy_profile_commands.py
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_profile_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
coin: Callable[[int], str],
|
||||||
|
cd_ts: Callable[[datetime.timedelta], str],
|
||||||
|
ensure_level_role: Callable[[discord.Member, int], Awaitable[None]],
|
||||||
|
) -> None:
|
||||||
|
def _profile_main_embed(target: discord.User | discord.Member, data: dict) -> discord.Embed:
|
||||||
|
exp = data.get("exp", 0)
|
||||||
|
level = economy.get_level(exp)
|
||||||
|
role_name = economy.level_role_name(level)
|
||||||
|
next_level = level + 1
|
||||||
|
exp_this = economy.exp_for_level(level)
|
||||||
|
exp_next = economy.exp_for_level(next_level)
|
||||||
|
progress = exp - exp_this
|
||||||
|
needed = exp_next - exp_this
|
||||||
|
pct = progress / needed if needed > 0 else 1.0
|
||||||
|
filled = int(pct * 12)
|
||||||
|
bar = "█" * filled + "░" * (12 - filled)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.PROFILE_UI["main_title"].format(name=target.display_name),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
embed.add_field(name=S.PROFILE_UI["f_balance"], value=coin(data.get("balance", 0)), inline=True)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.PROFILE_UI["f_level"],
|
||||||
|
value=S.PROFILE_UI["level_val"].format(level=level, role=role_name),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
streak = data.get("daily_streak", 0)
|
||||||
|
if streak:
|
||||||
|
embed.add_field(
|
||||||
|
name=S.PROFILE_UI["f_streak"],
|
||||||
|
value=S.BALANCE_UI["streak_val"].format(streak=streak),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
p_level = data.get("prestige_level", 0)
|
||||||
|
if p_level > 0:
|
||||||
|
p_pp = data.get("prestige_points", 0)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.PROFILE_UI["f_prestige"],
|
||||||
|
value=S.PROFILE_UI["prestige_val"].format(level=p_level, pp=p_pp),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
jail_remaining = economy._is_jailed(data)
|
||||||
|
if jail_remaining:
|
||||||
|
embed.add_field(name=S.PROFILE_UI["f_jail"], value=cd_ts(jail_remaining), inline=True)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.PROFILE_UI["f_progress"].format(next=next_level),
|
||||||
|
value=S.PROFILE_UI["progress_bar"].format(bar=bar, done=progress, needed=needed),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
if level < 10:
|
||||||
|
embed.set_footer(text=S.PROFILE_UI["footer_t1"])
|
||||||
|
elif level < 20:
|
||||||
|
embed.set_footer(text=S.PROFILE_UI["footer_t2"])
|
||||||
|
else:
|
||||||
|
embed.set_footer(text=S.PROFILE_UI["footer_t3"])
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _profile_items_embed(target: discord.User | discord.Member, data: dict) -> discord.Embed:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.PROFILE_UI["items_title"].format(name=target.display_name),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
uses_map = data.get("item_uses", {})
|
||||||
|
item_lines = []
|
||||||
|
for item_id in data.get("items", []):
|
||||||
|
if item_id not in economy.SHOP:
|
||||||
|
continue
|
||||||
|
line = f"{economy.SHOP[item_id]['emoji']} **{economy.SHOP[item_id]['name']}**"
|
||||||
|
if item_id in uses_map:
|
||||||
|
uses = uses_map[item_id]
|
||||||
|
line += (
|
||||||
|
S.BALANCE_UI["uses_one"].format(uses=uses)
|
||||||
|
if uses == 1
|
||||||
|
else S.BALANCE_UI["uses_many"].format(uses=uses)
|
||||||
|
)
|
||||||
|
item_lines.append(line)
|
||||||
|
embed.description = "\n".join(item_lines) if item_lines else S.PROFILE_UI["items_empty"]
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _profile_stats_embed(target: discord.User | discord.Member, data: dict) -> discord.Embed:
|
||||||
|
def _s(key: str) -> int:
|
||||||
|
return data.get(key, 0)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.PROFILE_UI["stats_title"].format(name=target.display_name),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["economy_field"],
|
||||||
|
value=S.STATS_UI["economy_val"].format(
|
||||||
|
peak=coin(_s("peak_balance")),
|
||||||
|
earned=coin(_s("lifetime_earned")),
|
||||||
|
lost=coin(_s("lifetime_lost")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["work_field"],
|
||||||
|
value=S.STATS_UI["work_val"].format(work=_s("work_count"), beg=_s("beg_count")),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(name="\u200b", value="\u200b", inline=False)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["gamble_field"],
|
||||||
|
value=S.STATS_UI["gamble_val"].format(
|
||||||
|
wagered=coin(_s("total_wagered")),
|
||||||
|
win=coin(_s("biggest_win")),
|
||||||
|
loss=coin(_s("biggest_loss")),
|
||||||
|
jackpots=_s("slots_jackpots"),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["crime_field"],
|
||||||
|
value=S.STATS_UI["crime_val"].format(
|
||||||
|
crimes=_s("crimes_attempted"),
|
||||||
|
succeeded=_s("crimes_succeeded"),
|
||||||
|
heists=_s("heists_joined"),
|
||||||
|
heists_won=_s("heists_won"),
|
||||||
|
jailed=_s("times_jailed"),
|
||||||
|
bail=coin(_s("total_bail_paid")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(name="\u200b", value="\u200b", inline=False)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["social_field"],
|
||||||
|
value=S.STATS_UI["social_val"].format(
|
||||||
|
given=coin(_s("total_given")),
|
||||||
|
received=coin(_s("total_received")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["records_field"],
|
||||||
|
value=S.STATS_UI["records_val"].format(streak=_s("best_daily_streak")),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _profile_fish_embed(target: discord.User | discord.Member, fish_res: dict) -> discord.Embed:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.PROFILE_UI["fish_title"].format(name=target.display_name),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
book: dict = fish_res["book"]
|
||||||
|
if not book:
|
||||||
|
embed.description = S.FISH_UI["book_empty"]
|
||||||
|
return embed
|
||||||
|
inv_counts: dict = fish_res.get("inv_counts", {})
|
||||||
|
caught_count = fish_res["unique_caught"]
|
||||||
|
total = fish_res["total_species"]
|
||||||
|
lines = [S.FISH_UI["book_caught"].format(caught=caught_count, total=total)]
|
||||||
|
for fish_id, fish_data in economy.FISH_CATALOGUE.items():
|
||||||
|
rarity = fish_data["rarity"]
|
||||||
|
emoji = S.FISH_RARITY_EMOJI[rarity]
|
||||||
|
rarity_name = S.FISH_RARITY_NAMES[rarity]
|
||||||
|
count = book.get(fish_id, 0)
|
||||||
|
if count > 0:
|
||||||
|
n_inv = inv_counts.get(fish_id, 0)
|
||||||
|
inv_str = S.FISH_UI["book_inv"].format(n=n_inv) if n_inv > 0 else ""
|
||||||
|
lines.append(
|
||||||
|
S.FISH_UI["book_yes"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=S.FISH_NAMES[fish_id],
|
||||||
|
rarity=rarity_name,
|
||||||
|
count=count,
|
||||||
|
inv=inv_str,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(S.FISH_UI["book_no"].format(rarity=rarity_name))
|
||||||
|
embed.description = "\n".join(lines)
|
||||||
|
embed.set_footer(
|
||||||
|
text=S.FISH_UI["book_footer"].format(
|
||||||
|
page=1,
|
||||||
|
total_pages=1,
|
||||||
|
caught=caught_count,
|
||||||
|
total=total,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
class ProfileView(discord.ui.View):
|
||||||
|
def __init__(self, target: discord.User | discord.Member, invoker_id: int, tab: str = "main"):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self.target = target
|
||||||
|
self.invoker_id = invoker_id
|
||||||
|
self.tab = tab
|
||||||
|
self._rebuild()
|
||||||
|
|
||||||
|
def _rebuild(self):
|
||||||
|
self.clear_items()
|
||||||
|
tabs = [
|
||||||
|
("main", S.PROFILE_UI["btn_profile"]),
|
||||||
|
("items", S.PROFILE_UI["btn_items"]),
|
||||||
|
("stats", S.PROFILE_UI["btn_stats"]),
|
||||||
|
("fish", S.PROFILE_UI["btn_fish"]),
|
||||||
|
]
|
||||||
|
for tab_id, label in tabs:
|
||||||
|
btn = discord.ui.Button(
|
||||||
|
label=label,
|
||||||
|
style=(
|
||||||
|
discord.ButtonStyle.primary
|
||||||
|
if tab_id == self.tab
|
||||||
|
else discord.ButtonStyle.secondary
|
||||||
|
),
|
||||||
|
disabled=(tab_id == self.tab),
|
||||||
|
)
|
||||||
|
btn.callback = self._make_cb(tab_id)
|
||||||
|
self.add_item(btn)
|
||||||
|
|
||||||
|
def _make_cb(self, tab_id: str):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.invoker_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
self.tab = tab_id
|
||||||
|
self._rebuild()
|
||||||
|
await interaction.response.defer()
|
||||||
|
data = await economy.get_user(self.target.id)
|
||||||
|
if tab_id == "fish":
|
||||||
|
fish_res = await economy.do_fishbook(self.target.id)
|
||||||
|
embed = _profile_fish_embed(self.target, fish_res)
|
||||||
|
inv: list = data.get("fish_inventory") or []
|
||||||
|
if inv and self.target.id == self.invoker_id:
|
||||||
|
total_value = sum(entry.get("value", 0) for entry in inv)
|
||||||
|
sell_btn = discord.ui.Button(
|
||||||
|
label=(
|
||||||
|
f"{S.FISH_UI['btn_sell']} "
|
||||||
|
f"({len(inv)} kala · {total_value:,} {economy.COIN})"
|
||||||
|
),
|
||||||
|
style=discord.ButtonStyle.success,
|
||||||
|
row=1,
|
||||||
|
)
|
||||||
|
sell_btn.callback = self._sell_fish_cb()
|
||||||
|
self.add_item(sell_btn)
|
||||||
|
elif tab_id == "items":
|
||||||
|
embed = _profile_items_embed(self.target, data)
|
||||||
|
elif tab_id == "stats":
|
||||||
|
embed = _profile_stats_embed(self.target, data)
|
||||||
|
else:
|
||||||
|
embed = _profile_main_embed(self.target, data)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
def _sell_fish_cb(self):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.invoker_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await interaction.response.defer()
|
||||||
|
res = await economy.do_fish_sell(self.invoker_id)
|
||||||
|
self.tab = "fish"
|
||||||
|
self._rebuild()
|
||||||
|
fish_res = await economy.do_fishbook(self.target.id)
|
||||||
|
embed = _profile_fish_embed(self.target, fish_res)
|
||||||
|
sold_line = S.FISH_UI["inv_sold_all"].format(
|
||||||
|
count=res.get("count", 0),
|
||||||
|
coins=coin(res.get("coins", 0)),
|
||||||
|
balance=coin(res.get("balance", 0)),
|
||||||
|
)
|
||||||
|
embed.description = f"{sold_line}\n\n{embed.description or ''}"
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
def _balance_embed(user: discord.User | discord.Member, data: dict) -> discord.Embed:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{economy.COIN} {user.display_name}",
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
embed.add_field(name=S.BALANCE_UI["saldo"], value=coin(data["balance"]), inline=True)
|
||||||
|
streak = data.get("daily_streak", 0)
|
||||||
|
if streak:
|
||||||
|
embed.add_field(
|
||||||
|
name=S.BALANCE_UI["streak"],
|
||||||
|
value=S.BALANCE_UI["streak_val"].format(streak=streak),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
jail_remaining = economy._is_jailed(data)
|
||||||
|
if jail_remaining:
|
||||||
|
embed.add_field(name=S.BALANCE_UI["jailed_until"], value=cd_ts(jail_remaining), inline=True)
|
||||||
|
item_lines = []
|
||||||
|
uses_map = data.get("item_uses", {})
|
||||||
|
for item_id in data.get("items", []):
|
||||||
|
if item_id not in economy.SHOP:
|
||||||
|
continue
|
||||||
|
line = f"{economy.SHOP[item_id]['emoji']} {economy.SHOP[item_id]['name']}"
|
||||||
|
if item_id in uses_map:
|
||||||
|
uses = uses_map[item_id]
|
||||||
|
line += (
|
||||||
|
S.BALANCE_UI["uses_one"].format(uses=uses)
|
||||||
|
if uses == 1
|
||||||
|
else S.BALANCE_UI["uses_many"].format(uses=uses)
|
||||||
|
)
|
||||||
|
item_lines.append(line)
|
||||||
|
if item_lines:
|
||||||
|
embed.add_field(name=S.BALANCE_UI["items"], value="\n".join(item_lines), inline=False)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
@tree.command(name="profile", description=S.CMD["profile"])
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["profile_kasutaja"])
|
||||||
|
async def cmd_profile(interaction: discord.Interaction, kasutaja: discord.Member | None = None):
|
||||||
|
target = kasutaja or interaction.user
|
||||||
|
data = await economy.get_user(target.id)
|
||||||
|
embed = _profile_main_embed(target, data)
|
||||||
|
invoker_id = interaction.user.id
|
||||||
|
await interaction.response.send_message(embed=embed, view=ProfileView(target, invoker_id))
|
||||||
|
if not kasutaja and interaction.guild:
|
||||||
|
member = interaction.guild.get_member(target.id)
|
||||||
|
if member:
|
||||||
|
asyncio.create_task(ensure_level_role(member, economy.get_level(data.get("exp", 0))))
|
||||||
|
|
||||||
|
@tree.command(name="balance", description=S.CMD["balance"])
|
||||||
|
async def cmd_balance(interaction: discord.Interaction, kasutaja: discord.Member | None = None):
|
||||||
|
target = kasutaja or interaction.user
|
||||||
|
data = await economy.get_user(target.id)
|
||||||
|
await interaction.response.send_message(embed=_balance_embed(target, data))
|
||||||
|
|
||||||
|
@tree.command(name="cooldowns", description=S.CMD["cooldowns"])
|
||||||
|
async def cmd_cooldowns(interaction: discord.Interaction):
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
items = set(data.get("items", []))
|
||||||
|
|
||||||
|
def _status(last_key: str, cooldown: datetime.timedelta) -> str:
|
||||||
|
raw = data.get(last_key)
|
||||||
|
if not raw:
|
||||||
|
return S.COOLDOWNS_UI["ready"]
|
||||||
|
last = economy._parse_dt(raw)
|
||||||
|
if last is None:
|
||||||
|
return S.COOLDOWNS_UI["ready"]
|
||||||
|
expires = last + cooldown
|
||||||
|
if expires <= now:
|
||||||
|
return S.COOLDOWNS_UI["ready"]
|
||||||
|
ts = int(expires.timestamp())
|
||||||
|
return f"⏳ <t:{ts}:R>"
|
||||||
|
|
||||||
|
work_cd = datetime.timedelta(minutes=40) if "monitor" in items else economy.COOLDOWNS["work"]
|
||||||
|
beg_cd = datetime.timedelta(minutes=3) if "hiirematt" in items else economy.COOLDOWNS["beg"]
|
||||||
|
daily_cd = datetime.timedelta(hours=18) if "korvaklapid" in items else economy.COOLDOWNS["daily"]
|
||||||
|
fish_cd = datetime.timedelta(seconds=90) if "ussipurk" in items else economy.COOLDOWNS["fish"]
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
S.COOLDOWNS_UI["daily_line"].format(
|
||||||
|
status=_status("last_daily", daily_cd),
|
||||||
|
note=S.COOLDOWNS_UI["note_korvak"] if "korvaklapid" in items else "",
|
||||||
|
),
|
||||||
|
S.COOLDOWNS_UI["work_line"].format(
|
||||||
|
status=_status("last_work", work_cd),
|
||||||
|
note=S.COOLDOWNS_UI["note_monitor"] if "monitor" in items else "",
|
||||||
|
),
|
||||||
|
S.COOLDOWNS_UI["beg_line"].format(
|
||||||
|
status=_status("last_beg", beg_cd),
|
||||||
|
note=S.COOLDOWNS_UI["note_hiirematt"] if "hiirematt" in items else "",
|
||||||
|
),
|
||||||
|
S.COOLDOWNS_UI["crime_line"].format(status=_status("last_crime", economy.COOLDOWNS["crime"])),
|
||||||
|
S.COOLDOWNS_UI["rob_line"].format(status=_status("last_rob", economy.COOLDOWNS["rob"])),
|
||||||
|
S.COOLDOWNS_UI["fish_line"].format(
|
||||||
|
status=_status("last_fish", fish_cd),
|
||||||
|
note=S.COOLDOWNS_UI["note_ussipurk"] if "ussipurk" in items else "",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
jailed = data.get("jailed_until")
|
||||||
|
if jailed:
|
||||||
|
jail_dt = datetime.datetime.fromisoformat(jailed)
|
||||||
|
if jail_dt > now:
|
||||||
|
ts = int(jail_dt.timestamp())
|
||||||
|
lines.append(S.COOLDOWNS_UI["jailed"].format(ts=ts))
|
||||||
|
else:
|
||||||
|
lines.append(S.COOLDOWNS_UI["jail_expired"])
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["cooldowns"],
|
||||||
|
description="\n".join(lines),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="jailed", description=S.CMD["jailed"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_jailed(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
jailed = await economy.do_get_jailed()
|
||||||
|
if not jailed:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.JAILED_UI["title"],
|
||||||
|
description=S.JAILED_UI["empty"],
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await interaction.followup.send(embed=embed)
|
||||||
|
return
|
||||||
|
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
lines = []
|
||||||
|
for uid, remaining in jailed:
|
||||||
|
ts = int((now + remaining).timestamp())
|
||||||
|
member = interaction.guild.get_member(uid) if interaction.guild else None
|
||||||
|
mention = member.mention if member else f"<@{uid}>"
|
||||||
|
lines.append(S.JAILED_UI["entry"].format(mention=mention, ts=ts))
|
||||||
|
|
||||||
|
plural = "" if len(jailed) == 1 else "i"
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.JAILED_UI["title"],
|
||||||
|
description="\n".join(lines),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
embed.set_footer(text=S.JAILED_UI["footer"].format(count=len(jailed), plural=plural))
|
||||||
|
await interaction.followup.send(embed=embed)
|
||||||
|
|
||||||
|
@tree.command(name="rank", description=S.CMD["rank"])
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["rank_kasutaja"])
|
||||||
|
async def cmd_rank(interaction: discord.Interaction, kasutaja: discord.Member | None = None):
|
||||||
|
target = kasutaja or interaction.user
|
||||||
|
data = await economy.get_user(target.id)
|
||||||
|
exp = data.get("exp", 0)
|
||||||
|
level = economy.get_level(exp)
|
||||||
|
role_name = economy.level_role_name(level)
|
||||||
|
next_level = level + 1
|
||||||
|
exp_this = economy.exp_for_level(level)
|
||||||
|
exp_next = economy.exp_for_level(next_level)
|
||||||
|
progress = exp - exp_this
|
||||||
|
needed = exp_next - exp_this
|
||||||
|
pct = progress / needed if needed > 0 else 1.0
|
||||||
|
filled = int(pct * 12)
|
||||||
|
bar = "█" * filled + "░" * (12 - filled)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.RANK_UI["title"].format(name=target.display_name, level=level),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.add_field(name=S.RANK_UI["field_title"], value=f"**{role_name}**", inline=True)
|
||||||
|
embed.add_field(name=S.RANK_UI["field_exp"], value=str(exp), inline=True)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.RANK_UI["field_progress"].format(next=next_level),
|
||||||
|
value=S.RANK_UI["progress_val"].format(bar=bar, progress=progress, needed=needed),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
p_level = data.get("prestige_level", 0)
|
||||||
|
p_pp = data.get("prestige_points", 0)
|
||||||
|
s_exp = data.get("season_total_exp", 0)
|
||||||
|
if p_level > 0 or s_exp > 0:
|
||||||
|
embed.add_field(
|
||||||
|
name="\u200b",
|
||||||
|
value=(
|
||||||
|
S.PRESTIGE_UI["rank_line"].format(level=p_level, pp=p_pp)
|
||||||
|
+ "\n"
|
||||||
|
+ S.PRESTIGE_UI["rank_season"].format(exp=s_exp)
|
||||||
|
),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
if level < 10:
|
||||||
|
embed.set_footer(text=S.RANK_UI["footer_t1"])
|
||||||
|
elif level < 20:
|
||||||
|
embed.set_footer(text=S.RANK_UI["footer_t2"])
|
||||||
|
else:
|
||||||
|
embed.set_footer(text=S.RANK_UI["footer_t3"])
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
if not kasutaja and interaction.guild:
|
||||||
|
member = interaction.guild.get_member(target.id)
|
||||||
|
if member:
|
||||||
|
asyncio.create_task(ensure_level_role(member, level))
|
||||||
|
|
||||||
|
@tree.command(name="stats", description=S.CMD["stats"])
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["stats_kasutaja"])
|
||||||
|
async def cmd_stats(interaction: discord.Interaction, kasutaja: discord.Member | None = None):
|
||||||
|
target = kasutaja or interaction.user
|
||||||
|
data = await economy.get_user(target.id)
|
||||||
|
|
||||||
|
def _s(key: str) -> int:
|
||||||
|
return data.get(key, 0)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{S.TITLE['stats']} - {target.display_name}",
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["economy_field"],
|
||||||
|
value=S.STATS_UI["economy_val"].format(
|
||||||
|
peak=coin(_s("peak_balance")),
|
||||||
|
earned=coin(_s("lifetime_earned")),
|
||||||
|
lost=coin(_s("lifetime_lost")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["work_field"],
|
||||||
|
value=S.STATS_UI["work_val"].format(work=_s("work_count"), beg=_s("beg_count")),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(name="\u200b", value="\u200b", inline=False)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["gamble_field"],
|
||||||
|
value=S.STATS_UI["gamble_val"].format(
|
||||||
|
wagered=coin(_s("total_wagered")),
|
||||||
|
win=coin(_s("biggest_win")),
|
||||||
|
loss=coin(_s("biggest_loss")),
|
||||||
|
jackpots=_s("slots_jackpots"),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["crime_field"],
|
||||||
|
value=S.STATS_UI["crime_val"].format(
|
||||||
|
crimes=_s("crimes_attempted"),
|
||||||
|
succeeded=_s("crimes_succeeded"),
|
||||||
|
heists=_s("heists_joined"),
|
||||||
|
heists_won=_s("heists_won"),
|
||||||
|
jailed=_s("times_jailed"),
|
||||||
|
bail=coin(_s("total_bail_paid")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(name="\u200b", value="\u200b", inline=False)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["social_field"],
|
||||||
|
value=S.STATS_UI["social_val"].format(
|
||||||
|
given=coin(_s("total_given")),
|
||||||
|
received=coin(_s("total_received")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["records_field"],
|
||||||
|
value=S.STATS_UI["records_val"].format(streak=_s("best_daily_streak")),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
216
economy_support_commands.py
Normal file
216
economy_support_commands.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_support_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
parse_amount: Callable[[str, int], tuple[int | None, str | None]],
|
||||||
|
coin: Callable[[int], str],
|
||||||
|
cancel_reminder_task: Callable[[int, str], None],
|
||||||
|
) -> None:
|
||||||
|
class FundModal(discord.ui.Modal):
|
||||||
|
summa = discord.ui.TextInput(
|
||||||
|
label=S.REQUEST_UI["modal_label"],
|
||||||
|
min_length=1,
|
||||||
|
max_length=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, view: "RequestView"):
|
||||||
|
super().__init__(title=S.REQUEST_UI["modal_title"])
|
||||||
|
self._view = view
|
||||||
|
self.summa.placeholder = f"1 - {view.remaining}"
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
|
amount, err = parse_amount(self.summa.value, 0)
|
||||||
|
if err or amount is None:
|
||||||
|
await interaction.response.send_message(S.ERR["invalid_amount"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if amount <= 0 or amount > self._view.remaining:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["fund_range"].format(max=self._view.remaining), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await economy.do_give(interaction.user.id, self._view.requester.id, amount)
|
||||||
|
if not res["ok"]:
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["broke"].format(bal=coin(data["balance"])), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._view.remaining -= amount
|
||||||
|
funded_line = S.REQUEST_UI["funded_line"].format(
|
||||||
|
name=interaction.user.display_name,
|
||||||
|
amount=coin(amount),
|
||||||
|
)
|
||||||
|
if self._view.remaining <= 0:
|
||||||
|
self._view.fund_btn.disabled = True
|
||||||
|
self._view.fund_btn.label = S.REQUEST_UI["btn_funded"]
|
||||||
|
self._view.fund_btn.style = discord.ButtonStyle.secondary
|
||||||
|
self._view.stop()
|
||||||
|
funded_line += S.REQUEST_UI["funded_full"]
|
||||||
|
else:
|
||||||
|
self._view.fund_btn.label = S.REQUEST_UI["btn_fund_remaining"].format(
|
||||||
|
remaining=self._view.remaining
|
||||||
|
)
|
||||||
|
funded_line += S.REQUEST_UI["funded_partial"].format(
|
||||||
|
remaining=coin(self._view.remaining)
|
||||||
|
)
|
||||||
|
|
||||||
|
await interaction.response.send_message(funded_line)
|
||||||
|
if self._view.message:
|
||||||
|
await self._view.message.edit(view=self._view)
|
||||||
|
|
||||||
|
class RequestView(discord.ui.View):
|
||||||
|
def __init__(self, requester: discord.Member, amount: int, target: discord.Member | None):
|
||||||
|
super().__init__(timeout=300)
|
||||||
|
self.requester = requester
|
||||||
|
self.remaining = amount
|
||||||
|
self.target = target
|
||||||
|
self.message: discord.Message | None = None
|
||||||
|
self.fund_btn = discord.ui.Button(
|
||||||
|
label=S.REQUEST_UI["btn_fund"],
|
||||||
|
style=discord.ButtonStyle.success,
|
||||||
|
)
|
||||||
|
self.fund_btn.callback = self._fund
|
||||||
|
self.add_item(self.fund_btn)
|
||||||
|
|
||||||
|
async def _fund(self, interaction: discord.Interaction):
|
||||||
|
if interaction.user.id == self.requester.id:
|
||||||
|
await interaction.response.send_message(S.ERR["request_self_fund"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if self.target and interaction.user.id != self.target.id:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["request_targeted"].format(name=self.target.display_name),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await interaction.response.send_modal(FundModal(self))
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
for item in self.children:
|
||||||
|
item.disabled = True
|
||||||
|
|
||||||
|
max_request = 1_000_000
|
||||||
|
|
||||||
|
@tree.command(name="request", description=S.CMD["request"])
|
||||||
|
@app_commands.describe(
|
||||||
|
summa=S.OPT["request_summa"],
|
||||||
|
põhjus=S.OPT["request_põhjus"],
|
||||||
|
sihtmärk=S.OPT["request_sihtmärk"],
|
||||||
|
)
|
||||||
|
async def cmd_request(
|
||||||
|
interaction: discord.Interaction,
|
||||||
|
summa: str,
|
||||||
|
põhjus: str,
|
||||||
|
sihtmärk: discord.Member | None = None,
|
||||||
|
):
|
||||||
|
summa_int, err = parse_amount(summa, 0)
|
||||||
|
if err or summa_int is None:
|
||||||
|
await interaction.response.send_message(err or S.ERR["invalid_amount"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if summa_int <= 0:
|
||||||
|
await interaction.response.send_message(S.ERR["positive_amount"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if summa_int > max_request:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["fund_range"].format(max=coin(max_request)),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
summa = summa_int
|
||||||
|
if sihtmärk and sihtmärk.id == interaction.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["request_self"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if sihtmärk and sihtmärk.bot:
|
||||||
|
await interaction.response.send_message(S.ERR["request_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
audience = (
|
||||||
|
S.REQUEST_UI["audience_targeted"].format(name=sihtmärk.display_name)
|
||||||
|
if sihtmärk
|
||||||
|
else S.REQUEST_UI["audience_all"]
|
||||||
|
)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["request"],
|
||||||
|
description=S.REQUEST_UI["desc"].format(
|
||||||
|
requester=interaction.user.display_name,
|
||||||
|
amount=coin(summa),
|
||||||
|
reason=põhjus,
|
||||||
|
audience=audience,
|
||||||
|
),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
embed.set_footer(text=S.REQUEST_UI["footer"])
|
||||||
|
view = RequestView(interaction.user, summa, sihtmärk)
|
||||||
|
await interaction.response.send_message(embed=embed, view=view)
|
||||||
|
view.message = await interaction.original_response()
|
||||||
|
|
||||||
|
class RemindersSelect(discord.ui.Select):
|
||||||
|
def __init__(self, user_id: int, current: list[str]):
|
||||||
|
self.user_id = user_id
|
||||||
|
options = [
|
||||||
|
discord.SelectOption(
|
||||||
|
label=label,
|
||||||
|
description=desc,
|
||||||
|
value=cmd,
|
||||||
|
default=cmd in current,
|
||||||
|
)
|
||||||
|
for cmd, label, desc in S.REMINDER_OPTS
|
||||||
|
]
|
||||||
|
super().__init__(
|
||||||
|
placeholder=S.REMINDERS_UI["select_placeholder"],
|
||||||
|
options=options,
|
||||||
|
min_values=0,
|
||||||
|
max_values=len(S.REMINDER_OPTS),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def callback(self, interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await economy.do_set_reminders(self.user_id, self.values)
|
||||||
|
enabled = set(self.values)
|
||||||
|
for cmd in [opt[0] for opt in S.REMINDER_OPTS]:
|
||||||
|
if cmd not in enabled:
|
||||||
|
cancel_reminder_task(self.user_id, cmd)
|
||||||
|
if self.values:
|
||||||
|
names = " ".join(f"`/{v}`" for v in self.values)
|
||||||
|
msg = S.REMINDERS_UI["saved_on"].format(names=names)
|
||||||
|
else:
|
||||||
|
msg = S.REMINDERS_UI["saved_off"]
|
||||||
|
await interaction.response.send_message(msg, ephemeral=True)
|
||||||
|
|
||||||
|
class RemindersView(discord.ui.View):
|
||||||
|
def __init__(self, user_id: int, current: list[str]):
|
||||||
|
super().__init__(timeout=60)
|
||||||
|
self.add_item(RemindersSelect(user_id, current))
|
||||||
|
|
||||||
|
@tree.command(name="reminders", description=S.CMD["reminders"])
|
||||||
|
async def cmd_reminders(interaction: discord.Interaction):
|
||||||
|
user_data = await economy.get_user(interaction.user.id)
|
||||||
|
current = user_data.get("reminders", [])
|
||||||
|
if current:
|
||||||
|
status = " ".join(f"`/{c}`" for c in current)
|
||||||
|
desc = S.REMINDERS_UI["desc_active"].format(status=status)
|
||||||
|
else:
|
||||||
|
desc = S.REMINDERS_UI["desc_none"]
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["reminders"],
|
||||||
|
description=desc,
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.set_footer(text=S.REMINDERS_UI["footer"])
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=embed,
|
||||||
|
view=RemindersView(interaction.user.id, current),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
1519
logs/bot.log
1519
logs/bot.log
File diff suppressed because it is too large
Load Diff
@@ -1,714 +0,0 @@
|
|||||||
2026-03-20 01:34:58 | WORK user=340451525799182357 earned=+123 lucky=False bal=27988
|
|
||||||
2026-03-20 01:35:02 | BEG user=340451525799182357 earned=+24 jailed=False bal=28012
|
|
||||||
2026-03-20 01:35:39 | ROB_FAIL robber=340451525799182357 victim=218972931701735424 fine=-120 robber_bal=27892
|
|
||||||
2026-03-20 01:35:56 | SLOTS_MISS user=340451525799182357 bet=100 change=-100 bal=27792
|
|
||||||
2026-03-20 01:36:04 | SLOTS_PAIR user=340451525799182357 bet=100 change=50 bal=27842
|
|
||||||
2026-03-20 01:36:11 | SLOTS_MISS user=340451525799182357 bet=100 change=-100 bal=27742
|
|
||||||
2026-03-20 01:36:19 | SLOTS_PAIR user=340451525799182357 bet=100 change=50 bal=27792
|
|
||||||
2026-03-20 01:37:38 | BLACKJACK user=340451525799182357 payout=+400 net=+200 bal=27992
|
|
||||||
2026-03-20 01:38:07 | BEG user=340451525799182357 earned=+30 jailed=False bal=28022
|
|
||||||
2026-03-20 01:38:23 | BLACKJACK user=340451525799182357 payout=+0 net=-100 bal=27922
|
|
||||||
2026-03-20 01:38:43 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=26922
|
|
||||||
2026-03-20 01:39:15 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=27922
|
|
||||||
2026-03-20 01:39:36 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=29922
|
|
||||||
2026-03-20 01:39:49 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=30922
|
|
||||||
2026-03-20 01:40:02 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=31922
|
|
||||||
2026-03-20 01:40:12 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=30922
|
|
||||||
2026-03-20 01:40:26 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=29922
|
|
||||||
2026-03-20 01:40:40 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=30922
|
|
||||||
2026-03-20 01:40:54 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=31922
|
|
||||||
2026-03-20 01:41:05 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=30922
|
|
||||||
2026-03-20 01:41:26 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=29922
|
|
||||||
2026-03-20 01:41:35 | BLACKJACK user=340451525799182357 payout=+2500 net=+1500 bal=31422
|
|
||||||
2026-03-20 01:42:32 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=32422
|
|
||||||
2026-03-20 01:42:46 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=31422
|
|
||||||
2026-03-20 01:43:34 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=30422
|
|
||||||
2026-03-20 01:43:48 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=32422
|
|
||||||
2026-03-20 01:44:00 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=31422
|
|
||||||
2026-03-20 01:44:11 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=32422
|
|
||||||
2026-03-20 01:44:22 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=33422
|
|
||||||
2026-03-20 01:44:47 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=35422
|
|
||||||
2026-03-20 01:45:02 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=36422
|
|
||||||
2026-03-20 01:45:17 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=35422
|
|
||||||
2026-03-20 01:45:28 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=36422
|
|
||||||
2026-03-20 01:45:42 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=37422
|
|
||||||
2026-03-20 01:45:55 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=38422
|
|
||||||
2026-03-20 01:46:15 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=39422
|
|
||||||
2026-03-20 01:46:27 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=38422
|
|
||||||
2026-03-20 01:46:38 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=37422
|
|
||||||
2026-03-20 01:46:54 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=38422
|
|
||||||
2026-03-20 01:47:05 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=37422
|
|
||||||
2026-03-20 01:47:15 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=36422
|
|
||||||
2026-03-20 01:47:46 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=38422
|
|
||||||
2026-03-20 01:47:59 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=40422
|
|
||||||
2026-03-20 01:48:11 | BLACKJACK user=340451525799182357 payout=+2000 net=+0 bal=40422
|
|
||||||
2026-03-20 01:48:23 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=41422
|
|
||||||
2026-03-20 01:48:39 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=40422
|
|
||||||
2026-03-20 01:48:54 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=41422
|
|
||||||
2026-03-20 01:49:12 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=42422
|
|
||||||
2026-03-20 01:49:25 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=43422
|
|
||||||
2026-03-20 01:49:36 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=43422
|
|
||||||
2026-03-20 01:49:51 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=44422
|
|
||||||
2026-03-20 01:50:06 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=45422
|
|
||||||
2026-03-20 01:50:16 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=45422
|
|
||||||
2026-03-20 01:50:28 | BLACKJACK user=340451525799182357 payout=+0 net=-2000 bal=43422
|
|
||||||
2026-03-20 01:50:38 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=42422
|
|
||||||
2026-03-20 01:50:53 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=41422
|
|
||||||
2026-03-20 01:51:04 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=40422
|
|
||||||
2026-03-20 01:51:20 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=39422
|
|
||||||
2026-03-20 01:51:31 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=40422
|
|
||||||
2026-03-20 01:51:44 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=40422
|
|
||||||
2026-03-20 01:51:54 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=39422
|
|
||||||
2026-03-20 01:52:07 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=38422
|
|
||||||
2026-03-20 01:52:20 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=39422
|
|
||||||
2026-03-20 01:52:33 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=38422
|
|
||||||
2026-03-20 01:52:43 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=39422
|
|
||||||
2026-03-20 01:52:58 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=40422
|
|
||||||
2026-03-20 01:54:04 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=41422
|
|
||||||
2026-03-20 01:54:20 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=42422
|
|
||||||
2026-03-20 01:55:01 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=41422
|
|
||||||
2026-03-20 01:55:12 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=41422
|
|
||||||
2026-03-20 01:55:20 | BLACKJACK user=340451525799182357 payout=+2500 net=+1500 bal=42922
|
|
||||||
2026-03-20 01:55:42 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=43922
|
|
||||||
2026-03-20 01:55:54 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=44922
|
|
||||||
2026-03-20 01:56:02 | BLACKJACK user=340451525799182357 payout=+2500 net=+1500 bal=46422
|
|
||||||
2026-03-20 01:56:16 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=45422
|
|
||||||
2026-03-20 01:56:27 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=44422
|
|
||||||
2026-03-20 01:56:39 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=45422
|
|
||||||
2026-03-20 01:56:49 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=45422
|
|
||||||
2026-03-20 01:57:01 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=45422
|
|
||||||
2026-03-20 01:58:03 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=44422
|
|
||||||
2026-03-20 01:58:19 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43422
|
|
||||||
2026-03-20 01:59:08 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=42422
|
|
||||||
2026-03-20 01:59:22 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=43422
|
|
||||||
2026-03-20 01:59:37 | BLACKJACK user=340451525799182357 payout=+2500 net=+1500 bal=44922
|
|
||||||
2026-03-20 01:59:56 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43922
|
|
||||||
2026-03-20 02:00:14 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=42922
|
|
||||||
2026-03-20 02:00:37 | BLACKJACK user=340451525799182357 payout=+2500 net=+1500 bal=44422
|
|
||||||
2026-03-20 02:00:57 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=45422
|
|
||||||
2026-03-20 02:01:08 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=44422
|
|
||||||
2026-03-20 02:01:38 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43422
|
|
||||||
2026-03-20 02:01:56 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=42422
|
|
||||||
2026-03-20 02:02:15 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=43422
|
|
||||||
2026-03-20 02:02:30 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=44422
|
|
||||||
2026-03-20 02:02:43 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=45422
|
|
||||||
2026-03-20 02:02:55 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=44422
|
|
||||||
2026-03-20 02:03:07 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43422
|
|
||||||
2026-03-20 02:03:32 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=44422
|
|
||||||
2026-03-20 02:03:48 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=45422
|
|
||||||
2026-03-20 02:04:01 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=44422
|
|
||||||
2026-03-20 02:04:20 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43422
|
|
||||||
2026-03-20 02:04:32 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=44422
|
|
||||||
2026-03-20 02:04:45 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43422
|
|
||||||
2026-03-20 02:04:57 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=45422
|
|
||||||
2026-03-20 02:05:17 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=46422
|
|
||||||
2026-03-20 02:09:33 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=47422
|
|
||||||
2026-03-20 02:09:46 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=48422
|
|
||||||
2026-03-20 02:10:00 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=49422
|
|
||||||
2026-03-20 02:10:17 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=48422
|
|
||||||
2026-03-20 02:10:27 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=47422
|
|
||||||
2026-03-20 02:10:41 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=48422
|
|
||||||
2026-03-20 02:11:43 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=49422
|
|
||||||
2026-03-20 02:20:03 | ROB_BLOCKED robber=178852380018868224 victim=340451525799182357 fine=-198 robber_bal=237 ac_uses_left=1
|
|
||||||
2026-03-20 02:50:48 | BEG user=178852380018868224 earned=+64 jailed=False bal=301
|
|
||||||
2026-03-20 02:50:50 | WORK user=178852380018868224 earned=+92 lucky=False bal=393
|
|
||||||
2026-03-20 02:50:53 | CRIME_WIN user=178852380018868224 earned=+414 bal=807
|
|
||||||
2026-03-20 03:11:24 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=48422
|
|
||||||
2026-03-20 04:25:19 | HEIST_HOUSE change=-79803 house_bal=123830
|
|
||||||
2026-03-20 04:25:19 | HEIST_WIN user=340451525799182357 change=+39901 bal=88323
|
|
||||||
2026-03-20 04:25:19 | HEIST_WIN user=178852380018868224 change=+39901 bal=40708
|
|
||||||
2026-03-20 04:26:00 | DAILY user=178852380018868224 earned=+950 streak=5 bal=41658
|
|
||||||
2026-03-20 04:28:21 | WORK user=340451525799182357 earned=+114 lucky=False bal=88437
|
|
||||||
2026-03-20 04:28:26 | BEG user=340451525799182357 earned=+72 jailed=False bal=88509
|
|
||||||
2026-03-20 04:29:11 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=89509
|
|
||||||
2026-03-20 04:30:44 | BLACKJACK user=340451525799182357 payout=+2500 net=+1500 bal=91009
|
|
||||||
2026-03-20 04:31:09 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=90009
|
|
||||||
2026-03-20 04:31:28 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=91009
|
|
||||||
2026-03-20 04:31:45 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=92009
|
|
||||||
2026-03-20 04:32:10 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=92009
|
|
||||||
2026-03-20 04:32:30 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=91009
|
|
||||||
2026-03-20 04:32:52 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=93009
|
|
||||||
2026-03-20 04:33:11 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=92009
|
|
||||||
2026-03-20 04:33:32 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=92009
|
|
||||||
2026-03-20 04:33:48 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=91009
|
|
||||||
2026-03-20 04:34:07 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=90009
|
|
||||||
2026-03-20 04:34:26 | BLACKJACK user=340451525799182357 payout=+0 net=-3000 bal=87009
|
|
||||||
2026-03-20 04:34:51 | BLACKJACK user=340451525799182357 payout=+0 net=-10000 bal=77009
|
|
||||||
2026-03-20 04:35:14 | BLACKJACK user=340451525799182357 payout=+0 net=-20000 bal=57009
|
|
||||||
2026-03-20 04:35:33 | BLACKJACK user=340451525799182357 payout=+0 net=-57009 bal=0
|
|
||||||
2026-03-20 06:12:01 | DAILY user=272518654715887618 earned=+225 streak=5 bal=930
|
|
||||||
2026-03-20 06:43:11 | BEG user=338622999127261185 earned=+78 jailed=False bal=751
|
|
||||||
2026-03-20 06:43:13 | WORK user=338622999127261185 earned=+102 lucky=False bal=853
|
|
||||||
2026-03-20 06:43:17 | CRIME_WIN user=338622999127261185 earned=+331 bal=1184
|
|
||||||
2026-03-20 06:44:17 | ROB_FAIL robber=338622999127261185 victim=218972931701735424 fine=-111 robber_bal=1073
|
|
||||||
2026-03-20 06:46:27 | BEG user=338622999127261185 earned=+30 jailed=False bal=1103
|
|
||||||
2026-03-20 07:00:53 | DAILY user=824516445382901800 earned=+491 streak=5 bal=1328
|
|
||||||
2026-03-20 07:03:25 | BEG user=338622999127261185 earned=+24 jailed=False bal=1127
|
|
||||||
2026-03-20 07:03:29 | DAILY user=338622999127261185 earned=+450 streak=5 bal=1577
|
|
||||||
2026-03-20 07:03:37 | ROULETTE_LOSE user=338622999127261185 bet=1577 colour=punane result=roheline mult=1 bal=0
|
|
||||||
2026-03-20 07:06:42 | BEG user=338622999127261185 earned=+58 jailed=False bal=58
|
|
||||||
2026-03-20 07:17:30 | CRIME_WIN user=401373976431165449 earned=+461 bal=10256
|
|
||||||
2026-03-20 07:17:46 | ROB_WIN robber=401373976431165449 victim=218972931701735424 stolen=+703 jackpot=False robber_bal=10959 victim_bal=2816
|
|
||||||
2026-03-20 07:17:52 | WORK user=401373976431165449 earned=+174 lucky=True bal=11133
|
|
||||||
2026-03-20 07:17:54 | BEG user=401373976431165449 earned=+58 jailed=False bal=11191
|
|
||||||
2026-03-20 07:18:00 | DAILY user=401373976431165449 earned=+950 streak=5 bal=12141
|
|
||||||
2026-03-20 07:25:09 | WORK user=338622999127261185 earned=+36 lucky=False bal=94
|
|
||||||
2026-03-20 07:25:11 | BEG user=338622999127261185 earned=+38 jailed=False bal=132
|
|
||||||
2026-03-20 07:28:30 | BEG user=401373976431165449 earned=+32 jailed=False bal=12173
|
|
||||||
2026-03-20 07:41:15 | BEG user=401373976431165449 earned=+38 jailed=False bal=12211
|
|
||||||
2026-03-20 07:51:21 | DAILY user=344531774518591498 earned=+500 streak=5 bal=1518
|
|
||||||
2026-03-20 07:51:27 | WORK user=344531774518591498 earned=+275 lucky=True bal=1793
|
|
||||||
2026-03-20 07:51:29 | BEG user=344531774518591498 earned=+42 jailed=False bal=1835
|
|
||||||
2026-03-20 07:51:39 | ROB_BLOCKED robber=344531774518591498 victim=178852380018868224 fine=-130 robber_bal=1705 ac_uses_left=0
|
|
||||||
2026-03-20 07:51:47 | CRIME_WIN user=344531774518591498 earned=+456 bal=2161
|
|
||||||
2026-03-20 07:55:18 | BLACKJACK user=344531774518591498 payout=+2000 net=+1000 bal=3161
|
|
||||||
2026-03-20 07:55:34 | BLACKJACK user=344531774518591498 payout=+0 net=-1000 bal=2161
|
|
||||||
2026-03-20 07:55:48 | BLACKJACK user=344531774518591498 payout=+0 net=-1000 bal=1161
|
|
||||||
2026-03-20 07:56:02 | BLACKJACK user=344531774518591498 payout=+1161 net=+0 bal=1161
|
|
||||||
2026-03-20 07:56:15 | BLACKJACK user=344531774518591498 payout=+0 net=-1161 bal=0
|
|
||||||
2026-03-20 08:08:09 | BEG user=401373976431165449 earned=+26 jailed=False bal=12237
|
|
||||||
2026-03-20 08:08:19 | WORK user=401373976431165449 earned=+205 lucky=False bal=12442
|
|
||||||
2026-03-20 08:25:18 | BEG user=401373976431165449 earned=+44 jailed=False bal=12486
|
|
||||||
2026-03-20 08:30:56 | HEIST_FAIL user=401373976431165449 fine=-1000 jailed_until=2026-03-20T08:00:56.238832+00:00 bal=11486
|
|
||||||
2026-03-20 08:30:56 | HEIST_FAIL user=824516445382901800 fine=-199 jailed_until=2026-03-20T08:00:56.238832+00:00 bal=1129
|
|
||||||
2026-03-20 08:31:23 | JAIL_FREE user=824516445382901800 method=doubles
|
|
||||||
2026-03-20 08:31:46 | JAIL_FREE user=401373976431165449 method=doubles
|
|
||||||
2026-03-20 08:32:25 | BEG user=401373976431165449 earned=+64 jailed=False bal=11550
|
|
||||||
2026-03-20 08:32:31 | BEG user=338622999127261185 earned=+52 jailed=False bal=184
|
|
||||||
2026-03-20 08:34:19 | WORK user=344531774518591498 earned=+258 lucky=True bal=258
|
|
||||||
2026-03-20 08:35:58 | BEG user=401373976431165449 earned=+72 jailed=False bal=11622
|
|
||||||
2026-03-20 08:46:55 | BEG user=401373976431165449 earned=+42 jailed=False bal=11664
|
|
||||||
2026-03-20 09:04:17 | WORK user=272518654715887618 earned=+83 lucky=False bal=1013
|
|
||||||
2026-03-20 09:06:05 | BEG user=401373976431165449 earned=+72 jailed=False bal=11736
|
|
||||||
2026-03-20 09:06:07 | WORK user=401373976431165449 earned=+54 lucky=False bal=11790
|
|
||||||
2026-03-20 09:17:49 | BEG user=401373976431165449 earned=+64 jailed=False bal=11854
|
|
||||||
2026-03-20 09:17:50 | CRIME_WIN user=401373976431165449 earned=+456 bal=12310
|
|
||||||
2026-03-20 09:18:22 | ROB_WIN robber=401373976431165449 victim=272518654715887618 stolen=+214 jackpot=False robber_bal=12524 victim_bal=799
|
|
||||||
2026-03-20 09:18:25 | WORK user=344531774518591498 earned=+61 lucky=False bal=319
|
|
||||||
2026-03-20 09:18:27 | BEG user=344531774518591498 earned=+28 jailed=False bal=347
|
|
||||||
2026-03-20 09:21:07 | BUY user=178852380018868224 item=anticheat cost=-1000 bal=40658
|
|
||||||
2026-03-20 09:28:45 | BEG user=401373976431165449 earned=+56 jailed=False bal=12580
|
|
||||||
2026-03-20 09:31:49 | BEG user=824516445382901800 earned=+80 jailed=False bal=1209
|
|
||||||
2026-03-20 09:32:13 | WORK user=824516445382901800 earned=+116 lucky=False bal=1325
|
|
||||||
2026-03-20 09:32:17 | CRIME_WIN user=824516445382901800 earned=+412 bal=1737
|
|
||||||
2026-03-20 09:32:24 | ROB_BLOCKED robber=824516445382901800 victim=323906492073771019 fine=-200 robber_bal=1537 ac_uses_left=1
|
|
||||||
2026-03-20 09:49:18 | BEG user=401373976431165449 earned=+68 jailed=False bal=12648
|
|
||||||
2026-03-20 09:49:23 | WORK user=401373976431165449 earned=+123 lucky=True bal=12771
|
|
||||||
2026-03-20 10:00:53 | BEG user=344531774518591498 earned=+54 jailed=False bal=401
|
|
||||||
2026-03-20 10:01:11 | CRIME_WIN user=344531774518591498 earned=+492 bal=893
|
|
||||||
2026-03-20 10:01:31 | ROB_BLOCKED robber=344531774518591498 victim=323906492073771019 fine=-110 robber_bal=783 ac_uses_left=0
|
|
||||||
2026-03-20 10:01:44 | WORK user=344531774518591498 earned=+168 lucky=False bal=951
|
|
||||||
2026-03-20 10:05:58 | BEG user=344531774518591498 earned=+54 jailed=False bal=1005
|
|
||||||
2026-03-20 10:10:28 | BEG user=344531774518591498 earned=+42 jailed=False bal=1047
|
|
||||||
2026-03-20 10:12:31 | BUY user=323906492073771019 item=anticheat cost=-1000 bal=42624
|
|
||||||
2026-03-20 10:14:15 | BEG user=323906492073771019 earned=+36 jailed=False bal=42660
|
|
||||||
2026-03-20 10:14:23 | WORK user=323906492073771019 earned=+101 lucky=False bal=42761
|
|
||||||
2026-03-20 10:17:56 | BEG user=338622999127261185 earned=+78 jailed=False bal=262
|
|
||||||
2026-03-20 10:17:58 | WORK user=338622999127261185 earned=+83 lucky=False bal=345
|
|
||||||
2026-03-20 10:18:01 | CRIME_WIN user=338622999127261185 earned=+290 bal=635
|
|
||||||
2026-03-20 10:18:44 | ROB_FAIL robber=338622999127261185 victim=209554152584380420 fine=-136 robber_bal=499
|
|
||||||
2026-03-20 10:24:36 | BEG user=344531774518591498 earned=+58 jailed=False bal=1105
|
|
||||||
2026-03-20 10:27:33 | BEG user=401373976431165449 earned=+32 jailed=False bal=12803
|
|
||||||
2026-03-20 10:28:15 | BLACKJACK user=401373976431165449 payout=+0 net=-100 bal=12703
|
|
||||||
2026-03-20 10:28:27 | BEG user=344531774518591498 earned=+78 jailed=False bal=1183
|
|
||||||
2026-03-20 10:28:38 | BLACKJACK user=401373976431165449 payout=+200 net=+100 bal=12803
|
|
||||||
2026-03-20 10:28:56 | GIVE from_=401373976431165449 to=344531774518591498 amount=500 from_bal=12203 to_bal=1683
|
|
||||||
2026-03-20 10:29:04 | BLACKJACK user=401373976431165449 payout=+200 net=+100 bal=12403
|
|
||||||
2026-03-20 10:29:15 | BLACKJACK user=401373976431165449 payout=+250 net=+150 bal=12553
|
|
||||||
2026-03-20 10:29:36 | BLACKJACK user=401373976431165449 payout=+400 net=+200 bal=12753
|
|
||||||
2026-03-20 10:29:58 | BLACKJACK user=401373976431165449 payout=+100 net=+0 bal=12753
|
|
||||||
2026-03-20 10:30:11 | BLACKJACK user=401373976431165449 payout=+1250 net=+750 bal=13503
|
|
||||||
2026-03-20 10:30:28 | BLACKJACK user=401373976431165449 payout=+0 net=-100 bal=13403
|
|
||||||
2026-03-20 10:30:37 | WORK user=401373976431165449 earned=+371 lucky=True bal=13674
|
|
||||||
2026-03-20 10:31:05 | BLACKJACK user=401373976431165449 payout=+0 net=-200 bal=13574
|
|
||||||
2026-03-20 10:31:20 | BLACKJACK user=401373976431165449 payout=+0 net=-100 bal=13474
|
|
||||||
2026-03-20 10:31:35 | BLACKJACK user=401373976431165449 payout=+200 net=+100 bal=13574
|
|
||||||
2026-03-20 10:31:47 | BLACKJACK user=401373976431165449 payout=+200 net=+100 bal=13674
|
|
||||||
2026-03-20 10:32:03 | BLACKJACK user=401373976431165449 payout=+0 net=-100 bal=13574
|
|
||||||
2026-03-20 10:32:13 | BLACKJACK user=401373976431165449 payout=+250 net=+150 bal=13724
|
|
||||||
2026-03-20 10:38:41 | BEG user=401373976431165449 earned=+28 jailed=False bal=13752
|
|
||||||
2026-03-20 10:39:06 | BEG user=344531774518591498 earned=+28 jailed=False bal=1711
|
|
||||||
2026-03-20 10:39:57 | BLACKJACK user=401373976431165449 payout=+400 net=+200 bal=13952
|
|
||||||
2026-03-20 10:40:24 | BLACKJACK user=401373976431165449 payout=+200 net=+100 bal=14052
|
|
||||||
2026-03-20 10:40:41 | BLACKJACK user=401373976431165449 payout=+0 net=-100 bal=13952
|
|
||||||
2026-03-20 10:40:52 | BLACKJACK user=401373976431165449 payout=+0 net=-100 bal=13852
|
|
||||||
2026-03-20 10:41:06 | BLACKJACK user=401373976431165449 payout=+200 net=+100 bal=13952
|
|
||||||
2026-03-20 10:41:21 | BLACKJACK user=401373976431165449 payout=+0 net=-100 bal=13852
|
|
||||||
2026-03-20 10:41:36 | BLACKJACK user=401373976431165449 payout=+0 net=-100 bal=13752
|
|
||||||
2026-03-20 10:41:49 | BLACKJACK user=401373976431165449 payout=+200 net=+100 bal=13852
|
|
||||||
2026-03-20 10:41:50 | BEG user=401373976431165449 earned=+62 jailed=False bal=13914
|
|
||||||
2026-03-20 10:41:58 | SLOTS_MISS user=401373976431165449 bet=100 change=-100 bal=13814
|
|
||||||
2026-03-20 10:42:22 | SLOTS_MISS user=401373976431165449 bet=100 change=-100 bal=13714
|
|
||||||
2026-03-20 10:43:45 | BLACKJACK user=401373976431165449 payout=+400 net=+200 bal=13914
|
|
||||||
2026-03-20 10:44:21 | BEG user=344531774518591498 earned=+36 jailed=False bal=1747
|
|
||||||
2026-03-20 10:44:22 | WORK user=344531774518591498 earned=+84 lucky=False bal=1831
|
|
||||||
2026-03-20 10:44:34 | BLACKJACK user=344531774518591498 payout=+3662 net=+1831 bal=3662
|
|
||||||
2026-03-20 10:44:46 | BLACKJACK user=344531774518591498 payout=+0 net=-3662 bal=0
|
|
||||||
2026-03-20 10:45:43 | BEG user=401373976431165449 earned=+56 jailed=False bal=13970
|
|
||||||
2026-03-20 10:47:55 | BLACKJACK user=401373976431165449 payout=+200 net=+100 bal=14070
|
|
||||||
2026-03-20 10:48:06 | BLACKJACK user=401373976431165449 payout=+0 net=-100 bal=13970
|
|
||||||
2026-03-20 10:48:14 | BLACKJACK user=401373976431165449 payout=+250 net=+150 bal=14120
|
|
||||||
2026-03-20 10:48:25 | BLACKJACK user=401373976431165449 payout=+0 net=-100 bal=14020
|
|
||||||
2026-03-20 10:48:49 | BEG user=401373976431165449 earned=+22 jailed=False bal=14042
|
|
||||||
2026-03-20 10:56:47 | BEG user=272518654715887618 earned=+13 jailed=False bal=812
|
|
||||||
2026-03-20 10:56:49 | WORK user=272518654715887618 earned=+105 lucky=False bal=917
|
|
||||||
2026-03-20 10:56:59 | CRIME_WIN user=272518654715887618 earned=+237 bal=1154
|
|
||||||
2026-03-20 10:57:12 | BEG user=344531774518591498 earned=+36 jailed=False bal=36
|
|
||||||
2026-03-20 10:58:57 | WORK user=338622999127261185 earned=+117 lucky=True bal=616
|
|
||||||
2026-03-20 10:59:29 | BEG user=401373976431165449 earned=+58 jailed=False bal=14100
|
|
||||||
2026-03-20 11:02:04 | BLACKJACK user=401373976431165449 payout=+0 net=-1000 bal=13100
|
|
||||||
2026-03-20 11:02:27 | BLACKJACK user=401373976431165449 payout=+200 net=+100 bal=13200
|
|
||||||
2026-03-20 11:02:43 | BEG user=344531774518591498 earned=+36 jailed=False bal=72
|
|
||||||
2026-03-20 11:05:34 | BEG user=401373976431165449 earned=+52 jailed=False bal=13252
|
|
||||||
2026-03-20 11:12:14 | WORK user=401373976431165449 earned=+230 lucky=True bal=13482
|
|
||||||
2026-03-20 11:12:25 | BEG user=401373976431165449 earned=+62 jailed=False bal=13544
|
|
||||||
2026-03-20 11:16:03 | BEG user=401373976431165449 earned=+48 jailed=False bal=13592
|
|
||||||
2026-03-20 11:16:45 | BEG user=824516445382901800 earned=+72 jailed=False bal=1609
|
|
||||||
2026-03-20 11:16:49 | WORK user=824516445382901800 earned=+71 lucky=False bal=1680
|
|
||||||
2026-03-20 11:17:03 | ROULETTE_LOSE user=824516445382901800 bet=1680 colour=punane result=must mult=1 bal=0
|
|
||||||
2026-03-20 11:20:45 | GIVE from_=401373976431165449 to=824516445382901800 amount=1000 from_bal=12592 to_bal=1000
|
|
||||||
2026-03-20 11:20:50 | BEG user=344531774518591498 earned=+80 jailed=False bal=152
|
|
||||||
2026-03-20 11:21:33 | ROULETTE_LOSE user=824516445382901800 bet=1000 colour=punane result=must mult=1 bal=0
|
|
||||||
2026-03-20 11:22:53 | BLACKJACK user=344531774518591498 payout=+304 net=+152 bal=304
|
|
||||||
2026-03-20 11:23:06 | BLACKJACK user=344531774518591498 payout=+0 net=-304 bal=0
|
|
||||||
2026-03-20 11:23:18 | BEG user=401373976431165449 earned=+38 jailed=False bal=12630
|
|
||||||
2026-03-20 11:23:28 | CRIME_WIN user=401373976431165449 earned=+347 bal=12977
|
|
||||||
2026-03-20 11:23:42 | ROB_FAIL robber=401373976431165449 victim=218972931701735424 fine=-205 robber_bal=12772
|
|
||||||
2026-03-20 11:24:13 | BEG user=344531774518591498 earned=+70 jailed=False bal=70
|
|
||||||
2026-03-20 11:24:43 | WORK user=344531774518591498 earned=+118 lucky=False bal=188
|
|
||||||
2026-03-20 11:24:57 | BLACKJACK user=344531774518591498 payout=+0 net=-188 bal=0
|
|
||||||
2026-03-20 11:38:51 | BEG user=272518654715887618 earned=+40 jailed=False bal=1194
|
|
||||||
2026-03-20 11:38:58 | BEG user=344531774518591498 earned=+80 jailed=False bal=80
|
|
||||||
2026-03-20 11:42:01 | WORK user=338622999127261185 earned=+27 lucky=False bal=643
|
|
||||||
2026-03-20 11:42:38 | BEG user=272518654715887618 earned=+21 jailed=False bal=1215
|
|
||||||
2026-03-20 11:42:41 | BEG user=344531774518591498 earned=+24 jailed=False bal=104
|
|
||||||
2026-03-20 11:47:54 | BEG user=338622999127261185 earned=+44 jailed=False bal=687
|
|
||||||
2026-03-20 11:50:32 | BEG user=272518654715887618 earned=+23 jailed=False bal=1238
|
|
||||||
2026-03-20 11:54:20 | WORK user=401373976431165449 earned=+56 lucky=False bal=12828
|
|
||||||
2026-03-20 11:54:26 | BEG user=401373976431165449 earned=+40 jailed=False bal=12868
|
|
||||||
2026-03-20 12:01:29 | BEG user=401373976431165449 earned=+56 jailed=False bal=12924
|
|
||||||
2026-03-20 12:04:36 | BEG user=401373976431165449 earned=+50 jailed=False bal=12974
|
|
||||||
2026-03-20 12:07:25 | ROB_BLOCKED robber=344531774518591498 victim=401373976431165449 fine=-169 robber_bal=0 ac_uses_left=1
|
|
||||||
2026-03-20 12:07:28 | BEG user=344531774518591498 earned=+34 jailed=False bal=34
|
|
||||||
2026-03-20 12:07:31 | CRIME_WIN user=344531774518591498 earned=+399 bal=433
|
|
||||||
2026-03-20 12:07:33 | WORK user=344531774518591498 earned=+90 lucky=True bal=523
|
|
||||||
2026-03-20 12:08:33 | BEG user=401373976431165449 earned=+34 jailed=False bal=13008
|
|
||||||
2026-03-20 12:11:44 | BEG user=401373976431165449 earned=+28 jailed=False bal=13036
|
|
||||||
2026-03-20 12:16:46 | BEG user=401373976431165449 earned=+66 jailed=False bal=13102
|
|
||||||
2026-03-20 12:17:10 | WORK user=824516445382901800 earned=+84 lucky=False bal=84
|
|
||||||
2026-03-20 12:17:11 | BEG user=824516445382901800 earned=+32 jailed=False bal=116
|
|
||||||
2026-03-20 12:17:13 | CRIME_WIN user=824516445382901800 earned=+501 bal=617
|
|
||||||
2026-03-20 12:17:22 | ROB_BLOCKED robber=824516445382901800 victim=344531774518591498 fine=-170 robber_bal=447 ac_uses_left=1
|
|
||||||
2026-03-20 12:17:37 | BEG user=344531774518591498 earned=+26 jailed=False bal=549
|
|
||||||
2026-03-20 12:19:11 | WORK user=178852380018868224 earned=+41 lucky=False bal=40699
|
|
||||||
2026-03-20 12:19:15 | BEG user=178852380018868224 earned=+32 jailed=False bal=40731
|
|
||||||
2026-03-20 12:19:18 | CRIME_WIN user=178852380018868224 earned=+460 bal=41191
|
|
||||||
2026-03-20 12:19:37 | ROULETTE_LOSE user=824516445382901800 bet=447 colour=punane result=must mult=1 bal=0
|
|
||||||
2026-03-20 12:23:18 | SLOTS_PAIR user=178852380018868224 bet=10000 change=5000 bal=46191
|
|
||||||
2026-03-20 12:23:28 | SLOTS_MISS user=178852380018868224 bet=10000 change=-10000 bal=36191
|
|
||||||
2026-03-20 12:26:00 | BEG user=344531774518591498 earned=+40 jailed=False bal=589
|
|
||||||
2026-03-20 12:34:38 | BEG user=401373976431165449 earned=+74 jailed=False bal=13176
|
|
||||||
2026-03-20 12:34:41 | WORK user=401373976431165449 earned=+105 lucky=False bal=13281
|
|
||||||
2026-03-20 12:35:44 | HEIST_HOUSE change=-479646 house_bal=529905
|
|
||||||
2026-03-20 12:35:44 | HEIST_WIN user=824516445382901800 change=+59955 bal=59955
|
|
||||||
2026-03-20 12:35:44 | HEIST_WIN user=401373976431165449 change=+59955 bal=73236
|
|
||||||
2026-03-20 12:35:44 | HEIST_WIN user=178852380018868224 change=+59955 bal=96146
|
|
||||||
2026-03-20 12:35:44 | HEIST_WIN user=338622999127261185 change=+59955 bal=60642
|
|
||||||
2026-03-20 12:35:44 | HEIST_WIN user=340451525799182357 change=+59955 bal=59955
|
|
||||||
2026-03-20 12:35:44 | HEIST_WIN user=218972931701735424 change=+59955 bal=62771
|
|
||||||
2026-03-20 12:35:44 | HEIST_WIN user=344531774518591498 change=+59955 bal=60544
|
|
||||||
2026-03-20 12:35:44 | HEIST_WIN user=272518654715887618 change=+59955 bal=61193
|
|
||||||
2026-03-20 12:36:08 | ROULETTE_WIN user=178852380018868224 bet=96146 colour=punane result=punane mult=1 bal=192292
|
|
||||||
2026-03-20 12:36:25 | ROULETTE_WIN user=178852380018868224 bet=192292 colour=punane result=punane mult=1 bal=384584
|
|
||||||
2026-03-20 12:36:26 | ROULETTE_WIN user=824516445382901800 bet=59955 colour=punane result=punane mult=1 bal=119910
|
|
||||||
2026-03-20 12:36:33 | BUY user=344531774518591498 item=karikas cost=-6000 bal=54544
|
|
||||||
2026-03-20 12:36:37 | BUY user=344531774518591498 item=monitor_360 cost=-7500 bal=47044
|
|
||||||
2026-03-20 12:36:50 | BLACKJACK user=344531774518591498 payout=+0 net=-47044 bal=0
|
|
||||||
2026-03-20 12:37:18 | ROULETTE_WIN user=178852380018868224 bet=384584 colour=punane result=punane mult=1 bal=769168
|
|
||||||
2026-03-20 12:37:48 | ROULETTE_WIN user=178852380018868224 bet=769168 colour=punane result=punane mult=1 bal=1538336
|
|
||||||
2026-03-20 12:37:52 | ROULETTE_LOSE user=824516445382901800 bet=119910 colour=punane result=must mult=1 bal=0
|
|
||||||
2026-03-20 12:39:40 | BUY user=272518654715887618 item=gaming_laptop cost=-1500 bal=59693
|
|
||||||
2026-03-20 12:39:51 | ROB_BLOCKED robber=824516445382901800 victim=178852380018868224 fine=-167 robber_bal=0 ac_uses_left=1
|
|
||||||
2026-03-20 12:39:51 | BUY user=272518654715887618 item=cat6 cost=-3500 bal=56193
|
|
||||||
2026-03-20 12:39:55 | ROB_BLOCKED robber=824516445382901800 victim=178852380018868224 fine=-100 robber_bal=0 ac_uses_left=0
|
|
||||||
2026-03-20 12:40:01 | ROB_WIN robber=401373976431165449 victim=178852380018868224 stolen=+162541 jackpot=False robber_bal=235777 victim_bal=1375795
|
|
||||||
2026-03-20 12:40:02 | ROB_WIN robber=824516445382901800 victim=178852380018868224 stolen=+340819 jackpot=False robber_bal=340819 victim_bal=1034976
|
|
||||||
2026-03-20 12:40:03 | BUY user=178852380018868224 item=anticheat cost=-1000 bal=1033976
|
|
||||||
2026-03-20 12:40:07 | ROB_BLOCKED robber=824516445382901800 victim=178852380018868224 fine=-109 robber_bal=340710 ac_uses_left=1
|
|
||||||
2026-03-20 12:40:09 | ROB_BLOCKED robber=824516445382901800 victim=178852380018868224 fine=-128 robber_bal=340582 ac_uses_left=0
|
|
||||||
2026-03-20 12:40:11 | ROB_FAIL robber=824516445382901800 victim=178852380018868224 fine=-188 robber_bal=340394
|
|
||||||
2026-03-20 12:40:12 | ROB_FAIL robber=824516445382901800 victim=178852380018868224 fine=-232 robber_bal=340162
|
|
||||||
2026-03-20 12:40:12 | BUY user=178852380018868224 item=anticheat cost=-1000 bal=1032976
|
|
||||||
2026-03-20 12:40:15 | ROB_BLOCKED robber=824516445382901800 victim=178852380018868224 fine=-103 robber_bal=340059 ac_uses_left=1
|
|
||||||
2026-03-20 12:40:17 | BUY user=272518654715887618 item=lan_pass cost=-1200 bal=54993
|
|
||||||
2026-03-20 12:40:18 | ROB_BLOCKED robber=824516445382901800 victim=178852380018868224 fine=-199 robber_bal=339860 ac_uses_left=0
|
|
||||||
2026-03-20 12:40:19 | BUY user=178852380018868224 item=anticheat cost=-1000 bal=1031976
|
|
||||||
2026-03-20 12:40:20 | ROB_BLOCKED robber=824516445382901800 victim=178852380018868224 fine=-100 robber_bal=339760 ac_uses_left=1
|
|
||||||
2026-03-20 12:40:23 | ROULETTE_LOSE user=178852380018868224 bet=1031976 colour=punane result=must mult=1 bal=0
|
|
||||||
2026-03-20 12:40:27 | BUY user=272518654715887618 item=anticheat cost=-1000 bal=53993
|
|
||||||
2026-03-20 12:40:34 | BUY user=272518654715887618 item=reguleeritav_laud cost=-3500 bal=50493
|
|
||||||
2026-03-20 12:40:42 | BUY user=272518654715887618 item=jellyfin cost=-4000 bal=46493
|
|
||||||
2026-03-20 12:40:55 | BUY user=272518654715887618 item=monitor cost=-2500 bal=43993
|
|
||||||
2026-03-20 12:41:00 | ROB_BLOCKED robber=824516445382901800 victim=401373976431165449 fine=-139 robber_bal=339621 ac_uses_left=0
|
|
||||||
2026-03-20 12:41:02 | ROB_FAIL robber=824516445382901800 victim=401373976431165449 fine=-131 robber_bal=339490
|
|
||||||
2026-03-20 12:41:02 | WORK user=272518654715887618 earned=+97 lucky=False bal=44090
|
|
||||||
2026-03-20 12:41:04 | ROB_WIN robber=824516445382901800 victim=401373976431165449 stolen=+51444 jackpot=False robber_bal=390934 victim_bal=184333
|
|
||||||
2026-03-20 12:41:05 | ROB_BLOCKED robber=178852380018868224 victim=824516445382901800 fine=-179 robber_bal=0 ac_uses_left=0
|
|
||||||
2026-03-20 12:41:05 | ROB_WIN robber=824516445382901800 victim=401373976431165449 stolen=+41097 jackpot=False robber_bal=432031 victim_bal=143236
|
|
||||||
2026-03-20 12:41:05 | ROB_WIN robber=401373976431165449 victim=824516445382901800 stolen=+67489 jackpot=False robber_bal=210725 victim_bal=364542
|
|
||||||
2026-03-20 12:41:07 | ROB_WIN robber=178852380018868224 victim=824516445382901800 stolen=+76500 jackpot=False robber_bal=76500 victim_bal=288042
|
|
||||||
2026-03-20 12:41:08 | ROB_FAIL robber=178852380018868224 victim=824516445382901800 fine=-136 robber_bal=76364
|
|
||||||
2026-03-20 12:41:08 | ROB_WIN robber=824516445382901800 victim=401373976431165449 stolen=+50641 jackpot=False robber_bal=338683 victim_bal=160084
|
|
||||||
2026-03-20 12:41:10 | BUY user=401373976431165449 item=anticheat cost=-1000 bal=159084
|
|
||||||
2026-03-20 12:41:11 | ROB_WIN robber=178852380018868224 victim=824516445382901800 stolen=+41788 jackpot=False robber_bal=118152 victim_bal=296895
|
|
||||||
2026-03-20 12:41:11 | ROB_BLOCKED robber=824516445382901800 victim=401373976431165449 fine=-103 robber_bal=296792 ac_uses_left=1
|
|
||||||
2026-03-20 12:41:13 | ROULETTE_WIN user=178852380018868224 bet=118152 colour=punane result=punane mult=1 bal=236304
|
|
||||||
2026-03-20 12:41:13 | ROB_BLOCKED robber=824516445382901800 victim=401373976431165449 fine=-125 robber_bal=296667 ac_uses_left=0
|
|
||||||
2026-03-20 12:41:24 | BUY user=401373976431165449 item=anticheat cost=-1000 bal=158084
|
|
||||||
2026-03-20 12:41:24 | BEG user=272518654715887618 earned=+38 jailed=False bal=44128
|
|
||||||
2026-03-20 12:41:35 | BEG user=401373976431165449 earned=+74 jailed=False bal=158158
|
|
||||||
2026-03-20 12:41:54 | ROULETTE_WIN user=178852380018868224 bet=236304 colour=punane result=punane mult=1 bal=472608
|
|
||||||
2026-03-20 12:41:59 | BLACKJACK user=272518654715887618 payout=+40000 net=+20000 bal=64128
|
|
||||||
2026-03-20 12:42:01 | SLOTS_PAIR user=824516445382901800 bet=296667 change=148333 bal=445000
|
|
||||||
2026-03-20 12:42:43 | ROULETTE_LOSE user=178852380018868224 bet=472608 colour=punane result=must mult=1 bal=0
|
|
||||||
2026-03-20 12:43:11 | SLOTS_PAIR user=338622999127261185 bet=60642 change=30321 bal=90963
|
|
||||||
2026-03-20 12:43:21 | BUY user=824516445382901800 item=anticheat cost=-1000 bal=444000
|
|
||||||
2026-03-20 12:43:28 | ROULETTE_WIN user=338622999127261185 bet=90963 colour=must result=must mult=1 bal=181926
|
|
||||||
2026-03-20 12:43:43 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=449000
|
|
||||||
2026-03-20 12:43:49 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=439000
|
|
||||||
2026-03-20 12:43:53 | WORK user=338622999127261185 earned=+61 lucky=False bal=181987
|
|
||||||
2026-03-20 12:43:54 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=444000
|
|
||||||
2026-03-20 12:43:55 | CRIME_FAIL user=338622999127261185 fine=-111 jailed=True bal=181876
|
|
||||||
2026-03-20 12:43:59 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=434000
|
|
||||||
2026-03-20 12:44:05 | SLOTS_TRIPLE user=824516445382901800 bet=10000 change=30000 bal=464000
|
|
||||||
2026-03-20 12:44:10 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=469000
|
|
||||||
2026-03-20 12:44:17 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=459000
|
|
||||||
2026-03-20 12:44:17 | BAIL_PAID user=338622999127261185 fine=-46087 pct=25% bal=135789
|
|
||||||
2026-03-20 12:44:22 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=449000
|
|
||||||
2026-03-20 12:44:25 | SLOTS_MISS user=338622999127261185 bet=135789 change=-135789 bal=0
|
|
||||||
2026-03-20 12:44:27 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=454000
|
|
||||||
2026-03-20 12:44:33 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=444000
|
|
||||||
2026-03-20 12:44:49 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=434000
|
|
||||||
2026-03-20 12:44:55 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=424000
|
|
||||||
2026-03-20 12:44:56 | BEG user=401373976431165449 earned=+20 jailed=False bal=158178
|
|
||||||
2026-03-20 12:45:02 | SLOTS_TRIPLE user=824516445382901800 bet=10000 change=30000 bal=454000
|
|
||||||
2026-03-20 12:45:08 | SLOTS_TRIPLE user=824516445382901800 bet=10000 change=60000 bal=514000
|
|
||||||
2026-03-20 12:45:13 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=519000
|
|
||||||
2026-03-20 12:45:20 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=509000
|
|
||||||
2026-03-20 12:45:25 | SLOTS_TRIPLE user=824516445382901800 bet=10000 change=30000 bal=539000
|
|
||||||
2026-03-20 12:45:30 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=544000
|
|
||||||
2026-03-20 12:45:35 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=534000
|
|
||||||
2026-03-20 12:45:42 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=524000
|
|
||||||
2026-03-20 12:45:47 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=514000
|
|
||||||
2026-03-20 12:45:57 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=504000
|
|
||||||
2026-03-20 12:46:02 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=494000
|
|
||||||
2026-03-20 12:46:07 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=499000
|
|
||||||
2026-03-20 12:46:12 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=504000
|
|
||||||
2026-03-20 12:46:18 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=509000
|
|
||||||
2026-03-20 12:46:22 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=499000
|
|
||||||
2026-03-20 12:46:28 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=489000
|
|
||||||
2026-03-20 12:46:33 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=494000
|
|
||||||
2026-03-20 12:46:40 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=484000
|
|
||||||
2026-03-20 12:46:45 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=489000
|
|
||||||
2026-03-20 12:46:50 | SLOTS_TRIPLE user=824516445382901800 bet=10000 change=30000 bal=519000
|
|
||||||
2026-03-20 12:46:56 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=509000
|
|
||||||
2026-03-20 12:47:02 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=514000
|
|
||||||
2026-03-20 12:47:07 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=519000
|
|
||||||
2026-03-20 12:47:13 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=509000
|
|
||||||
2026-03-20 12:47:21 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=514000
|
|
||||||
2026-03-20 12:47:27 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=504000
|
|
||||||
2026-03-20 12:47:33 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=494000
|
|
||||||
2026-03-20 12:47:38 | SLOTS_TRIPLE user=824516445382901800 bet=10000 change=90000 bal=584000
|
|
||||||
2026-03-20 12:47:43 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=589000
|
|
||||||
2026-03-20 12:47:48 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=594000
|
|
||||||
2026-03-20 12:47:55 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=584000
|
|
||||||
2026-03-20 12:48:00 | SLOTS_TRIPLE user=824516445382901800 bet=10000 change=40000 bal=624000
|
|
||||||
2026-03-20 12:48:06 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=614000
|
|
||||||
2026-03-20 12:48:12 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=619000
|
|
||||||
2026-03-20 12:48:18 | SLOTS_TRIPLE user=824516445382901800 bet=10000 change=40000 bal=659000
|
|
||||||
2026-03-20 12:48:20 | BEG user=401373976431165449 earned=+68 jailed=False bal=158246
|
|
||||||
2026-03-20 12:48:25 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=664000
|
|
||||||
2026-03-20 12:48:30 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=669000
|
|
||||||
2026-03-20 12:48:36 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=674000
|
|
||||||
2026-03-20 12:48:42 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=679000
|
|
||||||
2026-03-20 12:48:48 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=669000
|
|
||||||
2026-03-20 12:49:37 | ADMIN_JAIL admin=272518654715887618 target=401373976431165449 minutes=100 reason=Omavoliliselt
|
|
||||||
2026-03-20 12:51:31 | ADMIN_UNJAIL admin=272518654715887618 target=401373976431165449
|
|
||||||
2026-03-20 13:01:40 | BEG user=272518654715887618 earned=+10 jailed=False bal=64138
|
|
||||||
2026-03-20 13:02:01 | BLACKJACK user=272518654715887618 payout=+40000 net=+20000 bal=84138
|
|
||||||
2026-03-20 13:03:15 | BEG user=344531774518591498 earned=+78 jailed=False bal=78
|
|
||||||
2026-03-20 13:03:16 | WORK user=344531774518591498 earned=+120 lucky=False bal=198
|
|
||||||
2026-03-20 13:14:55 | BEG user=401373976431165449 earned=+70 jailed=False bal=158316
|
|
||||||
2026-03-20 13:14:58 | WORK user=401373976431165449 earned=+56 lucky=False bal=158372
|
|
||||||
2026-03-20 13:24:03 | BEG user=272518654715887618 earned=+40 jailed=False bal=84178
|
|
||||||
2026-03-20 13:24:12 | WORK user=272518654715887618 earned=+121 lucky=False bal=84299
|
|
||||||
2026-03-20 13:24:24 | BUY user=272518654715887618 item=energiajook cost=-800 bal=83499
|
|
||||||
2026-03-20 13:24:46 | BUY user=272518654715887618 item=mikrofon cost=-2800 bal=80699
|
|
||||||
2026-03-20 13:24:56 | BUY user=272518654715887618 item=klaviatuur cost=-1800 bal=78899
|
|
||||||
2026-03-20 13:25:12 | CRIME_FAIL user=272518654715887618 fine=-101 jailed=True bal=78798
|
|
||||||
2026-03-20 13:30:43 | BEG user=338622999127261185 earned=+70 jailed=False bal=70
|
|
||||||
2026-03-20 13:30:45 | WORK user=338622999127261185 earned=+109 lucky=False bal=179
|
|
||||||
2026-03-20 13:42:46 | CRIME_WIN user=401373976431165449 earned=+453 bal=158825
|
|
||||||
2026-03-20 13:44:12 | WORK user=344531774518591498 earned=+33 lucky=False bal=231
|
|
||||||
2026-03-20 13:44:14 | BEG user=344531774518591498 earned=+26 jailed=False bal=257
|
|
||||||
2026-03-20 14:45:12 | ROB_WIN robber=401373976431165449 victim=218972931701735424 stolen=+14535 jackpot=False robber_bal=173360 victim_bal=48236
|
|
||||||
2026-03-20 14:45:19 | WORK user=344531774518591498 earned=+88 lucky=False bal=345
|
|
||||||
2026-03-20 14:46:36 | ROB_BLOCKED robber=178852380018868224 victim=824516445382901800 fine=-142 robber_bal=0 ac_uses_left=1
|
|
||||||
2026-03-20 14:52:26 | WORK user=272518654715887618 earned=+88 lucky=False bal=78886
|
|
||||||
2026-03-20 14:52:30 | BEG user=272518654715887618 earned=+52 jailed=False bal=78938
|
|
||||||
2026-03-20 14:57:12 | BEG user=272518654715887618 earned=+80 jailed=False bal=79018
|
|
||||||
2026-03-20 15:17:52 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=679000
|
|
||||||
2026-03-20 15:17:58 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=659000
|
|
||||||
2026-03-20 15:18:05 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=639000
|
|
||||||
2026-03-20 15:18:10 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=649000
|
|
||||||
2026-03-20 15:18:15 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=659000
|
|
||||||
2026-03-20 15:18:21 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=639000
|
|
||||||
2026-03-20 15:18:27 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=649000
|
|
||||||
2026-03-20 15:18:33 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=659000
|
|
||||||
2026-03-20 15:18:38 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=669000
|
|
||||||
2026-03-20 15:18:45 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=649000
|
|
||||||
2026-03-20 15:18:50 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=659000
|
|
||||||
2026-03-20 15:18:55 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=669000
|
|
||||||
2026-03-20 15:19:03 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=649000
|
|
||||||
2026-03-20 15:19:08 | SLOTS_TRIPLE user=824516445382901800 bet=20000 change=60000 bal=709000
|
|
||||||
2026-03-20 15:19:13 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=689000
|
|
||||||
2026-03-20 15:19:18 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=669000
|
|
||||||
2026-03-20 15:19:23 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=679000
|
|
||||||
2026-03-20 15:19:30 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=689000
|
|
||||||
2026-03-20 15:19:47 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=669000
|
|
||||||
2026-03-20 15:19:53 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=649000
|
|
||||||
2026-03-20 15:19:58 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=629000
|
|
||||||
2026-03-20 15:20:03 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=639000
|
|
||||||
2026-03-20 15:20:08 | SLOTS_TRIPLE user=824516445382901800 bet=20000 change=80000 bal=719000
|
|
||||||
2026-03-20 15:20:13 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=699000
|
|
||||||
2026-03-20 15:20:20 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=679000
|
|
||||||
2026-03-20 15:20:25 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=659000
|
|
||||||
2026-03-20 15:20:30 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=669000
|
|
||||||
2026-03-20 15:20:35 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=649000
|
|
||||||
2026-03-20 15:20:40 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=659000
|
|
||||||
2026-03-20 15:20:47 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=669000
|
|
||||||
2026-03-20 15:20:53 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=679000
|
|
||||||
2026-03-20 15:20:58 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=689000
|
|
||||||
2026-03-20 15:21:03 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=669000
|
|
||||||
2026-03-20 15:21:10 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=649000
|
|
||||||
2026-03-20 15:21:16 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=659000
|
|
||||||
2026-03-20 15:21:23 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=669000
|
|
||||||
2026-03-20 15:21:32 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=679000
|
|
||||||
2026-03-20 15:21:37 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=689000
|
|
||||||
2026-03-20 15:21:43 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=669000
|
|
||||||
2026-03-20 15:21:48 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=649000
|
|
||||||
2026-03-20 15:21:53 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=659000
|
|
||||||
2026-03-20 15:22:00 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=669000
|
|
||||||
2026-03-20 15:22:05 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=649000
|
|
||||||
2026-03-20 15:22:10 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=629000
|
|
||||||
2026-03-20 15:22:15 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=609000
|
|
||||||
2026-03-20 15:22:20 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=589000
|
|
||||||
2026-03-20 15:22:25 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=569000
|
|
||||||
2026-03-20 15:22:31 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=549000
|
|
||||||
2026-03-20 15:22:36 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=559000
|
|
||||||
2026-03-20 15:22:41 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=539000
|
|
||||||
2026-03-20 15:23:15 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=529000
|
|
||||||
2026-03-20 15:23:20 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=519000
|
|
||||||
2026-03-20 15:23:26 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=524000
|
|
||||||
2026-03-20 15:23:31 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=514000
|
|
||||||
2026-03-20 15:23:36 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=519000
|
|
||||||
2026-03-20 15:23:41 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=509000
|
|
||||||
2026-03-20 15:23:48 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=514000
|
|
||||||
2026-03-20 15:23:53 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=519000
|
|
||||||
2026-03-20 15:23:58 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=524000
|
|
||||||
2026-03-20 15:24:03 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=514000
|
|
||||||
2026-03-20 15:24:10 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=504000
|
|
||||||
2026-03-20 15:24:18 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=494000
|
|
||||||
2026-03-20 15:24:23 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=484000
|
|
||||||
2026-03-20 15:24:30 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=489000
|
|
||||||
2026-03-20 15:24:35 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=479000
|
|
||||||
2026-03-20 15:24:40 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=469000
|
|
||||||
2026-03-20 15:24:46 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=459000
|
|
||||||
2026-03-20 15:24:51 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=464000
|
|
||||||
2026-03-20 15:24:56 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=454000
|
|
||||||
2026-03-20 15:25:03 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=444000
|
|
||||||
2026-03-20 15:25:09 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=434000
|
|
||||||
2026-03-20 15:25:16 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=424000
|
|
||||||
2026-03-20 15:25:21 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=414000
|
|
||||||
2026-03-20 15:25:28 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=419000
|
|
||||||
2026-03-20 15:25:35 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=409000
|
|
||||||
2026-03-20 15:25:40 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=414000
|
|
||||||
2026-03-20 15:25:46 | SLOTS_JACKPOT user=824516445382901800 bet=10000 change=240000 bal=654000
|
|
||||||
2026-03-20 15:25:53 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=659000
|
|
||||||
2026-03-20 15:25:58 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=664000
|
|
||||||
2026-03-20 15:26:03 | SLOTS_TRIPLE user=824516445382901800 bet=10000 change=140000 bal=804000
|
|
||||||
2026-03-20 15:28:09 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=809000
|
|
||||||
2026-03-20 15:28:14 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=799000
|
|
||||||
2026-03-20 15:28:19 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=804000
|
|
||||||
2026-03-20 15:28:25 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=794000
|
|
||||||
2026-03-20 15:28:32 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=799000
|
|
||||||
2026-03-20 15:29:08 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=804000
|
|
||||||
2026-03-20 15:29:14 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=794000
|
|
||||||
2026-03-20 15:29:21 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=799000
|
|
||||||
2026-03-20 15:29:26 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=789000
|
|
||||||
2026-03-20 15:29:31 | SLOTS_TRIPLE user=824516445382901800 bet=10000 change=40000 bal=829000
|
|
||||||
2026-03-20 15:29:36 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=819000
|
|
||||||
2026-03-20 15:29:41 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=824000
|
|
||||||
2026-03-20 15:29:47 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=829000
|
|
||||||
2026-03-20 15:29:56 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=834000
|
|
||||||
2026-03-20 15:30:02 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=839000
|
|
||||||
2026-03-20 15:30:07 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=844000
|
|
||||||
2026-03-20 15:30:15 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=834000
|
|
||||||
2026-03-20 15:30:32 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=839000
|
|
||||||
2026-03-20 15:30:38 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=829000
|
|
||||||
2026-03-20 15:30:43 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=834000
|
|
||||||
2026-03-20 15:31:35 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=839000
|
|
||||||
2026-03-20 15:31:40 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=844000
|
|
||||||
2026-03-20 15:31:46 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=849000
|
|
||||||
2026-03-20 15:31:53 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=839000
|
|
||||||
2026-03-20 15:31:58 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=844000
|
|
||||||
2026-03-20 15:32:03 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=834000
|
|
||||||
2026-03-20 15:32:09 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=824000
|
|
||||||
2026-03-20 15:32:14 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=814000
|
|
||||||
2026-03-20 15:32:20 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=804000
|
|
||||||
2026-03-20 15:32:26 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=794000
|
|
||||||
2026-03-20 15:32:31 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=799000
|
|
||||||
2026-03-20 15:32:36 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=804000
|
|
||||||
2026-03-20 15:32:41 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=794000
|
|
||||||
2026-03-20 15:32:48 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=784000
|
|
||||||
2026-03-20 15:32:53 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=774000
|
|
||||||
2026-03-20 15:33:09 | BUY user=824516445382901800 item=karikas cost=-6000 bal=768000
|
|
||||||
2026-03-20 15:33:13 | BUY user=824516445382901800 item=monitor_360 cost=-7500 bal=760500
|
|
||||||
2026-03-20 15:33:16 | BUY user=824516445382901800 item=gaming_tool cost=-9000 bal=751500
|
|
||||||
2026-03-20 15:35:01 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=761500
|
|
||||||
2026-03-20 15:35:08 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=771500
|
|
||||||
2026-03-20 15:35:14 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=781500
|
|
||||||
2026-03-20 15:35:20 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=761500
|
|
||||||
2026-03-20 15:35:25 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=771500
|
|
||||||
2026-03-20 15:35:30 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=751500
|
|
||||||
2026-03-20 15:35:35 | SLOTS_PAIR user=824516445382901800 bet=20000 change=10000 bal=761500
|
|
||||||
2026-03-20 15:35:42 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=741500
|
|
||||||
2026-03-20 15:35:47 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=721500
|
|
||||||
2026-03-20 15:35:52 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=701500
|
|
||||||
2026-03-20 15:35:57 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=681500
|
|
||||||
2026-03-20 15:36:02 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=661500
|
|
||||||
2026-03-20 15:36:07 | SLOTS_MISS user=824516445382901800 bet=20000 change=-20000 bal=641500
|
|
||||||
2026-03-20 15:36:12 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=646500
|
|
||||||
2026-03-20 15:36:17 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=651500
|
|
||||||
2026-03-20 15:36:22 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=641500
|
|
||||||
2026-03-20 15:36:28 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=631500
|
|
||||||
2026-03-20 15:36:33 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=621500
|
|
||||||
2026-03-20 15:36:40 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=626500
|
|
||||||
2026-03-20 15:36:45 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=616500
|
|
||||||
2026-03-20 15:36:53 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=621500
|
|
||||||
2026-03-20 15:36:58 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=611500
|
|
||||||
2026-03-20 15:37:03 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=601500
|
|
||||||
2026-03-20 15:37:09 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=606500
|
|
||||||
2026-03-20 15:37:14 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=611500
|
|
||||||
2026-03-20 15:37:20 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=601500
|
|
||||||
2026-03-20 15:37:27 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=606500
|
|
||||||
2026-03-20 15:37:32 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=611500
|
|
||||||
2026-03-20 15:37:40 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=601500
|
|
||||||
2026-03-20 15:37:47 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=591500
|
|
||||||
2026-03-20 15:37:52 | SLOTS_TRIPLE user=824516445382901800 bet=10000 change=60000 bal=651500
|
|
||||||
2026-03-20 15:37:57 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=641500
|
|
||||||
2026-03-20 15:38:06 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=631500
|
|
||||||
2026-03-20 15:38:12 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=636500
|
|
||||||
2026-03-20 15:38:18 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=626500
|
|
||||||
2026-03-20 15:38:24 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=616500
|
|
||||||
2026-03-20 15:38:29 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=621500
|
|
||||||
2026-03-20 15:38:35 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=611500
|
|
||||||
2026-03-20 15:38:40 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=616500
|
|
||||||
2026-03-20 15:42:05 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=606500
|
|
||||||
2026-03-20 15:42:12 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=596500
|
|
||||||
2026-03-20 15:42:18 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=586500
|
|
||||||
2026-03-20 15:42:24 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=576500
|
|
||||||
2026-03-20 15:42:29 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=566500
|
|
||||||
2026-03-20 15:42:36 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=556500
|
|
||||||
2026-03-20 15:42:41 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=546500
|
|
||||||
2026-03-20 15:42:46 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=551500
|
|
||||||
2026-03-20 15:42:51 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=556500
|
|
||||||
2026-03-20 15:42:57 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=561500
|
|
||||||
2026-03-20 15:46:36 | CRIME_WIN user=401373976431165449 earned=+304 bal=173664
|
|
||||||
2026-03-20 15:46:48 | WORK user=401373976431165449 earned=+410 lucky=True bal=174074
|
|
||||||
2026-03-20 15:46:50 | BEG user=401373976431165449 earned=+52 jailed=False bal=174126
|
|
||||||
2026-03-20 15:48:44 | WORK user=272518654715887618 earned=+93 lucky=False bal=79111
|
|
||||||
2026-03-20 15:49:06 | BLACKJACK user=272518654715887618 payout=+80000 net=+40000 bal=119111
|
|
||||||
2026-03-20 16:24:32 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=551500
|
|
||||||
2026-03-20 16:24:38 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=541500
|
|
||||||
2026-03-20 16:24:42 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=546500
|
|
||||||
2026-03-20 16:24:47 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=536500
|
|
||||||
2026-03-20 16:24:55 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=541500
|
|
||||||
2026-03-20 16:25:02 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=546500
|
|
||||||
2026-03-20 16:25:08 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=551500
|
|
||||||
2026-03-20 16:25:13 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=541500
|
|
||||||
2026-03-20 16:25:20 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=531500
|
|
||||||
2026-03-20 16:25:28 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=521500
|
|
||||||
2026-03-20 16:25:35 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=526500
|
|
||||||
2026-03-20 16:25:41 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=531500
|
|
||||||
2026-03-20 16:25:47 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=536500
|
|
||||||
2026-03-20 16:25:53 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=541500
|
|
||||||
2026-03-20 16:26:00 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=546500
|
|
||||||
2026-03-20 16:26:05 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=536500
|
|
||||||
2026-03-20 16:26:10 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=541500
|
|
||||||
2026-03-20 16:28:22 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=546500
|
|
||||||
2026-03-20 16:28:28 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=536500
|
|
||||||
2026-03-20 16:28:35 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=541500
|
|
||||||
2026-03-20 16:28:40 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=531500
|
|
||||||
2026-03-20 16:28:45 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=536500
|
|
||||||
2026-03-20 16:28:50 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=541500
|
|
||||||
2026-03-20 16:28:57 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=531500
|
|
||||||
2026-03-20 16:29:05 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=521500
|
|
||||||
2026-03-20 16:29:10 | SLOTS_MISS user=824516445382901800 bet=10000 change=-10000 bal=511500
|
|
||||||
2026-03-20 16:29:15 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=516500
|
|
||||||
2026-03-20 16:29:21 | SLOTS_PAIR user=824516445382901800 bet=10000 change=5000 bal=521500
|
|
||||||
2026-03-20 16:30:49 | WORK user=824516445382901800 earned=+140 lucky=False bal=521640
|
|
||||||
2026-03-20 16:30:50 | BEG user=824516445382901800 earned=+60 jailed=False bal=521700
|
|
||||||
2026-03-20 16:30:53 | CRIME_WIN user=824516445382901800 earned=+260 bal=521960
|
|
||||||
2026-03-20 16:31:06 | ROB_BLOCKED robber=824516445382901800 victim=272518654715887618 fine=-174 robber_bal=521786 ac_uses_left=1
|
|
||||||
2026-03-20 16:34:38 | BEG user=367347301322326016 earned=+15 jailed=False bal=217
|
|
||||||
2026-03-20 16:34:40 | WORK user=367347301322326016 earned=+43 lucky=False bal=260
|
|
||||||
2026-03-20 16:34:43 | CRIME_WIN user=367347301322326016 earned=+392 bal=652
|
|
||||||
2026-03-20 16:46:10 | BEG user=401373976431165449 earned=+62 jailed=False bal=174188
|
|
||||||
2026-03-20 16:46:12 | WORK user=401373976431165449 earned=+95 lucky=False bal=174283
|
|
||||||
2026-03-20 16:46:25 | ROB_BLOCKED robber=401373976431165449 victim=824516445382901800 fine=-197 robber_bal=174086 ac_uses_left=0
|
|
||||||
2026-03-20 16:46:35 | BUY user=824516445382901800 item=anticheat cost=-1000 bal=520786
|
|
||||||
2026-03-20 16:47:16 | GIVE from_=824516445382901800 to=450392724169031680 amount=520786 from_bal=0 to_bal=520786
|
|
||||||
2026-03-20 16:48:04 | HEIST_FAIL user=401373976431165449 fine=-1000 jailed_until=2026-03-20T16:18:04.449345+00:00 bal=173086
|
|
||||||
2026-03-20 16:48:04 | HEIST_FAIL user=824516445382901800 fine=-150 jailed_until=2026-03-20T16:18:04.449345+00:00 bal=0
|
|
||||||
2026-03-20 16:49:17 | BEG user=401373976431165449 earned=+26 jailed=True bal=173112
|
|
||||||
2026-03-20 16:49:59 | GIVE from_=450392724169031680 to=824516445382901800 amount=520786 from_bal=0 to_bal=520786
|
|
||||||
2026-03-20 16:52:08 | GIVE from_=824516445382901800 to=450392724169031680 amount=519786 from_bal=1000 to_bal=519786
|
|
||||||
2026-03-20 16:52:19 | BAIL_PAID user=824516445382901800 fine=-350 pct=24% bal=650
|
|
||||||
2026-03-20 16:54:04 | GIVE from_=450392724169031680 to=824516445382901800 amount=519786 from_bal=0 to_bal=520436
|
|
||||||
2026-03-20 16:56:31 | ROB_BLOCKED robber=178852380018868224 victim=824516445382901800 fine=-188 robber_bal=0 ac_uses_left=1
|
|
||||||
2026-03-20 16:57:43 | GIVE from_=824516445382901800 to=401373976431165449 amount=20000 from_bal=500436 to_bal=193112
|
|
||||||
2026-03-20 16:57:47 | GIVE from_=824516445382901800 to=272518654715887618 amount=20000 from_bal=480436 to_bal=139111
|
|
||||||
2026-03-20 16:57:54 | GIVE from_=824516445382901800 to=340451525799182357 amount=20000 from_bal=460436 to_bal=79955
|
|
||||||
2026-03-20 16:58:02 | GIVE from_=824516445382901800 to=218972931701735424 amount=20000 from_bal=440436 to_bal=68236
|
|
||||||
2026-03-20 16:58:18 | GIVE from_=824516445382901800 to=323906492073771019 amount=20000 from_bal=420436 to_bal=62761
|
|
||||||
2026-03-20 16:58:22 | GIVE from_=824516445382901800 to=367347301322326016 amount=20000 from_bal=400436 to_bal=20652
|
|
||||||
2026-03-20 16:58:31 | GIVE from_=824516445382901800 to=344531774518591498 amount=20000 from_bal=380436 to_bal=20345
|
|
||||||
2026-03-20 16:58:35 | GIVE from_=824516445382901800 to=209554152584380420 amount=20000 from_bal=360436 to_bal=20309
|
|
||||||
2026-03-20 16:58:42 | GIVE from_=824516445382901800 to=338622999127261185 amount=20000 from_bal=340436 to_bal=20179
|
|
||||||
2026-03-20 16:58:46 | ROULETTE_LOSE user=344531774518591498 bet=20345 colour=punane result=must mult=1 bal=0
|
|
||||||
2026-03-20 16:59:01 | GIVE from_=824516445382901800 to=344531774518591498 amount=20000 from_bal=320436 to_bal=20000
|
|
||||||
2026-03-20 16:59:05 | GIVE from_=824516445382901800 to=240454469668569088 amount=20000 from_bal=300436 to_bal=20044
|
|
||||||
2026-03-20 16:59:09 | GIVE from_=824516445382901800 to=311132892795371520 amount=20000 from_bal=280436 to_bal=20000
|
|
||||||
2026-03-20 16:59:12 | GIVE from_=824516445382901800 to=178852380018868224 amount=20000 from_bal=260436 to_bal=20000
|
|
||||||
2026-03-20 16:59:24 | GIVE from_=824516445382901800 to=296322817941569537 amount=20000 from_bal=240436 to_bal=20000
|
|
||||||
2026-03-20 16:59:29 | GIVE from_=824516445382901800 to=485760228508565504 amount=20000 from_bal=220436 to_bal=20000
|
|
||||||
2026-03-20 16:59:34 | GIVE from_=824516445382901800 to=450392724169031680 amount=20000 from_bal=200436 to_bal=20000
|
|
||||||
2026-03-20 17:00:10 | ROULETTE_LOSE user=344531774518591498 bet=10000 colour=punane result=must mult=1 bal=10000
|
|
||||||
2026-03-20 17:00:34 | ROULETTE_WIN user=344531774518591498 bet=10000 colour=punane result=punane mult=1 bal=20000
|
|
||||||
2026-03-20 17:00:58 | ROULETTE_LOSE user=344531774518591498 bet=10000 colour=punane result=must mult=1 bal=10000
|
|
||||||
2026-03-20 17:01:14 | ROULETTE_LOSE user=344531774518591498 bet=10000 colour=punane result=must mult=1 bal=0
|
|
||||||
2026-03-20 17:06:54 | BEG user=272518654715887618 earned=+20 jailed=False bal=139131
|
|
||||||
2026-03-20 17:06:56 | WORK user=272518654715887618 earned=+393 lucky=True bal=139524
|
|
||||||
2026-03-20 17:07:23 | BLACKJACK user=272518654715887618 payout=+0 net=-50000 bal=89524
|
|
||||||
2026-03-20 17:10:34 | BEG user=272518654715887618 earned=+30 jailed=False bal=89554
|
|
||||||
2026-03-20 17:10:40 | CRIME_FAIL user=272518654715887618 fine=-90 jailed=True bal=89464
|
|
||||||
2026-03-20 17:10:53 | JAIL_FREE user=272518654715887618 method=doubles
|
|
||||||
2026-03-20 17:16:58 | BEG user=272518654715887618 earned=+78 jailed=False bal=89542
|
|
||||||
2026-03-20 17:17:49 | ROULETTE_LOSE user=824516445382901800 bet=200436 colour=punane result=must mult=1 bal=0
|
|
||||||
2026-03-20 17:18:09 | BLACKJACK user=272518654715887618 payout=+179084 net=+89542 bal=179084
|
|
||||||
2026-03-20 17:18:37 | BLACKJACK user=272518654715887618 payout=+0 net=-179084 bal=0
|
|
||||||
2026-03-20 17:21:54 | BEG user=344531774518591498 earned=+14 jailed=False bal=14
|
|
||||||
2026-03-20 17:21:57 | WORK user=344531774518591498 earned=+56 lucky=False bal=70
|
|
||||||
2026-03-20 17:21:59 | BEG user=272518654715887618 earned=+39 jailed=False bal=39
|
|
||||||
2026-03-20 17:22:02 | DAILY user=344531774518591498 earned=+150 streak=1 bal=220
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
134
ops_admin_commands.py
Normal file
134
ops_admin_commands.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_ops_admin_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
process,
|
||||||
|
start_time: datetime.datetime,
|
||||||
|
log_dir: Path,
|
||||||
|
guild_obj: discord.Object,
|
||||||
|
restart_file: Path,
|
||||||
|
get_member_cache_size: Callable[[], int],
|
||||||
|
get_paused: Callable[[], bool],
|
||||||
|
set_paused: Callable[[bool], None],
|
||||||
|
count_economy_users: Callable[[], Awaitable[int]],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="status", description=S.CMD["status"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_status(interaction: discord.Interaction):
|
||||||
|
mem = process.memory_info()
|
||||||
|
cpu = process.cpu_percent(interval=0.1)
|
||||||
|
uptime = datetime.datetime.now() - start_time
|
||||||
|
hours, rem = divmod(int(uptime.total_seconds()), 3600)
|
||||||
|
minutes, seconds = divmod(rem, 60)
|
||||||
|
tasks_count = len(asyncio.all_tasks())
|
||||||
|
latency_ms = round(bot.latency * 1000, 1)
|
||||||
|
cache_size = get_member_cache_size()
|
||||||
|
user_count = await count_economy_users()
|
||||||
|
|
||||||
|
embed = discord.Embed(title=S.STATUS_UI["title"], color=0x57F287)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["uptime_field"],
|
||||||
|
value=S.STATUS_UI["uptime_val"].format(hours=hours, minutes=minutes, seconds=seconds),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["latency_field"],
|
||||||
|
value=S.STATUS_UI["latency_val"].format(ms=latency_ms),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["ram_field"],
|
||||||
|
value=S.STATUS_UI["ram_val"].format(mb=f"{mem.rss / 1024**2:.1f}"),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["cpu_field"],
|
||||||
|
value=S.STATUS_UI["cpu_val"].format(percent=f"{cpu:.1f}"),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["tasks_field"],
|
||||||
|
value=str(tasks_count),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["eco_players_field"],
|
||||||
|
value=str(user_count),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["members_cache_field"],
|
||||||
|
value=str(cache_size),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
log_lines = [
|
||||||
|
S.STATUS_UI["log_line"].format(name=p.name, size_kb=f"{p.stat().st_size / 1024:.1f}")
|
||||||
|
for p in sorted(log_dir.glob("*.log*"))
|
||||||
|
if p.is_file()
|
||||||
|
]
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["log_files_field"],
|
||||||
|
value="\n".join(log_lines) or S.STATUS_UI["none"],
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="sync", description=S.CMD["sync"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_sync(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
tree.copy_global_to(guild=guild_obj)
|
||||||
|
await tree.sync(guild=guild_obj)
|
||||||
|
tree.clear_commands(guild=None)
|
||||||
|
await tree.sync()
|
||||||
|
await interaction.followup.send(S.MSG_SYNC_DONE, ephemeral=True)
|
||||||
|
log.info("/sync triggered by %s", interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="restart", description=S.CMD["restart"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_restart(interaction: discord.Interaction):
|
||||||
|
restart_file.write_text(json.dumps({"channel_id": interaction.channel_id}), encoding="utf-8")
|
||||||
|
await interaction.response.send_message(S.MSG_RESTARTING, ephemeral=True)
|
||||||
|
log.info("/restart triggered by %s", interaction.user)
|
||||||
|
subprocess.Popen([sys.executable] + sys.argv, cwd=os.getcwd())
|
||||||
|
await bot.close()
|
||||||
|
|
||||||
|
@tree.command(name="shutdown", description=S.CMD["shutdown"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_shutdown(interaction: discord.Interaction):
|
||||||
|
await interaction.response.send_message(S.MSG_SHUTTING_DOWN, ephemeral=True)
|
||||||
|
log.info("/shutdown triggered by %s", interaction.user)
|
||||||
|
await bot.close()
|
||||||
|
|
||||||
|
@tree.command(name="pause", description=S.CMD["pause"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_pause(interaction: discord.Interaction):
|
||||||
|
paused = not get_paused()
|
||||||
|
set_paused(paused)
|
||||||
|
msg = S.MSG_PAUSED if paused else S.MSG_UNPAUSED
|
||||||
|
log.info("/pause toggled → %s by %s", "PAUSED" if paused else "UNPAUSED", interaction.user)
|
||||||
|
await interaction.response.send_message(msg, ephemeral=True)
|
||||||
135
ops_channel_commands.py
Normal file
135
ops_channel_commands.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_ops_channel_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
get_allowed_channels: Callable[[], list[int]],
|
||||||
|
set_allowed_channels: Callable[[list[int]], None],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="send", description=S.CMD["send"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kanal=S.OPT["send_kanal"],
|
||||||
|
sõnum=S.OPT["send_sõnum"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_send(interaction: discord.Interaction, kanal: discord.TextChannel, sõnum: str):
|
||||||
|
try:
|
||||||
|
await kanal.send(sõnum)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.SEND_UI["sent"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.SEND_UI["forbidden"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["send_failed"].format(error=e), ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="economysetup", description=S.CMD["economysetup"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_economysetup(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
guild = interaction.guild
|
||||||
|
bot_member = guild.get_member(bot.user.id)
|
||||||
|
bot_top_pos = max((r.position for r in bot_member.roles if r.managed), default=1)
|
||||||
|
|
||||||
|
all_role_names = [economy.ECONOMY_ROLE] + [name for _, name in economy.LEVEL_ROLES]
|
||||||
|
|
||||||
|
created, existing = [], []
|
||||||
|
for name in all_role_names:
|
||||||
|
role = discord.utils.find(lambda r, n=name: r.name == n, guild.roles)
|
||||||
|
if role is None:
|
||||||
|
await guild.create_role(name=name, reason="/economysetup")
|
||||||
|
created.append(name)
|
||||||
|
else:
|
||||||
|
existing.append(name)
|
||||||
|
|
||||||
|
positions: dict[discord.Role, int] = {}
|
||||||
|
base = max(bot_top_pos - 1, 1)
|
||||||
|
for i, name in enumerate(all_role_names):
|
||||||
|
role = discord.utils.find(lambda r, n=name: r.name == n, guild.roles)
|
||||||
|
if role:
|
||||||
|
positions[role] = max(base - i, 1)
|
||||||
|
if positions:
|
||||||
|
try:
|
||||||
|
await guild.edit_role_positions(positions=positions)
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
|
embed = discord.Embed(title=S.TITLE["economysetup"], color=0x57F287)
|
||||||
|
if created:
|
||||||
|
embed.add_field(name=S.ECONOMYSETUP_UI["created_field"], value="\n".join(created), inline=True)
|
||||||
|
if existing:
|
||||||
|
embed.add_field(name=S.ECONOMYSETUP_UI["existing_field"], value="\n".join(existing), inline=True)
|
||||||
|
embed.set_footer(text=S.ECONOMYSETUP_UI["footer"])
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
log.info("/economysetup triggered by %s", interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="allowchannel", description=S.CMD["allowchannel"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kanal=S.OPT["allowchannel_kanal"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_allowchannel(interaction: discord.Interaction, kanal: discord.TextChannel):
|
||||||
|
allowed = get_allowed_channels()
|
||||||
|
if kanal.id in allowed:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["already_allowed"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
allowed.append(kanal.id)
|
||||||
|
set_allowed_channels(allowed)
|
||||||
|
log.info("ALLOWCHANNEL +%s by %s", kanal, interaction.user)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["added"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="denychannel", description=S.CMD["denychannel"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kanal=S.OPT["denychannel_kanal"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_denychannel(interaction: discord.Interaction, kanal: discord.TextChannel):
|
||||||
|
allowed = get_allowed_channels()
|
||||||
|
if kanal.id not in allowed:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["not_in_list"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
allowed.remove(kanal.id)
|
||||||
|
set_allowed_channels(allowed)
|
||||||
|
log.info("DENYCHANNEL -%s by %s", kanal, interaction.user)
|
||||||
|
if allowed:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["removed"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["removed_last"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="channels", description=S.CMD["channels"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_channels(interaction: discord.Interaction):
|
||||||
|
allowed = get_allowed_channels()
|
||||||
|
if not allowed:
|
||||||
|
desc = S.CHANNEL_UI["list_empty"]
|
||||||
|
else:
|
||||||
|
lines = "\n".join(f"• <#{cid}>" for cid in allowed)
|
||||||
|
desc = S.CHANNEL_UI["list_filled"].format(lines=lines)
|
||||||
|
embed = discord.Embed(title=S.CHANNEL_UI["list_title"], description=desc, color=0x5865F2)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
12
pb_client.py
12
pb_client.py
@@ -7,24 +7,26 @@ Environment variables (set in .env):
|
|||||||
PB_URL Base URL of PocketBase (default: http://127.0.0.1:8090)
|
PB_URL Base URL of PocketBase (default: http://127.0.0.1:8090)
|
||||||
PB_ADMIN_EMAIL PocketBase admin e-mail
|
PB_ADMIN_EMAIL PocketBase admin e-mail
|
||||||
PB_ADMIN_PASSWORD PocketBase admin password
|
PB_ADMIN_PASSWORD PocketBase admin password
|
||||||
|
PB_ECONOMY_COLLECTION_DEV / PB_ECONOMY_COLLECTION_ECONOMY
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
_log = logging.getLogger("tipiCOIN.pb")
|
_log = logging.getLogger("tipiCOIN.pb")
|
||||||
|
|
||||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
PB_URL = config.PB_URL
|
||||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
||||||
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||||
ECONOMY_COLLECTION = "economy_users"
|
ECONOMY_COLLECTION = config.PB_ECONOMY_COLLECTION
|
||||||
|
|
||||||
_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ Requirements:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -21,10 +20,12 @@ from dotenv import load_dotenv
|
|||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
import config # noqa: E402
|
||||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
|
||||||
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
PB_URL = config.PB_URL
|
||||||
COLLECTION = "economy_users"
|
PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
||||||
|
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||||
|
COLLECTION = config.PB_ECONOMY_COLLECTION
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# New fields to add
|
# New fields to add
|
||||||
|
|||||||
180
scripts/reset_pb_collections.py
Normal file
180
scripts/reset_pb_collections.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
"""Destructively recreate economy PocketBase collections for dev + economy profiles.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/reset_pb_collections.py --confirm
|
||||||
|
|
||||||
|
This will DELETE and recreate the collections configured by:
|
||||||
|
- PB_ECONOMY_COLLECTION_DEV
|
||||||
|
- PB_ECONOMY_COLLECTION_ECONOMY
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
import config # noqa: E402
|
||||||
|
|
||||||
|
PB_URL = config.PB_URL
|
||||||
|
PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
||||||
|
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||||
|
|
||||||
|
|
||||||
|
def _text_field(name: str, required: bool = False) -> dict:
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"type": "text",
|
||||||
|
"required": required,
|
||||||
|
"options": {"min": None, "max": None, "pattern": ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _number_field(name: str) -> dict:
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"type": "number",
|
||||||
|
"required": False,
|
||||||
|
"options": {"min": None, "max": None, "noDecimal": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _bool_field(name: str) -> dict:
|
||||||
|
return {"name": name, "type": "bool", "required": False}
|
||||||
|
|
||||||
|
|
||||||
|
def _json_field(name: str) -> dict:
|
||||||
|
return {"name": name, "type": "json", "required": False}
|
||||||
|
|
||||||
|
|
||||||
|
def _collection_payload(name: str) -> dict:
|
||||||
|
fields = [
|
||||||
|
_text_field("user_id", required=True),
|
||||||
|
_number_field("balance"),
|
||||||
|
_number_field("exp"),
|
||||||
|
_number_field("daily_streak"),
|
||||||
|
_text_field("last_daily"),
|
||||||
|
_text_field("last_work"),
|
||||||
|
_text_field("last_beg"),
|
||||||
|
_text_field("last_crime"),
|
||||||
|
_text_field("last_rob"),
|
||||||
|
_text_field("last_heist"),
|
||||||
|
_text_field("last_streak_date"),
|
||||||
|
_text_field("jailed_until"),
|
||||||
|
_text_field("last_fish"),
|
||||||
|
_json_field("items"),
|
||||||
|
_json_field("item_uses"),
|
||||||
|
_json_field("reminders"),
|
||||||
|
_json_field("prestige_upgrades"),
|
||||||
|
_json_field("fish_book"),
|
||||||
|
_json_field("fish_inventory"),
|
||||||
|
_bool_field("eco_banned"),
|
||||||
|
_bool_field("jailbreak_used"),
|
||||||
|
_number_field("heist_global_cd_until"),
|
||||||
|
_number_field("peak_balance"),
|
||||||
|
_number_field("lifetime_earned"),
|
||||||
|
_number_field("lifetime_lost"),
|
||||||
|
_number_field("work_count"),
|
||||||
|
_number_field("beg_count"),
|
||||||
|
_number_field("total_wagered"),
|
||||||
|
_number_field("biggest_win"),
|
||||||
|
_number_field("biggest_loss"),
|
||||||
|
_number_field("slots_jackpots"),
|
||||||
|
_number_field("crimes_attempted"),
|
||||||
|
_number_field("crimes_succeeded"),
|
||||||
|
_number_field("times_jailed"),
|
||||||
|
_number_field("total_bail_paid"),
|
||||||
|
_number_field("heists_joined"),
|
||||||
|
_number_field("heists_won"),
|
||||||
|
_number_field("total_given"),
|
||||||
|
_number_field("total_received"),
|
||||||
|
_number_field("best_daily_streak"),
|
||||||
|
_number_field("prestige_level"),
|
||||||
|
_number_field("prestige_points"),
|
||||||
|
_number_field("season_total_exp"),
|
||||||
|
_number_field("total_fish_caught"),
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"type": "base",
|
||||||
|
"fields": fields,
|
||||||
|
"listRule": None,
|
||||||
|
"viewRule": None,
|
||||||
|
"createRule": None,
|
||||||
|
"updateRule": None,
|
||||||
|
"deleteRule": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _auth_token(session: aiohttp.ClientSession) -> str:
|
||||||
|
async with session.post(
|
||||||
|
f"{PB_URL}/api/collections/_superusers/auth-with-password",
|
||||||
|
json={"identity": PB_ADMIN_EMAIL, "password": PB_ADMIN_PASSWORD},
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise RuntimeError(f"Auth failed ({resp.status}): {await resp.text()}")
|
||||||
|
return (await resp.json())["token"]
|
||||||
|
|
||||||
|
|
||||||
|
async def _delete_if_exists(session: aiohttp.ClientSession, headers: dict[str, str], name: str) -> None:
|
||||||
|
async with session.get(f"{PB_URL}/api/collections/{name}", headers=headers) as resp:
|
||||||
|
if resp.status == 404:
|
||||||
|
print(f"[SKIP] {name} does not exist")
|
||||||
|
return
|
||||||
|
if resp.status != 200:
|
||||||
|
raise RuntimeError(f"Could not fetch {name} ({resp.status}): {await resp.text()}")
|
||||||
|
|
||||||
|
async with session.delete(f"{PB_URL}/api/collections/{name}", headers=headers) as resp:
|
||||||
|
if resp.status not in (200, 204):
|
||||||
|
raise RuntimeError(f"Delete failed for {name} ({resp.status}): {await resp.text()}")
|
||||||
|
print(f"[DELETE] {name}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_collection(session: aiohttp.ClientSession, headers: dict[str, str], name: str) -> None:
|
||||||
|
payload = _collection_payload(name)
|
||||||
|
async with session.post(f"{PB_URL}/api/collections", json=payload, headers=headers) as resp:
|
||||||
|
if resp.status not in (200, 201):
|
||||||
|
raise RuntimeError(f"Create failed for {name} ({resp.status}): {await resp.text()}")
|
||||||
|
print(f"[CREATE] {name}")
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--confirm", action="store_true", help="Required flag to run destructive reset")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.confirm:
|
||||||
|
raise SystemExit("Refusing to run without --confirm (this operation deletes collections).")
|
||||||
|
|
||||||
|
targets = []
|
||||||
|
for name in [config.PB_ECONOMY_COLLECTION_DEV, config.PB_ECONOMY_COLLECTION_ECONOMY]:
|
||||||
|
if name and name not in targets:
|
||||||
|
targets.append(name)
|
||||||
|
|
||||||
|
if not targets:
|
||||||
|
raise SystemExit("No target collections configured.")
|
||||||
|
|
||||||
|
timeout = aiohttp.ClientTimeout(total=20)
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
token = await _auth_token(session)
|
||||||
|
headers = {"Authorization": token}
|
||||||
|
|
||||||
|
for name in targets:
|
||||||
|
await _delete_if_exists(session, headers, name)
|
||||||
|
await _create_collection(session, headers, name)
|
||||||
|
|
||||||
|
print("\nDone. Collections recreated:")
|
||||||
|
for name in targets:
|
||||||
|
print(f" - {name}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
301
strings.py
301
strings.py
@@ -128,6 +128,7 @@ CMD: dict[str, str] = {
|
|||||||
"shutdown": "Lülita bot välja (ilma taaskäivituseta)",
|
"shutdown": "Lülita bot välja (ilma taaskäivituseta)",
|
||||||
"pause": "Peata / jätka kõik käsklused (hooldusrežiim)",
|
"pause": "Peata / jätka kõik käsklused (hooldusrežiim)",
|
||||||
"send": "Saada sõnum valitud kanalisse",
|
"send": "Saada sõnum valitud kanalisse",
|
||||||
|
"profile": "Vaata oma profiili: saldo, tase, esemed, statistika ja kalakogu",
|
||||||
"balance": "Vaata enda (või kellegi teise) TipiCOINide saldot",
|
"balance": "Vaata enda (või kellegi teise) TipiCOINide saldot",
|
||||||
"daily": "Võta enda päevane TipiCOINi boonus",
|
"daily": "Võta enda päevane TipiCOINi boonus",
|
||||||
"work": "Tööta ja teeni TipiCOINe (1h ooteaeg)",
|
"work": "Tööta ja teeni TipiCOINe (1h ooteaeg)",
|
||||||
@@ -157,11 +158,19 @@ CMD: dict[str, str] = {
|
|||||||
"adminunban": "[Admin] Eemalda majandussüsteemi keeld kasutajalt",
|
"adminunban": "[Admin] Eemalda majandussüsteemi keeld kasutajalt",
|
||||||
"adminreset": "[Admin] Lähtesta kasutaja majandusandmed",
|
"adminreset": "[Admin] Lähtesta kasutaja majandusandmed",
|
||||||
"adminview": "[Admin] Vaata kasutaja majandusandmeid",
|
"adminview": "[Admin] Vaata kasutaja majandusandmeid",
|
||||||
|
"adminexp": "[Admin] Anna v\u00f5i v\u00f5ta EXP kasutajalt",
|
||||||
|
"adminitem": "[Admin] Anna v\u00f5i eemalda ese kasutajalt (tasuta)",
|
||||||
"allowchannel": "[Admin] Lisa kanal, kus bot võib vastata käsklustele",
|
"allowchannel": "[Admin] Lisa kanal, kus bot võib vastata käsklustele",
|
||||||
"denychannel": "[Admin] Eemalda kanal lubatud kanalite nimekirjast",
|
"denychannel": "[Admin] Eemalda kanal lubatud kanalite nimekirjast",
|
||||||
"channels": "[Admin] Näita lubatud kanalite nimekirja",
|
"channels": "[Admin] Näita lubatud kanalite nimekirja",
|
||||||
"economysetup": "[Admin] Loo ja sea korda majandussüsteemi rollid",
|
"economysetup": "[Admin] Loo ja sea korda majandussüsteemi rollid",
|
||||||
"blackjack": "Mängi blackjacki TipiBOTi vastu",
|
"blackjack": "Mängi blackjacki TipiBOTi vastu",
|
||||||
|
"prestige": "Prestiiži (nõuab taset 30) ja teeni Prestiižipunkte",
|
||||||
|
"prestigeshop": "Vaata prestiižipoodi ja sinu uuenduste taset",
|
||||||
|
"prestigebuy": "Osta prestiižiuuendus Prestiižipunktide eest",
|
||||||
|
"fish": "Mine kalastama (interaktiivne mäng, 2min ooteaeg)",
|
||||||
|
"fishbook": "Vaata oma kalakogu ja kogutud kalaliike",
|
||||||
|
"fishsell": "Müü kalu oma inventarist",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -195,6 +204,12 @@ OPT: dict[str, str] = {
|
|||||||
"stats_kasutaja": "Mängija, kelle statistikat tahad vaadata (vaikimisi sina)",
|
"stats_kasutaja": "Mängija, kelle statistikat tahad vaadata (vaikimisi sina)",
|
||||||
"adminseason_top_n": "Kui palju mängijaid võitis (vaikimisi 10)",
|
"adminseason_top_n": "Kui palju mängijaid võitis (vaikimisi 10)",
|
||||||
"blackjack_panus": "Panus TipiCOINides ('all' = kogu saldo)",
|
"blackjack_panus": "Panus TipiCOINides ('all' = kogu saldo)",
|
||||||
|
"prestigebuy_upgrade": "Uuenduse ID (vaata /prestigeshop)",
|
||||||
|
"fishbook_kasutaja": "Mängija, kelle kalakogu vaadata (vaikimisi sina)",
|
||||||
|
"profile_kasutaja": "Mängija, kelle profiili vaadata (vaikimisi sina)",
|
||||||
|
"adminexp_kogus": "Positiivne = anna, negatiivne = võta",
|
||||||
|
"adminitem_ese": "Eseme ID (kasutatav sisse, vaata /shop)",
|
||||||
|
"adminitem_tegevus": "'anna' või 'eemalda'",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -218,8 +233,7 @@ HELP_CATEGORIES: dict[str, dict] = {
|
|||||||
"description": "TipiCOIN majandus",
|
"description": "TipiCOIN majandus",
|
||||||
"color": 0xF4C430,
|
"color": 0xF4C430,
|
||||||
"fields": [
|
"fields": [
|
||||||
("/balance [@user]", "Vaata enda (või kellegi teise) TipiCOINide saldot"),
|
("/profile [@user]", "Saldo, tase, EXP progress, prestiiz - kõik ühes kohas. Nupud: Esemed · Statistika · Kalakogu."),
|
||||||
("/rank [@user]", "Vaata oma EXP, taset, progressi ja edetabeli positsiooni. T2 pood avaneb tasemel 10, T3 tasemel 20."),
|
|
||||||
("/cooldowns", "Vaata kõikide käskude ooteaegu. Näitab ka vangla ooteaega."),
|
("/cooldowns", "Vaata kõikide käskude ooteaegu. Näitab ka vangla ooteaega."),
|
||||||
("/daily", "Võta enda päevane TipiCOINide boonus. 20h ooteaeg. Streak'i boonus: 3d=+50%, 7d=+100%, 14d=+200%."),
|
("/daily", "Võta enda päevane TipiCOINide boonus. 20h ooteaeg. Streak'i boonus: 3d=+50%, 7d=+100%, 14d=+200%."),
|
||||||
("/work", "Tööta ja teeni TipiCOINe (1h ooteaeg)"),
|
("/work", "Tööta ja teeni TipiCOINe (1h ooteaeg)"),
|
||||||
@@ -324,6 +338,7 @@ REMINDER_OPTS: list[tuple[str, str, str]] = [
|
|||||||
("beg", "🙏 /beg", "Kerjamine (5min ooteaeg)"),
|
("beg", "🙏 /beg", "Kerjamine (5min ooteaeg)"),
|
||||||
("crime", "🦹 /crime", "Kuritegu (2t ooteaeg)"),
|
("crime", "🦹 /crime", "Kuritegu (2t ooteaeg)"),
|
||||||
("rob", "🔫 /rob", "Rööv (2t ooteaeg)"),
|
("rob", "🔫 /rob", "Rööv (2t ooteaeg)"),
|
||||||
|
("fish", "🎣 /fish", "Kalapüük (2min ooteaeg)"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -530,8 +545,12 @@ TITLE: dict[str, str] = {
|
|||||||
"jailbreak_bail": "💸 Kautsjon",
|
"jailbreak_bail": "💸 Kautsjon",
|
||||||
"give": "<:TipiHEART:1483431377561976853> TipiCOINi ülekanne",
|
"give": "<:TipiHEART:1483431377561976853> TipiCOINi ülekanne",
|
||||||
"stats": "📊 Mängustatistika",
|
"stats": "📊 Mängustatistika",
|
||||||
"leaderboard_coins":"🪙 TipiBOTi edetabel - Mündid",
|
"leaderboard_coins": "🪙 TipiBOTi edetabel - Mündid",
|
||||||
"leaderboard_exp": "📊 TipiBOTi edetabel - EXP / Tase",
|
"leaderboard_exp": "📊 TipiBOTi edetabel - EXP / Tase",
|
||||||
|
"leaderboard_season": "🏆 TipiBOTi edetabel - Hooaja EXP",
|
||||||
|
"leaderboard_prestige": "<:TipiFIRE:1483431381668335687> TipiBOTi edetabel - Prestiiž",
|
||||||
|
"leaderboard_wagered": "🎲 TipiBOTi edetabel - Hasartmängud",
|
||||||
|
"leaderboard_fish": "🎣 TipiBOTi edetabel - Kalapüük",
|
||||||
"rps": "⚔️ Kivi, Paber, Käärid",
|
"rps": "⚔️ Kivi, Paber, Käärid",
|
||||||
"rps_duel": "⚔️ KPK duell",
|
"rps_duel": "⚔️ KPK duell",
|
||||||
"rps_duel_active": "⚔️ KPK duell - käimas",
|
"rps_duel_active": "⚔️ KPK duell - käimas",
|
||||||
@@ -556,6 +575,16 @@ TITLE: dict[str, str] = {
|
|||||||
"blackjack_push": "🤝 Viik!",
|
"blackjack_push": "🤝 Viik!",
|
||||||
"blackjack_dbust": "<:TipiSKULL:1483431378929451028> Üle 21 - mõlemad kaotasid!",
|
"blackjack_dbust": "<:TipiSKULL:1483431378929451028> Üle 21 - mõlemad kaotasid!",
|
||||||
"blackjack_dwin": "<:TipiFIRE:1483431381668335687> Topeltpanus võitis!",
|
"blackjack_dwin": "<:TipiFIRE:1483431381668335687> Topeltpanus võitis!",
|
||||||
|
"prestige_confirm": "🔥 Prestiiž - kinnita",
|
||||||
|
"prestige_success": "<:TipiFIRE:1483431381668335687> Prestiiž {level} saavutatud!",
|
||||||
|
"prestige_too_low": "❌ Prestiiž pole saadaval",
|
||||||
|
"prestige_shop": "<:TipiFIRE:1483431381668335687> Prestiižipood",
|
||||||
|
"prestige_buy_ok": "✅ Uuendus ostetud!",
|
||||||
|
"fish_cast": "🎣 Otsid kala...",
|
||||||
|
"fish_bite": "🐟 KALA NÄKKAB!",
|
||||||
|
"fish_escape": "🎣 Kala pääses!",
|
||||||
|
"fish_junk": "🗑️ Ai ai ai...",
|
||||||
|
"fishbook": "📖 Kalakogu",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -585,6 +614,7 @@ ERR: dict[str, str] = {
|
|||||||
"item_not_found": "❌ Eset ei leitud.",
|
"item_not_found": "❌ Eset ei leitud.",
|
||||||
"item_level_req": "🔒 Selle eseme ostmiseks vajad **taset {min_level}** (sul on tase {user_level}). Teeni EXP-id kõiki käske kasutades.",
|
"item_level_req": "🔒 Selle eseme ostmiseks vajad **taset {min_level}** (sul on tase {user_level}). Teeni EXP-id kõiki käske kasutades.",
|
||||||
"not_your_game": "❌ See pole sinu mäng!",
|
"not_your_game": "❌ See pole sinu mäng!",
|
||||||
|
"game_in_progress": "❌ Sul on juba mäng käimas! Lõpeta see enne.",
|
||||||
"not_your_challenge":"❌ See väljakutse pole sulle!",
|
"not_your_challenge":"❌ See väljakutse pole sulle!",
|
||||||
"not_your_menu": "❌ See ei ole sinu menüü.",
|
"not_your_menu": "❌ See ei ole sinu menüü.",
|
||||||
"give_self": "❌ Sa ei saa iseendale TipiCOINe anda.",
|
"give_self": "❌ Sa ei saa iseendale TipiCOINe anda.",
|
||||||
@@ -607,6 +637,7 @@ ERR: dict[str, str] = {
|
|||||||
"channel_only": "❌ Boti käske saab kasutada ainult nendes kanalites: {channels}",
|
"channel_only": "❌ Boti käske saab kasutada ainult nendes kanalites: {channels}",
|
||||||
"guild_only": "Seda käsku saab kasutada ainult serveris.",
|
"guild_only": "Seda käsku saab kasutada ainult serveris.",
|
||||||
"sheet_error": "❌ Tabeli laadimine ebaõnnestus: ```{error}```",
|
"sheet_error": "❌ Tabeli laadimine ebaõnnestus: ```{error}```",
|
||||||
|
"gamble_cooldown": "🎰 Oled just mänginud! Saad uuesti mängida {ts}.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -622,6 +653,7 @@ CD_MSG: dict[str, str] = {
|
|||||||
"heist": "⏳ Saad uuesti heisti teha {ts}.",
|
"heist": "⏳ Saad uuesti heisti teha {ts}.",
|
||||||
"heist_global": "⏳ Pangahoidla alles kosub eelmisest röövist. Järgmine heist võimalik {ts}.",
|
"heist_global": "⏳ Pangahoidla alles kosub eelmisest röövist. Järgmine heist võimalik {ts}.",
|
||||||
"jailed": "<:TipiTROLL:1483431380166774895> Oled vangis! Pääsed välja {ts}. Kasuta `/jailbreak`, et varem välja pääseda.",
|
"jailed": "<:TipiTROLL:1483431380166774895> Oled vangis! Pääsed välja {ts}. Kasuta `/jailbreak`, et varem välja pääseda.",
|
||||||
|
"fish": "🎣 Saad uuesti kalastada {ts}.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -661,20 +693,23 @@ BJ: dict[str, str] = {
|
|||||||
ITEM_DESCRIPTIONS: dict[str, str] = {
|
ITEM_DESCRIPTIONS: dict[str, str] = {
|
||||||
"gaming_hiir": "Koolist varastatud hiir? Ei, see on mängurihiir. Teeni töötades 50% rohkem TipiCOINe.",
|
"gaming_hiir": "Koolist varastatud hiir? Ei, see on mängurihiir. Teeni töötades 50% rohkem TipiCOINe.",
|
||||||
"hiirematt": "XXL suuruses, ainult parimast materjalist. Kerjamise ooteaeg 5min → 3min.",
|
"hiirematt": "XXL suuruses, ainult parimast materjalist. Kerjamise ooteaeg 5min → 3min.",
|
||||||
"korvaklapid": "Noise-cancelling - kuuled ainult TipiCOINide kõlinat. Päevase boonuse ooteaeg 20h → 18h.",
|
"korvaklapid": "Noise-cancelling - kuuled ainult TipiCOINide kõlinat. Päevase boonuse ooteaeg 20h → 18h + 25⬡ boonust.",
|
||||||
"lan_pass": "Ametlik TipiLANi pilet (2025). Päevane boonus on duubeldatud.",
|
"lan_pass": "Ametlik TipiLANi pilet (2025). Päevane boonus on duubeldatud.",
|
||||||
"energiajook": "Kolm Red Bulli järjest. 30% tõenäosus, et teenid töötades 3x rohkem.",
|
"energiajook": "Kolm Red Bulli järjest. 30% tõenäosus, et teenid töötades 3x rohkem.",
|
||||||
"gaming_laptop": "RTX 5090 jooksutab botte 24/7. Päevane boonus genereerib 5% intressi sinu saldo pealt.",
|
"gaming_laptop": "RTX 5090 jooksutab botte 24/7. Päevane boonus genereerib 5% intressi sinu saldo pealt.",
|
||||||
"anticheat": "VAC, EAC, Faceit AC ja BattlEye korraga. Röövimine sinu vastu ebaõnnestub. **2 kasutust**, siis pead ostma uue.",
|
"anticheat": "VAC, EAC, Faceit AC ja BattlEye korraga. Röövimine sinu vastu ebaõnnestub. **2 kasutust**, siis pead ostma uue.",
|
||||||
"reguleeritav_laud": "Võid nii seista kui istuda - alati võidad. /work teenib 25% rohkem (stackib mängurihiirega).",
|
"reguleeritav_laud": "Võid nii seista kui istuda - alati võidad. /work teenib 25% rohkem (stackib mängurihiirega).",
|
||||||
"jellyfin": "Self-hosted meediaserver - oled suurfirmadest sõltumatu. Röövimise edu tõenäosus 45% → 60%.",
|
"jellyfin": "Self-hosted meediaserver - oled suurfirmadest sõltumatu. Röövimise edu 45% → 60%. Grupiröövi õnnestumisele +5%.",
|
||||||
"mikrofon": "Parem helikvaliteet teeb sind usutavamaks. Teeni 30% rohkem eduka /crime puhul.",
|
"mikrofon": "Parem helikvaliteet teeb sind usutavamaks. Teeni 30% rohkem eduka /crime puhul.",
|
||||||
"klaviatuur": "Klõbinad kostuvad üle kogu saali. /beg teenib 2x rohkem.",
|
"klaviatuur": "Klõbinad kostuvad üle kogu saali. /beg teenib 2x rohkem.",
|
||||||
"monitor": "240Hz ja 27 tolli. /work ooteaeg: 1h → 40min.",
|
"monitor": "240Hz ja 27 tolli. /work ooteaeg: 1h → 40min.",
|
||||||
"cat6": "Gigabitine internet = ideaalne piraatluseks. /crime edu tõenäosus tõuseb 60% → 75%.",
|
"cat6": "Gigabitine internet = ideaalne piraatluseks. /crime edu tõenäosus tõuseb 60% → 75%.",
|
||||||
"monitor_360": "360Hz, 1ms. Mänguautomaadi jackpot 10x → 15x, kolmik 4x → 6x.",
|
"monitor_360": "360Hz, 1ms. Mänguautomaadi jackpot 10x → 15x, kolmik 4x → 6x. Hasartmängude ooteaeg 30s → 25s.",
|
||||||
"karikas": "Ainult legendidele. Streak ei nulli, kui sa mõne päeva vahele jätad.",
|
"karikas": "Ainult legendidele. Streak ei nulli, kui sa mõne päeva vahele jätad.",
|
||||||
"gaming_tool": "Nii mugav, et isegi admin ei saa sind üles. /crime ebaõnnestumine ei saada sind vanglasse.",
|
"gaming_tool": "Nii mugav, et isegi admin ei saa sind üles. /crime ebaõnnestumine ei saada sind vanglasse.",
|
||||||
|
"ussipurk": "Lakkumatu toiduga ussipurk - kalad ei saa vastu. Kalapyygi ooteaeg 2min → 90s.",
|
||||||
|
"kalavork": "Suurem võrk = suuremad kalad. Kõigi kalade haruldus tõuseb ühe astme võrra.",
|
||||||
|
"echolood": "Täpne ehholood näitab kala täpset asukohta. Haukamise aken 2s → 3s.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -742,19 +777,26 @@ MEMBER_FIELDS: list[tuple[str, str]] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
ADMINVIEW_UI: dict[str, str] = {
|
ADMINVIEW_UI: dict[str, str] = {
|
||||||
"title": "🔍 {name} - majandusandmed",
|
"title": "🔍 {name} - majandusandmed",
|
||||||
"banned_yes": "🚫 JAH",
|
"banned_yes": "🚫 JAH",
|
||||||
"banned_no": "✅ Ei",
|
"banned_no": "✅ Ei",
|
||||||
"f_balance": "💰 Saldo",
|
"f_balance": "💰 Saldo",
|
||||||
"f_streak": "🔥 Streak",
|
"f_exp": "📊 EXP / Tase",
|
||||||
"f_banned": "🚫 Keelatud",
|
"f_streak": "🔥 Streak",
|
||||||
"f_jailed": "🚔 Vangis kuni",
|
"f_banned": "🚫 Keelatud",
|
||||||
"f_items": "🎒 Esemed",
|
"f_jailed": "🚔 Vangis kuni",
|
||||||
"f_uses": "🔢 Kasutused",
|
"f_prestige": "🔥 Prestiiž",
|
||||||
"f_last_daily": "⏱️ Viimati daily",
|
"f_items": "🎒 Esemed",
|
||||||
"f_last_work": "⏱️ Viimati work",
|
"f_uses": "🔢 Kasutused",
|
||||||
"f_last_crime": "⏱️ Viimati crime",
|
"f_fish": "🎣 Kala",
|
||||||
"footer": "ID: {uid}",
|
"f_last_daily": "⏱️ Viimati daily",
|
||||||
|
"f_last_work": "⏱️ Viimati work",
|
||||||
|
"f_last_crime": "⏱️ Viimati crime",
|
||||||
|
"f_last_fish": "⏱️ Viimati fish",
|
||||||
|
"footer": "ID: {uid}",
|
||||||
|
"exp_val": "{exp} EXP (Tase {level})",
|
||||||
|
"prestige_val": "Prestiiž {level} · {pp} PP",
|
||||||
|
"fish_val": "{caught} püütud · {inv} inventaris",
|
||||||
}
|
}
|
||||||
|
|
||||||
ECONOMYSETUP_UI: dict[str, str] = {
|
ECONOMYSETUP_UI: dict[str, str] = {
|
||||||
@@ -816,6 +858,11 @@ BIRTHDAY_UI: dict[str, str] = {
|
|||||||
"footer": "Leht {month}/12 · {month_name}",
|
"footer": "Leht {month}/12 · {month_name}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIRTHDAY_MONTHS: list[str] = [
|
||||||
|
"Jaanuar", "Veebruar", "Märts", "Aprill", "Mai", "Juuni",
|
||||||
|
"Juuli", "August", "September", "Oktoober", "November", "Detsember",
|
||||||
|
]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# /check summary strings
|
# /check summary strings
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -828,6 +875,7 @@ CHECK_UI: dict[str, str] = {
|
|||||||
"stat_uid": "Kasutaja ID",
|
"stat_uid": "Kasutaja ID",
|
||||||
"stat_discord": "Discordi kasutajanimi",
|
"stat_discord": "Discordi kasutajanimi",
|
||||||
"stat_bday": "Sünnipäev",
|
"stat_bday": "Sünnipäev",
|
||||||
|
"no_name": "(no name)",
|
||||||
"done": "**Kontroll lõpetatud!**",
|
"done": "**Kontroll lõpetatud!**",
|
||||||
"already_ok": "✅ Juba korras: {count}",
|
"already_ok": "✅ Juba korras: {count}",
|
||||||
"fixed": "🔧 Parandatud: {count}",
|
"fixed": "🔧 Parandatud: {count}",
|
||||||
@@ -836,9 +884,35 @@ CHECK_UI: dict[str, str] = {
|
|||||||
"errors": "⚠️ Vead: {count}",
|
"errors": "⚠️ Vead: {count}",
|
||||||
"details_header": "**Üksikasjad:**",
|
"details_header": "**Üksikasjad:**",
|
||||||
"details_more": "... ja {count} rohkem",
|
"details_more": "... ja {count} rohkem",
|
||||||
|
"detail_error": "⚠️ {error}",
|
||||||
|
"detail_nickname": "hüüdnimi",
|
||||||
|
"detail_roles_added": "+rollid: {roles}",
|
||||||
|
"detail_changed": "🔧 **{name}**: {parts}",
|
||||||
"ids_filled": "\n🔑 Täideti **{count}** puuduvat kasutaja ID-d.",
|
"ids_filled": "\n🔑 Täideti **{count}** puuduvat kasutaja ID-d.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# /status UI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
STATUS_UI: dict[str, str] = {
|
||||||
|
"title": "🖥️ Boti olek",
|
||||||
|
"uptime_field": "🕐 Uptime",
|
||||||
|
"uptime_val": "{hours}t {minutes}m {seconds}s",
|
||||||
|
"latency_field": "📡 Latency",
|
||||||
|
"latency_val": "{ms} ms",
|
||||||
|
"ram_field": "🧠 RAM (RSS)",
|
||||||
|
"ram_val": "{mb} MB",
|
||||||
|
"cpu_field": "⚙️ CPU",
|
||||||
|
"cpu_val": "{percent}%",
|
||||||
|
"tasks_field": "🔄 Async tasks",
|
||||||
|
"eco_players_field": "👤 Eco players",
|
||||||
|
"members_cache_field": "📋 Liikmed (cache)",
|
||||||
|
"log_files_field": "📂 Log files",
|
||||||
|
"log_line": "`{name}` - {size_kb} KB",
|
||||||
|
"none": "-",
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Admin command responses and DMs
|
# Admin command responses and DMs
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -856,6 +930,14 @@ ADMIN: dict[str, str] = {
|
|||||||
"unban_dm": "✅ Sinu TipiBOTi majandussüsteemis osalemise keeld on tühistatud. Saad taas käske kasutada.",
|
"unban_dm": "✅ Sinu TipiBOTi majandussüsteemis osalemise keeld on tühistatud. Saad taas käske kasutada.",
|
||||||
"reset_done": "🗑️ **{name}** majandusandmed on lähtestatud.\n📝 Põhjus: {reason}",
|
"reset_done": "🗑️ **{name}** majandusandmed on lähtestatud.\n📝 Põhjus: {reason}",
|
||||||
"reset_dm": "🗑️ Admin lähtestas sinu TipiBOTi majandusandmed (saldo, esemed, streak).\n📝 Põhjus: *{reason}*",
|
"reset_dm": "🗑️ Admin lähtestas sinu TipiBOTi majandusandmed (saldo, esemed, streak).\n📝 Põhjus: *{reason}*",
|
||||||
|
"exp_done": "{emoji} **{name}**: {verb} EXP → kokku **{exp} EXP** (Tase {level}).\n📝 Põhjus: {reason}",
|
||||||
|
"exp_dm": "{emoji} Admin muutis sinu EXP-i: **{verb} EXP**\n📝 Põhjus: *{reason}*\nUus EXP: **{exp}** (Tase {level})",
|
||||||
|
"item_given": "✅ **{item}** antud kasutajale **{name}** (tasuta).",
|
||||||
|
"item_removed":"🗑️ **{item}** eemaldatud kasutajalt **{name}**.",
|
||||||
|
"item_invalid":"❌ Tundmatu ese: `{item_id}`. Kontrolli `/shop` eseme ID-d.",
|
||||||
|
"item_not_owned": "❌ **{name}** ei oma eset `{item_id}`.",
|
||||||
|
"item_dm_given": "✅ Admin andis sulle eseme: **{item}**.",
|
||||||
|
"item_dm_removed":"🗑️ Admin eemaldas sult eseme: **{item}**.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -870,6 +952,34 @@ SEASON: dict[str, str] = {
|
|||||||
"done": "✅ Hooaeg lõpetatud - EXP, mündid ja esemed lähtestatud.",
|
"done": "✅ Hooaeg lõpetatud - EXP, mündid ja esemed lähtestatud.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# /profile tabbed view
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
PROFILE_UI: dict[str, str] = {
|
||||||
|
"btn_profile": "💰 Profiil",
|
||||||
|
"btn_items": "🎒 Esemed",
|
||||||
|
"btn_stats": "📊 Statistika",
|
||||||
|
"btn_fish": "🎣 Kalakogu",
|
||||||
|
"main_title": "💰 {name}",
|
||||||
|
"items_title": "🎒 {name} - Esemed",
|
||||||
|
"stats_title": "📊 {name} - Statistika",
|
||||||
|
"fish_title": "🎣 {name} - Kalakogu",
|
||||||
|
"items_empty": "Sul pole ühtegi eset.",
|
||||||
|
"f_balance": "💰 Saldo",
|
||||||
|
"f_level": "📊 Tase",
|
||||||
|
"f_streak": "🔥 Streak",
|
||||||
|
"f_prestige": "⭐ Prestiiz",
|
||||||
|
"f_jail": "🚔 Vangis kuni",
|
||||||
|
"f_progress": "→ Tase {next}",
|
||||||
|
"progress_bar": "`{bar}` {done}/{needed} EXP",
|
||||||
|
"level_val": "Tase {level} - {role}",
|
||||||
|
"prestige_val":"⭐ P{level} · {pp} PP",
|
||||||
|
"footer_t1": "Tase 10 avab T2 poe · Tase 20 avab T3 poe",
|
||||||
|
"footer_t2": "T2 pood avatud · Tase 20 avab T3 poe",
|
||||||
|
"footer_t3": "T2 ja T3 pood avatud",
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# /balance embed strings
|
# /balance embed strings
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -895,10 +1005,12 @@ COOLDOWNS_UI: dict[str, str] = {
|
|||||||
"beg_line": "🙏 **/beg** {status}{note}",
|
"beg_line": "🙏 **/beg** {status}{note}",
|
||||||
"crime_line": "🦹 **/crime** {status}",
|
"crime_line": "🦹 **/crime** {status}",
|
||||||
"rob_line": "🔫 **/rob** {status}",
|
"rob_line": "🔫 **/rob** {status}",
|
||||||
|
"fish_line": "🎣 **/fish** {status}{note}",
|
||||||
"note_korvak": " *(kõrvaklapid: 18t)*",
|
"note_korvak": " *(kõrvaklapid: 18t)*",
|
||||||
"note_monitor": " *(monitor: 40min)*",
|
"note_monitor": " *(monitor: 40min)*",
|
||||||
"note_hiirematt": " *(hiirematt: 3min)*",
|
"note_hiirematt": " *(hiirematt: 3min)*",
|
||||||
"jailed": "\n🚔 **Vanglas** - vabaneb <t:{ts}:R>",
|
"note_ussipurk": " *(ussipurk: 90s)*",
|
||||||
|
"jailed": "\n<EFBFBD> **Vanglas** - vabaneb <t:{ts}:R>",
|
||||||
"jail_expired": "\n🔓 Vangla lõppes",
|
"jail_expired": "\n🔓 Vangla lõppes",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,6 +1074,7 @@ ROB_UI: dict[str, str] = {
|
|||||||
"win_desc": "Varastasid {stolen} kasutajalt **{name}**!\nSaldo: {balance}",
|
"win_desc": "Varastasid {stolen} kasutajalt **{name}**!\nSaldo: {balance}",
|
||||||
"anticheat_desc": "**{name}** kaitseb end Anticheati'ga - said trahvi {fine}.",
|
"anticheat_desc": "**{name}** kaitseb end Anticheati'ga - said trahvi {fine}.",
|
||||||
"anticheat_worn": "⚠️ Sinu **Anticheat** on kulunud! Osta uus `/buy` käsuga.",
|
"anticheat_worn": "⚠️ Sinu **Anticheat** on kulunud! Osta uus `/buy` käsuga.",
|
||||||
|
"victim_dm": "💸 **{robber}** varastas sinult **{stolen}** münti!",
|
||||||
"fail_desc": "Jäid vahele! Trahv: {fine}.\nSaldo: {balance}",
|
"fail_desc": "Jäid vahele! Trahv: {fine}.\nSaldo: {balance}",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -987,9 +1100,8 @@ BUY_UI: dict[str, str] = {
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
JAILBREAK_UI: dict[str, str] = {
|
JAILBREAK_UI: dict[str, str] = {
|
||||||
"btn_die1": "🎲 Viska 1. täring ({try_}/{max})",
|
"btn_roll": "🎲 Viska täringud ({try_}/{max})",
|
||||||
"btn_die2": "🎲 Viska 2. täring",
|
"rolling_desc": "<:TipiDICE:1485923107108556950> *Täringud lendavad...*",
|
||||||
"die1_desc": "1. täring: **{die}**\n\nNüüd viska 2. täring!",
|
|
||||||
"free_desc": "{d1} {d2}\n\n✅ Viskasid duubli - pääsesid vanglast!",
|
"free_desc": "{d1} {d2}\n\n✅ Viskasid duubli - pääsesid vanglast!",
|
||||||
"miss_desc": "{d1} {d2}\n\n{left} katset jäänud. Proovi uuesti!",
|
"miss_desc": "{d1} {d2}\n\n{left} katset jäänud. Proovi uuesti!",
|
||||||
"intro_desc": "Oled vangis kuni {ts}.\n\nViska täringuid ja proovi **duublit** saada - siis pääsed tasuta vabaks!\nSul on **{tries} katset**. Ebaõnnestumisel saad valida: maksa kautsjon **(20–30% saldost, min 350 ⬡)** või jää vanglasse kuni aja lõpuni.",
|
"intro_desc": "Oled vangis kuni {ts}.\n\nViska täringuid ja proovi **duublit** saada - siis pääsed tasuta vabaks!\nSul on **{tries} katset**. Ebaõnnestumisel saad valida: maksa kautsjon **(20–30% saldost, min 350 ⬡)** või jää vanglasse kuni aja lõpuni.",
|
||||||
@@ -1009,13 +1121,22 @@ JAILBREAK_UI: dict[str, str] = {
|
|||||||
|
|
||||||
LEADERBOARD_UI: dict[str, str] = {
|
LEADERBOARD_UI: dict[str, str] = {
|
||||||
"house_entry": "🤖 {name} *(maja)* - {balance}",
|
"house_entry": "🤖 {name} *(maja)* - {balance}",
|
||||||
|
"house_default_name": "TipiBOT",
|
||||||
"no_entries": "Keegi ei ole veel punkte teeninud.",
|
"no_entries": "Keegi ei ole veel punkte teeninud.",
|
||||||
"footer": "Lehekülg {page}/{total} · {count} mängijat",
|
"footer": "Lehekülg {page}/{total} · {count} mängijat",
|
||||||
"btn_coins": "🪙 Mündid",
|
"btn_coins": "🪙 Mündid",
|
||||||
"btn_exp": "📊 EXP",
|
"btn_exp": "📊 EXP",
|
||||||
"btn_find_me": "📍 Mina",
|
"btn_find_me": "📍 Mina",
|
||||||
"exp_entry": "{prefix} {name} - {exp} EXP *(Tase {level})*",
|
"exp_entry": "{prefix} {name} - {exp} EXP *(Tase {level})*",
|
||||||
"unknown_user": "Kasutaja {uid}",
|
"unknown_user": "Kasutaja {uid}",
|
||||||
|
"btn_season": "🏆 Hooaeg",
|
||||||
|
"btn_prestige": "🔥 Prestiiž",
|
||||||
|
"btn_wagered": "🎲 Hasartmäng",
|
||||||
|
"btn_fish": "🎣 Kalapyyk",
|
||||||
|
"season_entry": "{prefix} {name} - {exp} EXP *(Prestiiž {prestige})*",
|
||||||
|
"prestige_entry": "{prefix} {name} - Prestiiž **{prestige}** · {pp} PP",
|
||||||
|
"wagered_entry": "{prefix} {name} - {wagered} panustatud",
|
||||||
|
"fish_entry": "{prefix} {name} - {caught} kala",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1106,3 +1227,131 @@ BJ_UI: dict[str, str] = {
|
|||||||
"bust": " 💥",
|
"bust": " 💥",
|
||||||
"balance_line": " · Saldo: {balance}",
|
"balance_line": " · Saldo: {balance}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Prestige system strings
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
PRESTIGE_SHOP_NAMES: dict[str, str] = {
|
||||||
|
"coin_mult": "Mündiboost",
|
||||||
|
"exp_mult": "EXP-boost",
|
||||||
|
"daily_plus": "Päevabonus+",
|
||||||
|
"work_plus": "Töötaja+",
|
||||||
|
}
|
||||||
|
|
||||||
|
PRESTIGE_SHOP_DESCRIPTIONS: dict[str, str] = {
|
||||||
|
"coin_mult": "Kõik TipiCOINide teenimisallikad (daily, töö, kerja, kala) teenivad +8% rohkem iga taseme kohta. Max 5 taset → +40%.",
|
||||||
|
"exp_mult": "Kõik EXP allikad teenivad +8% rohkem iga taseme kohta. Max 5 taset → +40%.",
|
||||||
|
"daily_plus": "Päevase boonuse alussumma tõuseb +20% iga taseme kohta. Max 3 taset → +60%.",
|
||||||
|
"work_plus": "/work teenib +20% rohkem iga taseme kohta. Max 3 taset → +60%.",
|
||||||
|
}
|
||||||
|
|
||||||
|
PRESTIGE_UI: dict[str, str] = {
|
||||||
|
"confirm_desc": (
|
||||||
|
"Oled tasemel **{level}** ({exp} EXP).\n\n"
|
||||||
|
"Prestiiži korral saad **{pp}** <:TipiFIRE:1483431381668335687> ja kõik lähtestub:\n"
|
||||||
|
"• Saldo, EXP, esemed, ooteajad\n\n"
|
||||||
|
"**Kalakogu jääb alles!**\n\nKas oled kindel?"
|
||||||
|
),
|
||||||
|
"btn_confirm": "🔥 Jah, prestiiži!",
|
||||||
|
"btn_cancel": "❌ Tühista",
|
||||||
|
"btn_tab_status": "⭐ Prestiiz",
|
||||||
|
"btn_tab_shop": "🛍️ Uuendused",
|
||||||
|
"success_desc": (
|
||||||
|
"Said **{pp}** <:TipiFIRE:1483431381668335687>\n"
|
||||||
|
"Prestiiži tase: **{level}**\n"
|
||||||
|
"Kogutud PP: **{total_pp}** <:TipiFIRE:1483431381668335687>\n\n"
|
||||||
|
"*Kõik lähtestati. Alusta otsast!*"
|
||||||
|
),
|
||||||
|
"too_low_desc": "Prestiiži jaoks vajad taset **{required}** (sul on tase {level}).",
|
||||||
|
"shop_desc": "Sul on **{pp}** <:TipiFIRE:1483431381668335687> · Vajuta nuppu uuenduse ostmiseks",
|
||||||
|
"shop_maxed": "✅ Max",
|
||||||
|
"shop_level_fmt": "Tase {cur}/{max}",
|
||||||
|
"shop_cost_fmt": "{cost} <:TipiFIRE:1483431381668335687>",
|
||||||
|
"buy_success_desc":"**{name}** uuendatud tasemele **{new_level}/{max_level}**!\nPP alles: **{pp}** <:TipiFIRE:1483431381668335687>",
|
||||||
|
"buy_no_pp": "<:TipICRY:1483431288852709387> Sul pole piisavalt PP. Sul on **{have}**, vajad **{need}** <:TipiFIRE:1483431381668335687>.",
|
||||||
|
"buy_maxed": "❌ See uuendus on juba maksimumtasemel.",
|
||||||
|
"buy_not_found": "❌ Sellist uuendust ei leitud. Vaata `/prestigeshop`.",
|
||||||
|
"rank_line": "<:TipiFIRE:1483431381668335687> Prestiiž **{level}** · {pp} PP",
|
||||||
|
"rank_season": "🏆 Hooaja EXP: **{exp}**",
|
||||||
|
"btn_buy_upgrade": "{emoji} {name} +1 ({cost} PP)",
|
||||||
|
"status_footer": "⭐ Prestiiž {level} · {pp} PP",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fishing system strings
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
FISH_NAMES: dict[str, str] = {
|
||||||
|
"sarj": "Särg",
|
||||||
|
"ahven": "Ahven",
|
||||||
|
"koger": "Koger",
|
||||||
|
"viidikas": "Viidikas",
|
||||||
|
"latikas": "Latikas",
|
||||||
|
"karpkala": "Karpkala",
|
||||||
|
"linask": "Linask",
|
||||||
|
"haug": "Haug",
|
||||||
|
"angerjas": "Angerjas",
|
||||||
|
"siig": "Siig",
|
||||||
|
"forell": "Forell",
|
||||||
|
"koha": "Koha",
|
||||||
|
"tougjas": "Tõugjas",
|
||||||
|
"lohe": "Lõhe",
|
||||||
|
"vimb": "Vimb",
|
||||||
|
}
|
||||||
|
|
||||||
|
FISH_RARITY_NAMES: dict[str, str] = {
|
||||||
|
"common": "Tavaline",
|
||||||
|
"uncommon": "Ebatavaline",
|
||||||
|
"rare": "Haruldane",
|
||||||
|
"epic": "Eepiline",
|
||||||
|
"legendary": "Legendaarne",
|
||||||
|
}
|
||||||
|
|
||||||
|
FISH_RARITY_EMOJI: dict[str, str] = {
|
||||||
|
"common": "🐟",
|
||||||
|
"uncommon": "🐠",
|
||||||
|
"rare": "🎣",
|
||||||
|
"epic": "⭐",
|
||||||
|
"legendary": "🌟",
|
||||||
|
}
|
||||||
|
|
||||||
|
FISH_JUNK_LINES: list[str] = [
|
||||||
|
"Sa saad... **vana saabas**. Klassika.",
|
||||||
|
"Õnnitlused, leidsid **kasutatud autorehvi**. Keskkond tänab sind... mitte.",
|
||||||
|
"Taas üks **klaaspudel** rohkem jões.",
|
||||||
|
"**Vana poes käimise kott**! Hoidis aega hästi.",
|
||||||
|
"**Roostes konserv** - ilma sildita. Parem mitte teada, mis sees on.",
|
||||||
|
"**Ummistunud drenaažitoru**. Keegi oli hoolimatu.",
|
||||||
|
"**Tühi rahakott**. Kellegi päev läks halvemaks kui sinu oma.",
|
||||||
|
"**Vana CD-plaat** - Evanescence, 2003. Heas seisukorras.",
|
||||||
|
"Sa said **kivikese**. Ilus kivikene. Aga siiski kivikene.",
|
||||||
|
"**Kaotsi läinud droon**. GPS ei tööta, aku tühi.",
|
||||||
|
]
|
||||||
|
|
||||||
|
FISH_UI: dict[str, str] = {
|
||||||
|
"btn_wait": "🎣 Oota näkkamist...",
|
||||||
|
"btn_bite": "🐟 TÕMBA!",
|
||||||
|
"btn_sell": "💰 Müü",
|
||||||
|
"btn_keep": "🎒 Hoia",
|
||||||
|
"cast_desc": "Viskad õnge vette. Oota, kuni kala näkkab...\n\n-# Vajuta nuppu, kui kala näkkab!",
|
||||||
|
"bite_desc": "**KALA NÄKKAB!** Tõmba kiiresti! ⚡\n\n-# Sul on 2 sekundit!",
|
||||||
|
"escape_desc": "Liiga hilja - kala lipsas minema. Proovi järgmine kord kiiremini!",
|
||||||
|
"junk_desc": "{text}\n\n-# Saldo: {balance}",
|
||||||
|
"catch_desc": "**{name}** · {weight}g · +{exp} EXP\n-# Kas müüd kohe ({value}) või hoiad inventaris?",
|
||||||
|
"catch_sold": "**{name}** · {weight}g\n\n+{coins} · +{exp} EXP\nSaldo: {balance}",
|
||||||
|
"catch_kept": "**{name}** · {weight}g lisatud inventarisse. *(+{exp} EXP)*",
|
||||||
|
"new_fish": "\n✨ **Uus kala kalakogusse lisatud!**",
|
||||||
|
"too_early": "❌ Kala pole veel näkkanud! Oota...",
|
||||||
|
"book_caught": "Püütud kalaliike: **{caught}/{total}**",
|
||||||
|
"book_yes": "✅ {emoji} **{name}** *({rarity})* · {count}×{inv}",
|
||||||
|
"book_inv": " *(inventaris: {n})*",
|
||||||
|
"book_no": "❓ **???** *({rarity})*",
|
||||||
|
"book_footer": "Lehekülg {page}/{total_pages} · {caught}/{total} liiki",
|
||||||
|
"book_empty": "Sa pole veel ühtegi kala püüdnud! Kasuta `/fish`.",
|
||||||
|
"inv_empty": "Sinu kalainventaar on tühi! Kasuta `/fish` kala püüdmiseks.",
|
||||||
|
"inv_header": "Sul on **{count}** kala inventaris *(kokku väärt {total_value})*",
|
||||||
|
"inv_entry": "{emoji} **{name}** · {weight}g · {value}",
|
||||||
|
"inv_sold_all": "Müüsid **{count}** kala kokku {coins} eest!\nSaldo: {balance}",
|
||||||
|
"inv_none": "Inventaaris pole midagi müüa.",
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user