Compare commits
3 Commits
master
...
rewrite-v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,
|
||||||
|
)
|
||||||
9867
logs/bot.log
9867
logs/bot.log
File diff suppressed because it is too large
Load Diff
@@ -1,714 +1,108 @@
|
|||||||
2026-03-20 01:34:58 | WORK user=340451525799182357 earned=+123 lucky=False bal=27988
|
2026-04-01 18:44:29 | WORK user=272518654715887618 earned=+54 lucky=False bal=24077
|
||||||
2026-03-20 01:35:02 | BEG user=340451525799182357 earned=+24 jailed=False bal=28012
|
2026-04-01 20:02:11 | BEG user=272518654715887618 earned=+23 jailed=False bal=24100
|
||||||
2026-03-20 01:35:39 | ROB_FAIL robber=340451525799182357 victim=218972931701735424 fine=-120 robber_bal=27892
|
2026-04-01 20:02:53 | ROULETTE_WIN user=178852380018868224 bet=15214708 colour=punane result=punane mult=1 bal=30429416
|
||||||
2026-03-20 01:35:56 | SLOTS_MISS user=340451525799182357 bet=100 change=-100 bal=27792
|
2026-04-01 20:03:42 | ROULETTE_WIN user=178852380018868224 bet=30429416 colour=must result=must mult=1 bal=60858832
|
||||||
2026-03-20 01:36:04 | SLOTS_PAIR user=340451525799182357 bet=100 change=50 bal=27842
|
2026-04-01 20:04:16 | ROULETTE_LOSE user=178852380018868224 bet=60858832 colour=punane result=must mult=1 bal=0
|
||||||
2026-03-20 01:36:11 | SLOTS_MISS user=340451525799182357 bet=100 change=-100 bal=27742
|
2026-04-01 20:06:37 | BEG user=401373976431165449 earned=+52 jailed=False bal=8446
|
||||||
2026-03-20 01:36:19 | SLOTS_PAIR user=340451525799182357 bet=100 change=50 bal=27792
|
2026-04-01 20:06:39 | WORK user=401373976431165449 earned=+60 lucky=False bal=8506
|
||||||
2026-03-20 01:37:38 | BLACKJACK user=340451525799182357 payout=+400 net=+200 bal=27992
|
2026-04-01 20:06:52 | DAILY user=401373976431165449 earned=+750 streak=1 bal=9256
|
||||||
2026-03-20 01:38:07 | BEG user=340451525799182357 earned=+30 jailed=False bal=28022
|
2026-04-01 20:07:07 | DAILY user=272518654715887618 earned=+825 streak=1 bal=24925
|
||||||
2026-03-20 01:38:23 | BLACKJACK user=340451525799182357 payout=+0 net=-100 bal=27922
|
2026-04-01 20:07:12 | CRIME_WIN user=272518654715887618 earned=+331 bal=25256
|
||||||
2026-03-20 01:38:43 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=26922
|
2026-04-01 20:07:38 | CRIME_WIN user=401373976431165449 earned=+391 bal=9647
|
||||||
2026-03-20 01:39:15 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=27922
|
2026-04-01 20:07:49 | BUY user=401373976431165449 item=echolood cost=-8000 bal=1647
|
||||||
2026-03-20 01:39:36 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=29922
|
2026-04-01 20:09:16 | ROB_BLOCKED robber=824516445382901800 victim=340451525799182357 fine=-118 robber_bal=891 ac_uses_left=1
|
||||||
2026-03-20 01:39:49 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=30922
|
2026-04-01 20:09:23 | ROB_BLOCKED robber=401373976431165449 victim=340451525799182357 fine=-175 robber_bal=1472 ac_uses_left=0
|
||||||
2026-03-20 01:40:02 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=31922
|
2026-04-01 20:09:52 | ROB_WIN robber=178852380018868224 victim=340451525799182357 stolen=+34868 jackpot=False robber_bal=34868 victim_bal=140238
|
||||||
2026-03-20 01:40:12 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=30922
|
2026-04-01 20:10:48 | DAILY user=367347301322326016 earned=+712 streak=1 bal=8462
|
||||||
2026-03-20 01:40:26 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=29922
|
2026-04-01 20:10:55 | WORK user=367347301322326016 earned=+25 lucky=False bal=8487
|
||||||
2026-03-20 01:40:40 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=30922
|
2026-04-01 20:11:00 | BEG user=367347301322326016 earned=+15 jailed=False bal=8502
|
||||||
2026-03-20 01:40:54 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=31922
|
2026-04-01 20:11:37 | ROB_FAIL robber=272518654715887618 victim=340451525799182357 fine=-140 robber_bal=25116
|
||||||
2026-03-20 01:41:05 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=30922
|
2026-04-01 20:15:19 | HEIST_FAIL user=178852380018868224 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=33868
|
||||||
2026-03-20 01:41:26 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=29922
|
2026-04-01 20:15:19 | HEIST_FAIL user=340451525799182357 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=139238
|
||||||
2026-03-20 01:41:35 | BLACKJACK user=340451525799182357 payout=+2500 net=+1500 bal=31422
|
2026-04-01 20:15:19 | HEIST_FAIL user=272518654715887618 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=24116
|
||||||
2026-03-20 01:42:32 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=32422
|
2026-04-01 20:15:19 | HEIST_FAIL user=209554152584380420 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=20112
|
||||||
2026-03-20 01:42:46 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=31422
|
2026-04-01 20:15:19 | HEIST_FAIL user=401373976431165449 fine=-220 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=1252
|
||||||
2026-03-20 01:43:34 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=30422
|
2026-04-01 20:15:19 | HEIST_FAIL user=344531774518591498 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=112701
|
||||||
2026-03-20 01:43:48 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=32422
|
2026-04-01 20:15:19 | HEIST_FAIL user=367347301322326016 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=7502
|
||||||
2026-03-20 01:44:00 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=31422
|
2026-04-01 20:15:45 | JAIL_FREE user=272518654715887618 method=doubles
|
||||||
2026-03-20 01:44:11 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=32422
|
2026-04-01 20:20:07 | JAIL_FREE user=344531774518591498 method=doubles
|
||||||
2026-03-20 01:44:22 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=33422
|
2026-04-01 20:20:14 | DAILY user=344531774518591498 earned=+825 streak=1 bal=113526
|
||||||
2026-03-20 01:44:47 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=35422
|
2026-04-01 20:20:16 | WORK user=344531774518591498 earned=+45 lucky=False bal=113571
|
||||||
2026-03-20 01:45:02 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=36422
|
2026-04-01 20:20:19 | WORK user=272518654715887618 earned=+55 lucky=False bal=24171
|
||||||
2026-03-20 01:45:17 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=35422
|
2026-04-01 20:20:19 | BEG user=344531774518591498 earned=+22 jailed=False bal=113593
|
||||||
2026-03-20 01:45:28 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=36422
|
2026-04-01 20:20:36 | BLACKJACK user=272518654715887618 payout=+0 net=-24171 bal=0
|
||||||
2026-03-20 01:45:42 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=37422
|
2026-04-01 20:21:03 | CRIME_FAIL user=344531774518591498 fine=-90 jailed=True bal=113503
|
||||||
2026-03-20 01:45:55 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=38422
|
2026-04-01 20:21:11 | FISH user=272518654715887618 fish=koger weight=590 value=15
|
||||||
2026-03-20 01:46:15 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=39422
|
2026-04-01 20:21:45 | ROB_WIN robber=344531774518591498 victim=340451525799182357 stolen=+15566 jackpot=False robber_bal=129069 victim_bal=123672
|
||||||
2026-03-20 01:46:27 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=38422
|
2026-04-01 20:25:40 | BAIL_PAID user=178852380018868224 fine=-8760 pct=26% bal=25108
|
||||||
2026-03-20 01:46:38 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=37422
|
2026-04-01 20:28:28 | BEG user=178852380018868224 earned=+28 jailed=False bal=25136
|
||||||
2026-03-20 01:46:54 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=38422
|
2026-04-01 20:28:30 | WORK user=178852380018868224 earned=+92 lucky=False bal=25228
|
||||||
2026-03-20 01:47:05 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=37422
|
2026-04-01 20:28:33 | DAILY user=178852380018868224 earned=+825 streak=1 bal=26053
|
||||||
2026-03-20 01:47:15 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=36422
|
2026-04-01 20:28:38 | CRIME_WIN user=178852380018868224 earned=+640 bal=26693
|
||||||
2026-03-20 01:47:46 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=38422
|
2026-04-01 20:35:35 | BEG user=401373976431165449 earned=+56 jailed=True bal=1308
|
||||||
2026-03-20 01:47:59 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=40422
|
2026-04-01 20:36:20 | JAIL_FREE user=401373976431165449 method=doubles
|
||||||
2026-03-20 01:48:11 | BLACKJACK user=340451525799182357 payout=+2000 net=+0 bal=40422
|
2026-04-01 20:37:47 | FISH user=401373976431165449 fish=angerjas weight=989 value=79
|
||||||
2026-03-20 01:48:23 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=41422
|
2026-04-01 20:46:02 | DAILY user=338622999127261185 earned=+300 streak=1 bal=300
|
||||||
2026-03-20 01:48:39 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=40422
|
2026-04-01 20:56:03 | BEG user=344531774518591498 earned=+60 jailed=False bal=129129
|
||||||
2026-03-20 01:48:54 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=41422
|
2026-04-01 20:56:29 | ROULETTE_LOSE user=344531774518591498 bet=1000 colour=punane result=must mult=1 bal=128129
|
||||||
2026-03-20 01:49:12 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=42422
|
2026-04-01 21:01:23 | BEG user=272518654715887618 earned=+33 jailed=False bal=33
|
||||||
2026-03-20 01:49:25 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=43422
|
2026-04-01 21:01:42 | FISH user=272518654715887618 fish=sarj weight=151 value=6
|
||||||
2026-03-20 01:49:36 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=43422
|
2026-04-01 21:02:38 | FISH user=344531774518591498 fish=viidikas weight=98 value=6
|
||||||
2026-03-20 01:49:51 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=44422
|
2026-04-01 21:02:57 | FISH_SELL user=344531774518591498 count=2 coins=+13 bal=128142
|
||||||
2026-03-20 01:50:06 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=45422
|
2026-04-01 21:03:10 | BEG user=401373976431165449 earned=+54 jailed=False bal=1362
|
||||||
2026-03-20 01:50:16 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=45422
|
2026-04-01 21:03:31 | FISH user=401373976431165449 fish=siig weight=584 value=63
|
||||||
2026-03-20 01:50:28 | BLACKJACK user=340451525799182357 payout=+0 net=-2000 bal=43422
|
2026-04-01 21:05:19 | WORK user=401373976431165449 earned=+131 lucky=False bal=1493
|
||||||
2026-03-20 01:50:38 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=42422
|
2026-04-01 21:05:38 | FISH user=401373976431165449 fish=siig weight=1624 value=112
|
||||||
2026-03-20 01:50:53 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=41422
|
2026-04-01 21:05:48 | FISH_SELL user=401373976431165449 count=3 coins=+254 bal=1747
|
||||||
2026-03-20 01:51:04 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=40422
|
2026-04-01 21:08:31 | BEG user=401373976431165449 earned=+32 jailed=False bal=1779
|
||||||
2026-03-20 01:51:20 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=39422
|
2026-04-01 21:08:45 | FISH user=401373976431165449 fish=tougjas weight=3051 value=207
|
||||||
2026-03-20 01:51:31 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=40422
|
2026-04-01 21:09:01 | ROULETTE_WIN user=401373976431165449 bet=1000 colour=punane result=punane mult=1 bal=2779
|
||||||
2026-03-20 01:51:44 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=40422
|
2026-04-01 21:09:35 | BLACKJACK user=401373976431165449 payout=+3000 net=+1500 bal=4279
|
||||||
2026-03-20 01:51:54 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=39422
|
2026-04-01 21:09:35 | ROULETTE_LOSE user=344531774518591498 bet=1000 colour=punane result=must mult=1 bal=127142
|
||||||
2026-03-20 01:52:07 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=38422
|
2026-04-01 21:10:05 | ROULETTE_WIN user=401373976431165449 bet=1000 colour=punane result=punane mult=1 bal=5279
|
||||||
2026-03-20 01:52:20 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=39422
|
2026-04-01 21:10:46 | ROULETTE_WIN user=344531774518591498 bet=1000 colour=punane result=punane mult=1 bal=128142
|
||||||
2026-03-20 01:52:33 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=38422
|
2026-04-01 21:11:03 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=must result=punane mult=1 bal=4279
|
||||||
2026-03-20 01:52:43 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=39422
|
2026-04-01 21:11:24 | ROULETTE_LOSE user=344531774518591498 bet=1000 colour=punane result=roheline mult=1 bal=127142
|
||||||
2026-03-20 01:52:58 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=40422
|
2026-04-01 21:15:50 | WORK user=338622999127261185 earned=+15 lucky=False bal=315
|
||||||
2026-03-20 01:54:04 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=41422
|
2026-04-01 21:15:54 | CRIME_WIN user=338622999127261185 earned=+453 bal=768
|
||||||
2026-03-20 01:54:20 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=42422
|
2026-04-01 21:16:00 | BEG user=338622999127261185 earned=+20 jailed=False bal=788
|
||||||
2026-03-20 01:55:01 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=41422
|
2026-04-01 21:16:13 | FISH user=338622999127261185 fish=ahven weight=422 value=14
|
||||||
2026-03-20 01:55:12 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=41422
|
2026-04-01 21:18:36 | BEG user=401373976431165449 earned=+20 jailed=False bal=4299
|
||||||
2026-03-20 01:55:20 | BLACKJACK user=340451525799182357 payout=+2500 net=+1500 bal=42922
|
2026-04-01 21:18:52 | FISH user=401373976431165449 fish=karpkala weight=1920 value=47
|
||||||
2026-03-20 01:55:42 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=43922
|
2026-04-01 21:20:58 | SLOTS_TRIPLE user=344531774518591498 bet=1000 change=4000 bal=131142
|
||||||
2026-03-20 01:55:54 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=44922
|
2026-04-01 21:21:39 | SLOTS_MISS user=344531774518591498 bet=1000 change=-1000 bal=130142
|
||||||
2026-03-20 01:56:02 | BLACKJACK user=340451525799182357 payout=+2500 net=+1500 bal=46422
|
2026-04-01 21:28:25 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=3299
|
||||||
2026-03-20 01:56:16 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=45422
|
2026-04-01 21:29:10 | SLOTS_PAIR user=344531774518591498 bet=1000 change=500 bal=130642
|
||||||
2026-03-20 01:56:27 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=44422
|
2026-04-01 21:29:28 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=2299
|
||||||
2026-03-20 01:56:39 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=45422
|
2026-04-01 21:30:49 | SLOTS_PAIR user=344531774518591498 bet=1000 change=500 bal=131142
|
||||||
2026-03-20 01:56:49 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=45422
|
2026-04-01 21:31:30 | SLOTS_PAIR user=344531774518591498 bet=1000 change=500 bal=131642
|
||||||
2026-03-20 01:57:01 | BLACKJACK user=340451525799182357 payout=+1000 net=+0 bal=45422
|
2026-04-01 21:31:33 | ROULETTE_WIN user=401373976431165449 bet=1000 colour=punane result=punane mult=1 bal=3299
|
||||||
2026-03-20 01:58:03 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=44422
|
2026-04-01 21:31:37 | BEG user=401373976431165449 earned=+68 jailed=False bal=3367
|
||||||
2026-03-20 01:58:19 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43422
|
2026-04-01 21:32:05 | FISH user=401373976431165449 fish=latikas weight=2351 value=66
|
||||||
2026-03-20 01:59:08 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=42422
|
2026-04-01 21:33:14 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=2367
|
||||||
2026-03-20 01:59:22 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=43422
|
2026-04-01 21:35:09 | SLOTS_MISS user=344531774518591498 bet=1000 change=-1000 bal=130642
|
||||||
2026-03-20 01:59:37 | BLACKJACK user=340451525799182357 payout=+2500 net=+1500 bal=44922
|
2026-04-01 21:35:14 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=1367
|
||||||
2026-03-20 01:59:56 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43922
|
2026-04-01 21:48:35 | ROB_FAIL robber=338622999127261185 victim=340451525799182357 fine=-237 robber_bal=551
|
||||||
2026-03-20 02:00:14 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=42922
|
2026-04-01 21:49:59 | BEG user=401373976431165449 earned=+56 jailed=False bal=1423
|
||||||
2026-03-20 02:00:37 | BLACKJACK user=340451525799182357 payout=+2500 net=+1500 bal=44422
|
2026-04-01 21:50:15 | FISH user=401373976431165449 fish=vimb weight=856 value=462
|
||||||
2026-03-20 02:00:57 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=45422
|
2026-04-01 22:11:02 | BEG user=401373976431165449 earned=+52 jailed=False bal=1475
|
||||||
2026-03-20 02:01:08 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=44422
|
2026-04-01 22:11:13 | WORK user=401373976431165449 earned=+73 lucky=False bal=1548
|
||||||
2026-03-20 02:01:38 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43422
|
2026-04-01 22:11:27 | ROB_WIN robber=401373976431165449 victim=367347301322326016 stolen=+1818 jackpot=False robber_bal=3366 victim_bal=5684
|
||||||
2026-03-20 02:01:56 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=42422
|
2026-04-01 22:11:44 | FISH user=401373976431165449 fish=lohe weight=2973 value=313
|
||||||
2026-03-20 02:02:15 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=43422
|
2026-04-01 22:13:30 | ROULETTE_LOSE user=401373976431165449 bet=2000 colour=punane result=roheline mult=1 bal=1366
|
||||||
2026-03-20 02:02:30 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=44422
|
2026-04-01 22:14:33 | ROULETTE_LOSE user=401373976431165449 bet=1366 colour=punane result=must mult=1 bal=0
|
||||||
2026-03-20 02:02:43 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=45422
|
2026-04-01 22:40:19 | WORK user=367347301322326016 earned=+82 lucky=False bal=5766
|
||||||
2026-03-20 02:02:55 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=44422
|
2026-04-01 22:40:23 | CRIME_FAIL user=367347301322326016 fine=-100 jailed=True bal=5666
|
||||||
2026-03-20 02:03:07 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43422
|
2026-04-01 22:46:27 | WORK user=344531774518591498 earned=+116 lucky=False bal=130758
|
||||||
2026-03-20 02:03:32 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=44422
|
2026-04-01 22:46:30 | BEG user=344531774518591498 earned=+58 jailed=False bal=130816
|
||||||
2026-03-20 02:03:48 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=45422
|
2026-04-01 22:46:35 | CRIME_WIN user=344531774518591498 earned=+419 bal=131235
|
||||||
2026-03-20 02:04:01 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=44422
|
2026-04-01 22:46:44 | ROB_FAIL robber=344531774518591498 victim=340451525799182357 fine=-246 robber_bal=130989
|
||||||
2026-03-20 02:04:20 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43422
|
2026-04-01 22:47:01 | FISH user=344531774518591498 fish=viidikas weight=80 value=5
|
||||||
2026-03-20 02:04:32 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=44422
|
2026-04-01 22:48:58 | WORK user=178852380018868224 earned=+106 lucky=True bal=26799
|
||||||
2026-03-20 02:04:45 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=43422
|
2026-04-01 22:49:03 | CRIME_WIN user=178852380018868224 earned=+486 bal=27285
|
||||||
2026-03-20 02:04:57 | BLACKJACK user=340451525799182357 payout=+4000 net=+2000 bal=45422
|
2026-04-01 22:49:05 | BEG user=178852380018868224 earned=+76 jailed=False bal=27361
|
||||||
2026-03-20 02:05:17 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=46422
|
2026-04-01 22:52:25 | BEG user=401373976431165449 earned=+44 jailed=False bal=44
|
||||||
2026-03-20 02:09:33 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=47422
|
2026-04-01 22:52:29 | WORK user=401373976431165449 earned=+103 lucky=False bal=147
|
||||||
2026-03-20 02:09:46 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=48422
|
2026-04-01 22:52:39 | CRIME_FAIL user=401373976431165449 fine=-125 jailed=False bal=22
|
||||||
2026-03-20 02:10:00 | BLACKJACK user=340451525799182357 payout=+2000 net=+1000 bal=49422
|
2026-04-01 22:53:01 | FISH user=401373976431165449 fish=latikas weight=1217 value=40
|
||||||
2026-03-20 02:10:17 | BLACKJACK user=340451525799182357 payout=+0 net=-1000 bal=48422
|
2026-04-01 22:57:11 | SLOTS_PAIR user=344531774518591498 bet=10000 change=5000 bal=135989
|
||||||
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
|
|
||||||
|
|||||||
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