Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 338b32430d | |||
| 9f1a539501 | |||
| 425686ec40 |
@@ -1,35 +1,124 @@
|
|||||||
# ShaiBot
|
# ShaiBot
|
||||||
|
|
||||||
A modular **Discord.js** bot with a shared Cookie economy, interactive
|
A modular **Discord.js** bot built around a shared Cookie economy,
|
||||||
Crumb Saucer games, cooperative boss battles, raid planning, and
|
interactive Crumb Saucer games, player profiles, achievements, global
|
||||||
community interactions.
|
badges, cooperative boss battles, raid planning, and community
|
||||||
|
interactions.
|
||||||
|
|
||||||
> **Status:** Active development. Game balance and individual features
|
> **Status:** Active development. Features, achievements, and game
|
||||||
> may change.
|
> balance may change.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### 🍪 Cookie Economy
|
### 🍪 Cookie Economy
|
||||||
|
|
||||||
Cookies are ShaiBot's shared virtual currency. The economy supports
|
Cookies are ShaiBot's shared virtual currency. The economy supports
|
||||||
persistent balances, atomic balance checks, transfers, per-user
|
persistent balances, atomic balance checks, transfers, Mongo-backed
|
||||||
cooldowns, and a shared bot/house vault.
|
cooldowns, and a shared bot/house vault.
|
||||||
|
|
||||||
The bot itself acts as the general Cookie vault: gambling losses and
|
The bot itself acts as the general Cookie vault: gambling losses and
|
||||||
penalties can feed the vault, while wins, heists, and rewards can pay
|
penalties can feed the vault, while wins, heists, collection rewards,
|
||||||
Cookies back out.
|
and other systems can pay Cookies back out.
|
||||||
|
|
||||||
|
Cookie collection is shared between `/cookies collect` and the **Crumb
|
||||||
|
Saucer Cookie Bakery**, using the same cooldown and reward logic.
|
||||||
|
|
||||||
|
### 👤 Player Profiles
|
||||||
|
|
||||||
|
ShaiBot includes persistent player profiles that bring together Cookie
|
||||||
|
and game activity.
|
||||||
|
|
||||||
|
Profiles can expose:
|
||||||
|
|
||||||
|
- Cookie economy statistics
|
||||||
|
- Per-game statistics
|
||||||
|
- Lifetime gains and losses
|
||||||
|
- Stealing and giving activity
|
||||||
|
- Achievements
|
||||||
|
- Global badges
|
||||||
|
- Collection progress
|
||||||
|
|
||||||
|
Profile menus are public so other members can view them, while
|
||||||
|
interactive controls are intended to remain restricted to the session
|
||||||
|
owner. Errors, cooldown/debug-style feedback, and unauthorized
|
||||||
|
interaction messages may be ephemeral.
|
||||||
|
|
||||||
|
### 🏆 Achievements
|
||||||
|
|
||||||
|
Achievements are repeatable across the community: every eligible player
|
||||||
|
can earn their own copy.
|
||||||
|
|
||||||
|
Achievement tracking can use persistent profile statistics as well as
|
||||||
|
explicit game events. Current and planned categories include:
|
||||||
|
|
||||||
|
- Cookie collection and economy milestones
|
||||||
|
- Giving and stealing milestones
|
||||||
|
- Game wins and losses
|
||||||
|
- Game-specific loss milestones
|
||||||
|
- All-In accomplishments
|
||||||
|
- Run It Back streaks
|
||||||
|
- Cross-game special challenges
|
||||||
|
- Collector achievements for earning a portion or all available
|
||||||
|
achievements
|
||||||
|
|
||||||
|
Game-specific achievements use names such as:
|
||||||
|
|
||||||
|
``` text
|
||||||
|
The House Always Wins: Blackjack
|
||||||
|
```
|
||||||
|
|
||||||
|
where sharing a generalized achievement concept between games makes
|
||||||
|
sense.
|
||||||
|
|
||||||
|
### 🎖️ Global Badges
|
||||||
|
|
||||||
|
Global badges are guild-wide collectibles that can have a single owner.
|
||||||
|
They are intended for special "first player to..." accomplishments and
|
||||||
|
other unique distinctions.
|
||||||
|
|
||||||
|
Examples include:
|
||||||
|
|
||||||
|
- First-time game accomplishments
|
||||||
|
- Slots triplet combinations
|
||||||
|
- All-In / Run It Back feats
|
||||||
|
- Special or hidden manually awarded badges
|
||||||
|
- Achievement collection milestones
|
||||||
|
|
||||||
|
Badges may be hidden until discovered. Some special badges are
|
||||||
|
intentionally managed manually in MongoDB rather than automatically
|
||||||
|
awarded by gameplay.
|
||||||
|
|
||||||
### 🎰 The Crumb Saucer
|
### 🎰 The Crumb Saucer
|
||||||
|
|
||||||
The **Crumb Saucer** is ShaiBot's Cookie-powered entertainment hub.
|
The **Crumb Saucer** is ShaiBot's Cookie-powered entertainment hub.
|
||||||
|
|
||||||
|
#### Cookie Bakery
|
||||||
|
|
||||||
|
Players can collect a fresh batch of Cookies from the Saucer.
|
||||||
|
|
||||||
|
The Bakery and `/cookies collect` use the same shared collection
|
||||||
|
utility, meaning they share:
|
||||||
|
|
||||||
|
- The same Mongo-backed cooldown
|
||||||
|
- The same reward range
|
||||||
|
- The same balance mutation
|
||||||
|
- The same profile/stat tracking
|
||||||
|
|
||||||
|
The current default collection range is **1--1000 Cookies**.
|
||||||
|
|
||||||
#### Slots
|
#### Slots
|
||||||
|
|
||||||
- Multiple Cookie wagers
|
- Multiple Cookie wagers
|
||||||
- Embed-based reel presentation
|
- Embed-based reel presentation
|
||||||
- Different symbol payouts and jackpots
|
- Different symbol payouts
|
||||||
- Replay support
|
- Triplet combinations
|
||||||
|
- Jackpot support
|
||||||
|
- Run It Back support
|
||||||
- Shared Cookie vault
|
- Shared Cookie vault
|
||||||
|
- Game-specific achievements and first-time global badges
|
||||||
|
|
||||||
|
Slots triplets can participate in unique global badge progression,
|
||||||
|
including Lemon, Cherry, Cookie, Star, and Diamond combinations.
|
||||||
|
|
||||||
#### Blackjack
|
#### Blackjack
|
||||||
|
|
||||||
@@ -37,15 +126,33 @@ The **Crumb Saucer** is ShaiBot's Cookie-powered entertainment hub.
|
|||||||
- Dealer stands on 17
|
- Dealer stands on 17
|
||||||
- Natural Blackjack pays 3:2
|
- Natural Blackjack pays 3:2
|
||||||
- Push returns the wager
|
- Push returns the wager
|
||||||
|
- All-In support
|
||||||
|
- Run It Back support
|
||||||
- Persistent active hands
|
- Persistent active hands
|
||||||
- Per-player session ownership
|
- Per-player session ownership
|
||||||
- Safe simultaneous sessions
|
- Safe simultaneous sessions
|
||||||
|
- Profile, achievement, and badge integration
|
||||||
|
|
||||||
|
All-In state should be derived from the committed wager and the player's
|
||||||
|
vault at the time the wager is made rather than solely from which button
|
||||||
|
was pressed.
|
||||||
|
|
||||||
|
Run It Back preserves immutable metadata from the previous hand. This
|
||||||
|
allows historical challenges to remain valid even if another player
|
||||||
|
changes the user's Cookie balance between rounds.
|
||||||
|
|
||||||
|
For example, an All-In that pushes can be followed by Run It Back and
|
||||||
|
still qualify for an All-In → Run It Back challenge even if Cookies were
|
||||||
|
gifted to the player in between.
|
||||||
|
|
||||||
|
Blackjack resolution is designed to guard against rapid repeated
|
||||||
|
interactions so a hand cannot be resolved and paid multiple times.
|
||||||
|
|
||||||
#### Roulette
|
#### Roulette
|
||||||
|
|
||||||
A compact European Roulette implementation using **0--36**.
|
A compact European Roulette implementation using **0--36**.
|
||||||
|
|
||||||
Available bets:
|
Available bets include:
|
||||||
|
|
||||||
- 🔴 Red
|
- 🔴 Red
|
||||||
- ⚫ Black
|
- ⚫ Black
|
||||||
@@ -57,12 +164,74 @@ Roulette uses repeated edits of the same embed for a
|
|||||||
rate-limit-conscious pseudo-animation. Zero loses on Red, Black, Odd,
|
rate-limit-conscious pseudo-animation. Zero loses on Red, Black, Odd,
|
||||||
and Even.
|
and Even.
|
||||||
|
|
||||||
|
Roulette supports All-In and Run It Back tracking for profile
|
||||||
|
achievements and special accomplishments.
|
||||||
|
|
||||||
#### Vault Heist
|
#### Vault Heist
|
||||||
|
|
||||||
Players can attempt to steal directly from ShaiBot's Cookie vault using
|
Players can attempt to steal directly from ShaiBot's Cookie vault using
|
||||||
different risk/reward approaches such as **Sneak**, **Hack**, and
|
different risk/reward approaches such as **Sneak**, **Hack**, and
|
||||||
**Smash**. Heist actions share a per-player cooldown so changing
|
**Smash**.
|
||||||
approach does not bypass it.
|
|
||||||
|
Heist actions share a per-player cooldown so changing approach does not
|
||||||
|
bypass it.
|
||||||
|
|
||||||
|
Heist rewards and fines use percentage-based mechanics with hard caps so
|
||||||
|
the system remains meaningful for wealthy players without allowing
|
||||||
|
extreme balances to produce unreasonable penalties.
|
||||||
|
|
||||||
|
### 🔁 Run It Back
|
||||||
|
|
||||||
|
Supported Crumb Saucer games can track consecutive use of **Run It
|
||||||
|
Back**.
|
||||||
|
|
||||||
|
The streak represents the player's choice to immediately replay through
|
||||||
|
the Run It Back action; wins, losses, pushes, jackpots, or other
|
||||||
|
outcomes do not inherently break the streak.
|
||||||
|
|
||||||
|
Current achievement concepts include using Run It Back five consecutive
|
||||||
|
times in supported games:
|
||||||
|
|
||||||
|
``` text
|
||||||
|
One More Spin: Slots
|
||||||
|
One More Bet: Roulette
|
||||||
|
One More Hand: Blackjack
|
||||||
|
```
|
||||||
|
|
||||||
|
A normal/new wager breaks that game's Run It Back chain.
|
||||||
|
|
||||||
|
### ♾️ I Can Do This All Day
|
||||||
|
|
||||||
|
A special cross-game accomplishment tracks the sequence:
|
||||||
|
|
||||||
|
``` text
|
||||||
|
All-In
|
||||||
|
→ round resolves
|
||||||
|
→ Run It Back with the same committed wager
|
||||||
|
```
|
||||||
|
|
||||||
|
The original All-In is determined from immutable wager-time data. The
|
||||||
|
Run It Back relationship is determined from the previous game's stored
|
||||||
|
bet and All-In state rather than relying on the player's live vault
|
||||||
|
remaining unchanged.
|
||||||
|
|
||||||
|
This prevents another player gifting Cookies between rounds from
|
||||||
|
invalidating a legitimate sequence.
|
||||||
|
|
||||||
|
The accomplishment can exist both as a normal achievement available to
|
||||||
|
everyone and as a global first-time badge for the first player to
|
||||||
|
perform it.
|
||||||
|
|
||||||
|
### 🏴 Cookie Stealing
|
||||||
|
|
||||||
|
Normal player-to-player stealing is separate from Vault Heist.
|
||||||
|
|
||||||
|
Steal difficulty can account for attacker and target wealth. Successful
|
||||||
|
steals use randomized percentages with hard caps, while failed attempts
|
||||||
|
can apply a percentage-based penalty.
|
||||||
|
|
||||||
|
Profile tracking records relevant Cookie gains/losses and stealing
|
||||||
|
activity.
|
||||||
|
|
||||||
### ⚔️ Crumble Strike
|
### ⚔️ Crumble Strike
|
||||||
|
|
||||||
@@ -90,13 +259,6 @@ Roles are **DPS**, **Tank**, and **Healer**, with role-specific
|
|||||||
proficiencies. The general progression theme includes **Sprout →
|
proficiencies. The general progression theme includes **Sprout →
|
||||||
Adventurer → Warrior of Light**.
|
Adventurer → Warrior of Light**.
|
||||||
|
|
||||||
### 🏴 Cookie Stealing
|
|
||||||
|
|
||||||
Normal player-to-player stealing is separate from Vault Heist. Steal
|
|
||||||
difficulty can account for attacker and target wealth, successful steals
|
|
||||||
can use a randomized percentage with a cap, and failed attempts can
|
|
||||||
apply a fee that feeds the bot vault.
|
|
||||||
|
|
||||||
### 📅 Raid Planner
|
### 📅 Raid Planner
|
||||||
|
|
||||||
Raid planning uses Discord embeds and reactions. Participation can be
|
Raid planning uses Discord embeds and reactions. Participation can be
|
||||||
@@ -117,7 +279,8 @@ A typical layout:
|
|||||||
ShaiBot/
|
ShaiBot/
|
||||||
├── buttons/
|
├── buttons/
|
||||||
│ ├── crumbSaucer/
|
│ ├── crumbSaucer/
|
||||||
│ └── crumbleStrike/
|
│ ├── crumbleStrike/
|
||||||
|
│ └── profile/
|
||||||
├── commands/
|
├── commands/
|
||||||
│ └── applications/
|
│ └── applications/
|
||||||
├── events/
|
├── events/
|
||||||
@@ -125,8 +288,12 @@ ShaiBot/
|
|||||||
├── modals/
|
├── modals/
|
||||||
├── utils/
|
├── utils/
|
||||||
│ ├── cookieEconomy.js
|
│ ├── cookieEconomy.js
|
||||||
|
│ ├── crumbCollect.js
|
||||||
|
│ ├── crumbSlots.js
|
||||||
│ ├── crumbBlackjack.js
|
│ ├── crumbBlackjack.js
|
||||||
│ ├── crumbRoulette.js
|
│ ├── crumbRoulette.js
|
||||||
|
│ ├── cookieHeist.js
|
||||||
|
│ ├── profileSystem.js
|
||||||
│ └── crumbleStrike.js
|
│ └── crumbleStrike.js
|
||||||
├── index.js
|
├── index.js
|
||||||
├── package.json
|
├── package.json
|
||||||
@@ -144,6 +311,40 @@ client.selectMenus = new Collection();
|
|||||||
client.modals = new Collection();
|
client.modals = new Collection();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Shared Utility Architecture
|
||||||
|
|
||||||
|
Game/application files should remain thin wherever practical.
|
||||||
|
|
||||||
|
Shared behavior belongs in `utils/`, particularly:
|
||||||
|
|
||||||
|
- `cookieEconomy.js` --- balances, transfers, cooldowns, and economy
|
||||||
|
primitives
|
||||||
|
- `crumbCollect.js` --- authoritative Cookie collection behavior
|
||||||
|
- `crumbSlots.js` --- Slots state and resolution
|
||||||
|
- `crumbBlackjack.js` --- Blackjack state and resolution
|
||||||
|
- `crumbRoulette.js` --- Roulette state and resolution
|
||||||
|
- `cookieHeist.js` --- Vault Heist behavior
|
||||||
|
- `profileSystem.js` --- profile statistics, achievements, badges,
|
||||||
|
rendering, and audit helpers
|
||||||
|
|
||||||
|
Application commands and button handlers should call these utilities
|
||||||
|
rather than independently reimplementing economy or progression logic.
|
||||||
|
|
||||||
|
This is especially important for features reachable from multiple
|
||||||
|
interfaces. For example:
|
||||||
|
|
||||||
|
``` text
|
||||||
|
/cookies collect
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
crumbCollect.js
|
||||||
|
▲
|
||||||
|
│
|
||||||
|
Crumb Saucer Bakery
|
||||||
|
```
|
||||||
|
|
||||||
|
Both interfaces therefore share one authoritative implementation.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Node.js
|
- Node.js
|
||||||
@@ -152,15 +353,14 @@ client.modals = new Collection();
|
|||||||
- A Discord bot application/token
|
- A Discord bot application/token
|
||||||
- `dotenv`
|
- `dotenv`
|
||||||
|
|
||||||
Use the Node.js version required by the dependencies in your
|
Use the Node.js version required by the dependencies in `package.json`.
|
||||||
`package.json`.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Clone the repository:
|
Clone the repository:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
git clone https://github.com/YOUR_USERNAME/ShaiBot.git
|
git clone https://git.desq-gaming.de/Shaiwase/ShaiBot.git
|
||||||
cd ShaiBot
|
cd ShaiBot
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -210,7 +410,11 @@ M_URI=mongodb://USER:PASSWORD@HOST:PORT/DATABASE?authSource=admin&retryWrites=fa
|
|||||||
|
|
||||||
Never commit database credentials.
|
Never commit database credentials.
|
||||||
|
|
||||||
Important economy collections include:
|
Important collections include Cookie balances, cooldowns, persistent
|
||||||
|
game state, profile statistics, achievements, and global badge
|
||||||
|
definitions/ownership.
|
||||||
|
|
||||||
|
Core economy collections include:
|
||||||
|
|
||||||
``` text
|
``` text
|
||||||
items_cookies
|
items_cookies
|
||||||
@@ -221,6 +425,10 @@ items_games
|
|||||||
`items_cookies` stores balances, `cooldown_cookies` stores action
|
`items_cookies` stores balances, `cooldown_cookies` stores action
|
||||||
cooldown timestamps, and `items_games` can store persistent game state.
|
cooldown timestamps, and `items_games` can store persistent game state.
|
||||||
|
|
||||||
|
Profile-related collection names should be treated as part of the
|
||||||
|
profile utility's implementation contract rather than duplicated
|
||||||
|
throughout command/button modules.
|
||||||
|
|
||||||
### Recommended Indexes
|
### Recommended Indexes
|
||||||
|
|
||||||
Run the shared index initializer after MongoDB connects:
|
Run the shared index initializer after MongoDB connects:
|
||||||
@@ -234,13 +442,15 @@ await mClient.connect();
|
|||||||
await ensureCookieIndexes();
|
await ensureCookieIndexes();
|
||||||
```
|
```
|
||||||
|
|
||||||
The intended unique keys are:
|
The intended unique keys include:
|
||||||
|
|
||||||
``` text
|
``` text
|
||||||
items_cookies → userId
|
items_cookies → userId
|
||||||
cooldown_cookies → userId + guildId
|
cooldown_cookies → userId + guildId
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Profile and badge utilities may establish their own required indexes.
|
||||||
|
|
||||||
## Discord Intents
|
## Discord Intents
|
||||||
|
|
||||||
The current client uses intents including:
|
The current client uses intents including:
|
||||||
@@ -272,14 +482,20 @@ Enable any required privileged intents in the Discord Developer Portal.
|
|||||||
|
|
||||||
ShaiBot generally follows these conventions:
|
ShaiBot generally follows these conventions:
|
||||||
|
|
||||||
- Public game output uses **embeds**.
|
- Normal user-facing messages, achievements, menus, and game output
|
||||||
- Short private errors/feedback can use **ephemeral messages**.
|
are **public by default**.
|
||||||
|
- Errors, unauthorized interaction feedback, cooldown/debug-style
|
||||||
|
notices, and similar operational messages may be **ephemeral**.
|
||||||
- Games use **buttons** for interaction.
|
- Games use **buttons** for interaction.
|
||||||
- Existing messages are edited where practical instead of repeatedly
|
- Existing messages are edited where practical instead of repeatedly
|
||||||
sending new messages.
|
sending new messages.
|
||||||
- Game/session ownership is preserved so multiple users can play
|
- Game/session ownership is preserved so multiple users can play
|
||||||
independently.
|
independently.
|
||||||
|
- Public menus may be visible to everyone without allowing other users
|
||||||
|
to control the owner's session.
|
||||||
- Animation-like effects use rate-limit-conscious message edits.
|
- Animation-like effects use rate-limit-conscious message edits.
|
||||||
|
- Embed state should not be treated as the only authoritative source
|
||||||
|
for critical economy/game resolution data.
|
||||||
|
|
||||||
## Economy Safety
|
## Economy Safety
|
||||||
|
|
||||||
@@ -295,6 +511,14 @@ failures.
|
|||||||
When adding games, check that the house vault can cover the maximum
|
When adding games, check that the house vault can cover the maximum
|
||||||
possible payout before accepting a wager.
|
possible payout before accepting a wager.
|
||||||
|
|
||||||
|
Game resolution should be protected against rapid repeated interactions.
|
||||||
|
A result must never be paid twice simply because two button interactions
|
||||||
|
reached the server close together.
|
||||||
|
|
||||||
|
Historical game facts such as wager amount, vault-at-wager, All-In
|
||||||
|
state, and replay origin should be persisted when needed rather than
|
||||||
|
reconstructed later from a player's live balance.
|
||||||
|
|
||||||
## Adding a Crumb Saucer Game
|
## Adding a Crumb Saucer Game
|
||||||
|
|
||||||
Preferred pattern:
|
Preferred pattern:
|
||||||
@@ -304,31 +528,80 @@ Preferred pattern:
|
|||||||
directory.
|
directory.
|
||||||
3. Reuse `cookieEconomy.js` rather than implementing separate balance
|
3. Reuse `cookieEconomy.js` rather than implementing separate balance
|
||||||
logic.
|
logic.
|
||||||
4. Preserve the player/session owner through every screen.
|
4. Reuse `profileSystem.js` for statistics, achievements, and badges.
|
||||||
5. Check house liquidity before accepting a wager.
|
5. Preserve the player/session owner through every screen.
|
||||||
6. Prefer editing the current embed for animation/state transitions.
|
6. Check house liquidity before accepting a wager.
|
||||||
7. Protect wager resolution against rapid repeated interactions.
|
7. Prefer editing the current embed for animation/state transitions.
|
||||||
|
8. Protect wager resolution against rapid repeated interactions.
|
||||||
|
9. Persist immutable wager metadata required by achievements or replay
|
||||||
|
behavior.
|
||||||
|
10. Keep application/button handlers thin and avoid duplicating shared
|
||||||
|
rules.
|
||||||
|
|
||||||
|
## Achievement and Badge Design
|
||||||
|
|
||||||
|
Achievements and global badges serve different purposes:
|
||||||
|
|
||||||
|
**Achievements** are personal progression. Multiple players can earn the
|
||||||
|
same achievement.
|
||||||
|
|
||||||
|
**Global badges** are unique guild-wide distinctions and may have a
|
||||||
|
single owner.
|
||||||
|
|
||||||
|
When adding progression:
|
||||||
|
|
||||||
|
- Prefer stat-backed achievements for durable numeric milestones.
|
||||||
|
- Use explicit event tracking when the accomplishment depends on a
|
||||||
|
particular sequence or action.
|
||||||
|
- Keep generalized achievement naming consistent between games where
|
||||||
|
appropriate.
|
||||||
|
- Do not make collector completion depend on achievements that cannot
|
||||||
|
actually be obtained.
|
||||||
|
- Use atomic badge claiming so two players cannot both receive a
|
||||||
|
guild-first badge.
|
||||||
|
- Hidden/manual badges do not need gameplay evaluators if they are
|
||||||
|
intentionally awarded directly in MongoDB.
|
||||||
|
|
||||||
|
## Administration and Auditing
|
||||||
|
|
||||||
|
The profile system includes administrative/audit concepts for validating
|
||||||
|
profile progression data and expected badge definitions.
|
||||||
|
|
||||||
|
When changing achievement or badge schemas:
|
||||||
|
|
||||||
|
- Keep existing IDs stable once players may have unlocked them.
|
||||||
|
- Prefer adding new IDs over renaming historical IDs.
|
||||||
|
- Treat legacy/disabled definitions explicitly.
|
||||||
|
- Audit missing or malformed definitions after database changes.
|
||||||
|
- Avoid hardcoding optional manually managed badges as mandatory
|
||||||
|
configuration unless they are intended to exist in every guild.
|
||||||
|
|
||||||
## Roadmap / TODO
|
## Roadmap / TODO
|
||||||
|
|
||||||
The major planned Cookie systems are implemented or designed. Remaining
|
ShaiBot is currently focused on testing, balancing, hardening, and
|
||||||
work is primarily testing, balancing, and hardening.
|
expanding progression.
|
||||||
|
|
||||||
- [ ] Verify Slots replay behavior after deployment
|
Near-term work includes:
|
||||||
- [ ] Deploy and test limited Roulette
|
|
||||||
|
- [ ] Continue stress-testing simultaneous gambling interactions
|
||||||
|
- [ ] Continue testing repeated/double button presses
|
||||||
|
- [ ] Test insufficient-balance races
|
||||||
|
- [ ] Test behavior with a nearly empty house vault
|
||||||
|
- [ ] Validate All-In and Run It Back achievement edge cases
|
||||||
|
- [ ] Validate Run It Back streak progression across supported games
|
||||||
|
- [ ] Validate Slots triplet badge claiming
|
||||||
|
- [ ] Continue balancing Vault Heist rewards and fines
|
||||||
|
- [ ] Add Bankruptcy / starter recovery behavior
|
||||||
|
- [ ] Continue centralizing persistent interaction/session ownership
|
||||||
|
- [ ] Consider an economy audit/log collection
|
||||||
- [ ] Verify Crumble Strike AoE Revive behavior
|
- [ ] Verify Crumble Strike AoE Revive behavior
|
||||||
- [ ] Verify Crumble Strike expiration/failure cleanup survives bot
|
- [ ] Verify Crumble Strike expiration/failure cleanup survives bot
|
||||||
restarts
|
restarts
|
||||||
- [ ] Balance boss HP, damage, healing, Energy, Limit generation,
|
- [ ] Balance Crumble Strike boss HP, damage, healing, Energy, Limit
|
||||||
entry costs, and rewards
|
generation, entry costs, and rewards
|
||||||
- [ ] Stress-test simultaneous gambling interactions
|
|
||||||
- [ ] Stress-test repeated/double button presses
|
|
||||||
- [ ] Test insufficient-balance races
|
|
||||||
- [ ] Test behavior with a nearly empty house vault
|
|
||||||
- [ ] Consider dynamic Vault Heist payout caps
|
|
||||||
- [ ] Consider an economy audit/log collection
|
|
||||||
- [ ] Add more Crumble Strike bosses, lore, and encounter variety
|
- [ ] Add more Crumble Strike bosses, lore, and encounter variety
|
||||||
- [ ] Add more Crumb Saucer attractions
|
- [ ] Add more achievements, hidden badges, and Crumb Saucer
|
||||||
|
attractions
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
@@ -343,6 +616,9 @@ node_modules/
|
|||||||
If a Discord token or MongoDB credential is accidentally committed,
|
If a Discord token or MongoDB credential is accidentally committed,
|
||||||
rotate it immediately.
|
rotate it immediately.
|
||||||
|
|
||||||
|
Do not expose administrative secrets or database credentials through
|
||||||
|
embeds, logs intended for public channels, or repository commits.
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
ShaiBot Cookies are fictional in-bot currency with no real-world
|
ShaiBot Cookies are fictional in-bot currency with no real-world
|
||||||
@@ -360,11 +636,15 @@ Bug reports, balance suggestions, and contributions are welcome.
|
|||||||
When contributing:
|
When contributing:
|
||||||
|
|
||||||
- Keep commands and button handlers modular.
|
- Keep commands and button handlers modular.
|
||||||
- Reuse shared economy utilities.
|
- Reuse shared economy and profile utilities.
|
||||||
- Avoid blocking the event loop.
|
- Avoid blocking the event loop.
|
||||||
- Be mindful of Discord API rate limits.
|
- Be mindful of Discord API rate limits.
|
||||||
- Validate interaction ownership.
|
- Validate interaction ownership.
|
||||||
- Keep database operations safe under concurrent interactions.
|
- Keep database operations safe under concurrent interactions.
|
||||||
|
- Preserve existing achievement/badge IDs where users may already own
|
||||||
|
them.
|
||||||
|
- Avoid introducing a second implementation of behavior that already
|
||||||
|
has a shared utility.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,54 @@
|
|||||||
const {
|
const {
|
||||||
requireProfileOwner,
|
|
||||||
buildAchievementsEmbed,
|
buildAchievementsEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
buildProfilePaginationButtons
|
getProfileContextFromFooter,
|
||||||
} = require("../../utils/profileSystem");
|
requireProfileOwner
|
||||||
|
} = require(
|
||||||
|
"../../utils/profileSystem"
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "profileAchievements",
|
name:
|
||||||
|
"profileAchievements",
|
||||||
|
|
||||||
async execute(interaction) {
|
async execute(
|
||||||
const session = await requireProfileOwner(
|
interaction
|
||||||
|
) {
|
||||||
|
const context =
|
||||||
|
await requireProfileOwner(
|
||||||
interaction
|
interaction
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!session) {
|
if (!context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await interaction.client.users.fetch(
|
const user =
|
||||||
session.profileUserId
|
await interaction.client.users.fetch(
|
||||||
|
context.profileUserId
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await buildAchievementsEmbed(
|
const embed =
|
||||||
|
await buildAchievementsEmbed(
|
||||||
interaction.guildId,
|
interaction.guildId,
|
||||||
user,
|
user,
|
||||||
0,
|
context.sessionOwnerId,
|
||||||
session.sessionOwnerId
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
const components = [
|
const pageContext =
|
||||||
...buildProfileButtons()
|
getProfileContextFromFooter(
|
||||||
];
|
embed.data.footer?.text
|
||||||
|
|
||||||
if (result.pageCount > 1) {
|
|
||||||
components.push(
|
|
||||||
buildProfilePaginationButtons(
|
|
||||||
"profileAchievementsLeft",
|
|
||||||
"profileAchievementsRight",
|
|
||||||
result.page,
|
|
||||||
result.pageCount
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [result.embed],
|
embeds: [embed],
|
||||||
components
|
components:
|
||||||
|
buildProfileButtons(
|
||||||
|
"achievements",
|
||||||
|
pageContext?.page ?? 0,
|
||||||
|
pageContext?.pageCount ?? 1
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,53 +1,54 @@
|
|||||||
// buttons/profile/achievementsLeft.js
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
requireProfileOwner,
|
|
||||||
buildAchievementsEmbed,
|
buildAchievementsEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
buildProfilePaginationButtons
|
getProfileContextFromFooter,
|
||||||
} = require("../../utils/profileSystem");
|
requireProfileOwner
|
||||||
|
} = require(
|
||||||
|
"../../utils/profileSystem"
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "profileAchievementsLeft",
|
name:
|
||||||
|
"profileAchievementsLeft",
|
||||||
|
|
||||||
async execute(interaction) {
|
async execute(
|
||||||
const session = await requireProfileOwner(
|
interaction
|
||||||
|
) {
|
||||||
|
const context =
|
||||||
|
await requireProfileOwner(
|
||||||
interaction
|
interaction
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!session) {
|
if (!context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await interaction.client.users.fetch(
|
const user =
|
||||||
session.profileUserId
|
await interaction.client.users.fetch(
|
||||||
|
context.profileUserId
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await buildAchievementsEmbed(
|
const embed =
|
||||||
|
await buildAchievementsEmbed(
|
||||||
interaction.guildId,
|
interaction.guildId,
|
||||||
user,
|
user,
|
||||||
session.page - 1,
|
context.sessionOwnerId,
|
||||||
session.sessionOwnerId
|
context.page - 1
|
||||||
);
|
);
|
||||||
|
|
||||||
const components = [
|
const pageContext =
|
||||||
...buildProfileButtons()
|
getProfileContextFromFooter(
|
||||||
];
|
embed.data.footer?.text
|
||||||
|
|
||||||
if (result.pageCount > 1) {
|
|
||||||
components.push(
|
|
||||||
buildProfilePaginationButtons(
|
|
||||||
"profileAchievementsLeft",
|
|
||||||
"profileAchievementsRight",
|
|
||||||
result.page,
|
|
||||||
result.pageCount
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [result.embed],
|
embeds: [embed],
|
||||||
components
|
components:
|
||||||
|
buildProfileButtons(
|
||||||
|
"achievements",
|
||||||
|
pageContext?.page ?? 0,
|
||||||
|
pageContext?.pageCount ?? 1
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,53 +1,54 @@
|
|||||||
// buttons/profile/achievementsRight.js
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
requireProfileOwner,
|
|
||||||
buildAchievementsEmbed,
|
buildAchievementsEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
buildProfilePaginationButtons
|
getProfileContextFromFooter,
|
||||||
} = require("../../utils/profileSystem");
|
requireProfileOwner
|
||||||
|
} = require(
|
||||||
|
"../../utils/profileSystem"
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "profileAchievementsRight",
|
name:
|
||||||
|
"profileAchievementsRight",
|
||||||
|
|
||||||
async execute(interaction) {
|
async execute(
|
||||||
const session = await requireProfileOwner(
|
interaction
|
||||||
|
) {
|
||||||
|
const context =
|
||||||
|
await requireProfileOwner(
|
||||||
interaction
|
interaction
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!session) {
|
if (!context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await interaction.client.users.fetch(
|
const user =
|
||||||
session.profileUserId
|
await interaction.client.users.fetch(
|
||||||
|
context.profileUserId
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await buildAchievementsEmbed(
|
const embed =
|
||||||
|
await buildAchievementsEmbed(
|
||||||
interaction.guildId,
|
interaction.guildId,
|
||||||
user,
|
user,
|
||||||
session.page + 1,
|
context.sessionOwnerId,
|
||||||
session.sessionOwnerId
|
context.page + 1
|
||||||
);
|
);
|
||||||
|
|
||||||
const components = [
|
const pageContext =
|
||||||
...buildProfileButtons()
|
getProfileContextFromFooter(
|
||||||
];
|
embed.data.footer?.text
|
||||||
|
|
||||||
if (result.pageCount > 1) {
|
|
||||||
components.push(
|
|
||||||
buildProfilePaginationButtons(
|
|
||||||
"profileAchievementsLeft",
|
|
||||||
"profileAchievementsRight",
|
|
||||||
result.page,
|
|
||||||
result.pageCount
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [result.embed],
|
embeds: [embed],
|
||||||
components
|
components:
|
||||||
|
buildProfileButtons(
|
||||||
|
"achievements",
|
||||||
|
pageContext?.page ?? 0,
|
||||||
|
pageContext?.pageCount ?? 1
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+30
-27
@@ -1,51 +1,54 @@
|
|||||||
const {
|
const {
|
||||||
requireProfileOwner,
|
|
||||||
buildBadgesEmbed,
|
buildBadgesEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
buildProfilePaginationButtons
|
getProfileContextFromFooter,
|
||||||
} = require("../../utils/profileSystem");
|
requireProfileOwner
|
||||||
|
} = require(
|
||||||
|
"../../utils/profileSystem"
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "profileBadges",
|
name:
|
||||||
|
"profileBadges",
|
||||||
|
|
||||||
async execute(interaction) {
|
async execute(
|
||||||
const session = await requireProfileOwner(
|
interaction
|
||||||
|
) {
|
||||||
|
const context =
|
||||||
|
await requireProfileOwner(
|
||||||
interaction
|
interaction
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!session) {
|
if (!context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await interaction.client.users.fetch(
|
const user =
|
||||||
session.profileUserId
|
await interaction.client.users.fetch(
|
||||||
|
context.profileUserId
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await buildBadgesEmbed(
|
const embed =
|
||||||
|
await buildBadgesEmbed(
|
||||||
interaction.guildId,
|
interaction.guildId,
|
||||||
user,
|
user,
|
||||||
0,
|
context.sessionOwnerId,
|
||||||
session.sessionOwnerId
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
const components = [
|
const pageContext =
|
||||||
...buildProfileButtons()
|
getProfileContextFromFooter(
|
||||||
];
|
embed.data.footer?.text
|
||||||
|
|
||||||
if (result.pageCount > 1) {
|
|
||||||
components.push(
|
|
||||||
buildProfilePaginationButtons(
|
|
||||||
"profileBadgesLeft",
|
|
||||||
"profileBadgesRight",
|
|
||||||
result.page,
|
|
||||||
result.pageCount
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [result.embed],
|
embeds: [embed],
|
||||||
components
|
components:
|
||||||
|
buildProfileButtons(
|
||||||
|
"badges",
|
||||||
|
pageContext?.page ?? 0,
|
||||||
|
pageContext?.pageCount ?? 1
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,53 +1,54 @@
|
|||||||
// buttons/profile/badgesLeft.js
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
requireProfileOwner,
|
|
||||||
buildBadgesEmbed,
|
buildBadgesEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
buildProfilePaginationButtons
|
getProfileContextFromFooter,
|
||||||
} = require("../../utils/profileSystem");
|
requireProfileOwner
|
||||||
|
} = require(
|
||||||
|
"../../utils/profileSystem"
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "profileBadgesLeft",
|
name:
|
||||||
|
"profileBadgesLeft",
|
||||||
|
|
||||||
async execute(interaction) {
|
async execute(
|
||||||
const session = await requireProfileOwner(
|
interaction
|
||||||
|
) {
|
||||||
|
const context =
|
||||||
|
await requireProfileOwner(
|
||||||
interaction
|
interaction
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!session) {
|
if (!context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await interaction.client.users.fetch(
|
const user =
|
||||||
session.profileUserId
|
await interaction.client.users.fetch(
|
||||||
|
context.profileUserId
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await buildBadgesEmbed(
|
const embed =
|
||||||
|
await buildBadgesEmbed(
|
||||||
interaction.guildId,
|
interaction.guildId,
|
||||||
user,
|
user,
|
||||||
session.page - 1,
|
context.sessionOwnerId,
|
||||||
session.sessionOwnerId
|
context.page - 1
|
||||||
);
|
);
|
||||||
|
|
||||||
const components = [
|
const pageContext =
|
||||||
...buildProfileButtons()
|
getProfileContextFromFooter(
|
||||||
];
|
embed.data.footer?.text
|
||||||
|
|
||||||
if (result.pageCount > 1) {
|
|
||||||
components.push(
|
|
||||||
buildProfilePaginationButtons(
|
|
||||||
"profileBadgesLeft",
|
|
||||||
"profileBadgesRight",
|
|
||||||
result.page,
|
|
||||||
result.pageCount
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [result.embed],
|
embeds: [embed],
|
||||||
components
|
components:
|
||||||
|
buildProfileButtons(
|
||||||
|
"badges",
|
||||||
|
pageContext?.page ?? 0,
|
||||||
|
pageContext?.pageCount ?? 1
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,53 +1,54 @@
|
|||||||
// buttons/profile/badgesRight.js
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
requireProfileOwner,
|
|
||||||
buildBadgesEmbed,
|
buildBadgesEmbed,
|
||||||
buildProfileButtons,
|
buildProfileButtons,
|
||||||
buildProfilePaginationButtons
|
getProfileContextFromFooter,
|
||||||
} = require("../../utils/profileSystem");
|
requireProfileOwner
|
||||||
|
} = require(
|
||||||
|
"../../utils/profileSystem"
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "profileBadgesRight",
|
name:
|
||||||
|
"profileBadgesRight",
|
||||||
|
|
||||||
async execute(interaction) {
|
async execute(
|
||||||
const session = await requireProfileOwner(
|
interaction
|
||||||
|
) {
|
||||||
|
const context =
|
||||||
|
await requireProfileOwner(
|
||||||
interaction
|
interaction
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!session) {
|
if (!context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await interaction.client.users.fetch(
|
const user =
|
||||||
session.profileUserId
|
await interaction.client.users.fetch(
|
||||||
|
context.profileUserId
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await buildBadgesEmbed(
|
const embed =
|
||||||
|
await buildBadgesEmbed(
|
||||||
interaction.guildId,
|
interaction.guildId,
|
||||||
user,
|
user,
|
||||||
session.page + 1,
|
context.sessionOwnerId,
|
||||||
session.sessionOwnerId
|
context.page + 1
|
||||||
);
|
);
|
||||||
|
|
||||||
const components = [
|
const pageContext =
|
||||||
...buildProfileButtons()
|
getProfileContextFromFooter(
|
||||||
];
|
embed.data.footer?.text
|
||||||
|
|
||||||
if (result.pageCount > 1) {
|
|
||||||
components.push(
|
|
||||||
buildProfilePaginationButtons(
|
|
||||||
"profileBadgesLeft",
|
|
||||||
"profileBadgesRight",
|
|
||||||
result.page,
|
|
||||||
result.pageCount
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [result.embed],
|
embeds: [embed],
|
||||||
components
|
components:
|
||||||
|
buildProfileButtons(
|
||||||
|
"badges",
|
||||||
|
pageContext?.page ?? 0,
|
||||||
|
pageContext?.pageCount ?? 1
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+26
-13
@@ -1,34 +1,47 @@
|
|||||||
const {
|
const {
|
||||||
requireProfileOwner,
|
buildStatsEmbed,
|
||||||
buildProfileStatsEmbed,
|
buildProfileButtons,
|
||||||
buildProfileButtons
|
requireProfileOwner
|
||||||
} = require("../../utils/profileSystem");
|
} = require(
|
||||||
|
"../../utils/profileSystem"
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: "profileStats",
|
name:
|
||||||
|
"profileStats",
|
||||||
|
|
||||||
async execute(interaction) {
|
async execute(
|
||||||
const session = await requireProfileOwner(
|
interaction
|
||||||
|
) {
|
||||||
|
const context =
|
||||||
|
await requireProfileOwner(
|
||||||
interaction
|
interaction
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!session) {
|
if (!context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await interaction.client.users.fetch(
|
const user =
|
||||||
session.profileUserId
|
await interaction.client.users.fetch(
|
||||||
|
context.profileUserId
|
||||||
);
|
);
|
||||||
|
|
||||||
const embed = await buildProfileStatsEmbed(
|
const embed =
|
||||||
|
await buildStatsEmbed(
|
||||||
interaction.guildId,
|
interaction.guildId,
|
||||||
user,
|
user,
|
||||||
session.sessionOwnerId
|
context.sessionOwnerId
|
||||||
);
|
);
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [embed],
|
embeds: [embed],
|
||||||
components: buildProfileButtons()
|
components:
|
||||||
|
buildProfileButtons(
|
||||||
|
"stats",
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,46 +1,63 @@
|
|||||||
const { SlashCommandBuilder } = require("discord.js");
|
const {
|
||||||
|
SlashCommandBuilder
|
||||||
|
} = require(
|
||||||
|
"discord.js"
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
buildProfileOverview,
|
buildProfileOverview,
|
||||||
buildProfileButtons
|
buildProfileButtons
|
||||||
} = require("../../utils/profileSystem");
|
} = require(
|
||||||
const {
|
"../../utils/profileSystem"
|
||||||
createInteractionSession
|
);
|
||||||
} = require("../../utils/interactionSession");
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
data: new SlashCommandBuilder()
|
data:
|
||||||
.setName("profile")
|
new SlashCommandBuilder()
|
||||||
.setDescription("View a player profile")
|
.setName(
|
||||||
.addUserOption(option =>
|
"profile"
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
"View a player profile"
|
||||||
|
)
|
||||||
|
.addUserOption(
|
||||||
|
option =>
|
||||||
option
|
option
|
||||||
.setName("user")
|
.setName(
|
||||||
.setDescription("Player to view")
|
"user"
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
"Player to view"
|
||||||
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
async execute(interaction) {
|
async execute(
|
||||||
const user = interaction.options.getUser("user") ?? interaction.user;
|
interaction
|
||||||
|
) {
|
||||||
|
const user =
|
||||||
|
interaction.options.getUser(
|
||||||
|
"user"
|
||||||
|
) ??
|
||||||
|
interaction.user;
|
||||||
|
|
||||||
const embed = await buildProfileOverview(
|
const sessionOwnerId =
|
||||||
|
interaction.user.id;
|
||||||
|
|
||||||
|
const embed =
|
||||||
|
await buildProfileOverview(
|
||||||
interaction.guildId,
|
interaction.guildId,
|
||||||
user,
|
user,
|
||||||
interaction.user.id
|
sessionOwnerId
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.reply({
|
return interaction.reply({
|
||||||
embeds: [embed],
|
embeds: [embed],
|
||||||
components: buildProfileButtons()
|
components:
|
||||||
|
buildProfileButtons(
|
||||||
|
"overview",
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const message = await interaction.fetchReply();
|
|
||||||
|
|
||||||
await createInteractionSession(
|
|
||||||
interaction,
|
|
||||||
{
|
|
||||||
messageId: message.id,
|
|
||||||
ownerId: interaction.user.id,
|
|
||||||
kind: "profile",
|
|
||||||
targetId: user.id
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,415 +1,263 @@
|
|||||||
const {
|
const {
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
MessageFlags
|
PermissionFlagsBits,
|
||||||
} = require(
|
MessageFlags,
|
||||||
"discord.js"
|
EmbedBuilder
|
||||||
);
|
} = require("discord.js");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
requireAdminAccess
|
reconcileUserAchievements,
|
||||||
|
reconcileAllAchievements,
|
||||||
|
auditProfileData
|
||||||
} = require(
|
} = require(
|
||||||
"../../utils/permissions"
|
"../../utils/profileMaintenance"
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
|
||||||
reconcileProfileAchievements,
|
|
||||||
auditProfileSystem
|
|
||||||
} = require(
|
|
||||||
"../../utils/profileSystem"
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ============================================================
|
|
||||||
* COMMAND
|
|
||||||
* ============================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
data:
|
data:
|
||||||
new SlashCommandBuilder()
|
new SlashCommandBuilder()
|
||||||
.setName(
|
.setName(
|
||||||
"profileadmin"
|
"profileadmin"
|
||||||
)
|
)
|
||||||
|
|
||||||
.setDescription(
|
.setDescription(
|
||||||
"Profile and achievement administration."
|
"Administrative profile maintenance tools"
|
||||||
|
)
|
||||||
|
.setDefaultMemberPermissions(
|
||||||
|
PermissionFlagsBits.Administrator
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
|
||||||
* =================================================
|
|
||||||
* RECONCILE
|
|
||||||
* =================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
.addSubcommand(
|
.addSubcommand(
|
||||||
subcommand =>
|
subcommand =>
|
||||||
subcommand
|
subcommand
|
||||||
.setName(
|
.setName(
|
||||||
"reconcile"
|
"reconcile"
|
||||||
)
|
)
|
||||||
|
|
||||||
.setDescription(
|
.setDescription(
|
||||||
"Re-evaluate stored statistics for achievements."
|
"Backfill database-trackable achievements"
|
||||||
)
|
)
|
||||||
|
|
||||||
.addUserOption(
|
.addUserOption(
|
||||||
option =>
|
option =>
|
||||||
option
|
option
|
||||||
.setName(
|
.setName(
|
||||||
"user"
|
"user"
|
||||||
)
|
)
|
||||||
|
|
||||||
.setDescription(
|
.setDescription(
|
||||||
"Only reconcile this user's profile."
|
"Only reconcile this user; omit for everyone"
|
||||||
)
|
|
||||||
|
|
||||||
.setRequired(
|
|
||||||
false
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
.addBooleanOption(
|
.addBooleanOption(
|
||||||
option =>
|
option =>
|
||||||
option
|
option
|
||||||
.setName(
|
.setName(
|
||||||
"dry-run"
|
"dry-run"
|
||||||
)
|
)
|
||||||
|
|
||||||
.setDescription(
|
.setDescription(
|
||||||
"Show what would change without unlocking anything."
|
"Show what would change without writing it"
|
||||||
)
|
|
||||||
|
|
||||||
.setRequired(
|
|
||||||
false
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
|
||||||
* =================================================
|
|
||||||
* AUDIT
|
|
||||||
* =================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
.addSubcommand(
|
.addSubcommand(
|
||||||
subcommand =>
|
subcommand =>
|
||||||
subcommand
|
subcommand
|
||||||
.setName(
|
.setName(
|
||||||
"audit"
|
"audit"
|
||||||
)
|
)
|
||||||
|
|
||||||
.setDescription(
|
.setDescription(
|
||||||
"Audit the profile and achievement system."
|
"Audit stored profile statistics for inconsistencies"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ========================================================
|
|
||||||
* EXECUTE
|
|
||||||
* ========================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
interaction
|
interaction
|
||||||
) {
|
) {
|
||||||
/*
|
|
||||||
* Runtime authorization.
|
|
||||||
*
|
|
||||||
* This intentionally replaces:
|
|
||||||
*
|
|
||||||
* .setDefaultMemberPermissions(
|
|
||||||
* PermissionFlagsBits.Administrator
|
|
||||||
* )
|
|
||||||
*
|
|
||||||
* so BOT_AUTHOR_ID can also invoke the command.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (
|
|
||||||
!await requireAdminAccess(
|
|
||||||
interaction
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const subcommand =
|
const subcommand =
|
||||||
interaction.options
|
interaction.options.getSubcommand();
|
||||||
.getSubcommand();
|
|
||||||
|
|
||||||
|
|
||||||
switch (
|
|
||||||
subcommand
|
|
||||||
) {
|
|
||||||
case "reconcile":
|
|
||||||
|
|
||||||
return runReconcile(
|
|
||||||
interaction
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
case "audit":
|
|
||||||
|
|
||||||
return runAudit(
|
|
||||||
interaction
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
default:
|
|
||||||
|
|
||||||
return interaction.reply({
|
|
||||||
content:
|
|
||||||
"Unknown profile administration action.",
|
|
||||||
|
|
||||||
|
await interaction.deferReply({
|
||||||
flags:
|
flags:
|
||||||
MessageFlags.Ephemeral
|
MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (
|
||||||
/*
|
subcommand ===
|
||||||
* ============================================================
|
"reconcile"
|
||||||
* RECONCILE
|
) {
|
||||||
* ============================================================
|
const user =
|
||||||
*/
|
|
||||||
|
|
||||||
async function runReconcile(
|
|
||||||
interaction
|
|
||||||
) {
|
|
||||||
const target =
|
|
||||||
interaction.options.getUser(
|
interaction.options.getUser(
|
||||||
"user"
|
"user"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const dryRun =
|
const dryRun =
|
||||||
interaction.options.getBoolean(
|
interaction.options.getBoolean(
|
||||||
"dry-run"
|
"dry-run"
|
||||||
)
|
) ?? false;
|
||||||
??
|
|
||||||
false;
|
|
||||||
|
|
||||||
|
if (user) {
|
||||||
/*
|
|
||||||
* This can touch multiple Mongo documents,
|
|
||||||
* so acknowledge the interaction first.
|
|
||||||
*/
|
|
||||||
|
|
||||||
await interaction.deferReply({
|
|
||||||
flags:
|
|
||||||
MessageFlags.Ephemeral
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result =
|
const result =
|
||||||
await reconcileProfileAchievements(
|
await reconcileUserAchievements({
|
||||||
|
guildId:
|
||||||
interaction.guildId,
|
interaction.guildId,
|
||||||
{
|
|
||||||
userId:
|
userId:
|
||||||
target?.id
|
user.id,
|
||||||
??
|
|
||||||
null,
|
|
||||||
|
|
||||||
dryRun
|
dryRun
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
const lines = [
|
|
||||||
dryRun
|
|
||||||
? "### 🔎 Profile Reconciliation — Dry Run"
|
|
||||||
: "### ✅ Profile Reconciliation Complete",
|
|
||||||
|
|
||||||
"",
|
|
||||||
|
|
||||||
target
|
|
||||||
? `Target: ${target}`
|
|
||||||
: "Target: **Entire Guild**",
|
|
||||||
|
|
||||||
"",
|
|
||||||
|
|
||||||
`Profiles checked: **${result.profilesChecked ?? 0}**`,
|
|
||||||
`Achievement unlocks found: **${result.unlocksFound ?? 0}**`
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
if (
|
|
||||||
!dryRun
|
|
||||||
) {
|
|
||||||
lines.push(
|
|
||||||
`Achievements unlocked: **${result.unlocksApplied ?? 0}**`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (
|
|
||||||
result.skippedEventOnly !==
|
|
||||||
undefined
|
|
||||||
) {
|
|
||||||
lines.push(
|
|
||||||
`Event-only checks skipped: **${result.skippedEventOnly}**`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (
|
|
||||||
Array.isArray(
|
|
||||||
result.details
|
|
||||||
)
|
|
||||||
&&
|
|
||||||
result.details.length >
|
|
||||||
0
|
|
||||||
) {
|
|
||||||
const preview =
|
|
||||||
result.details
|
|
||||||
.slice(
|
|
||||||
0,
|
|
||||||
15
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
entry => {
|
|
||||||
if (
|
|
||||||
typeof entry ===
|
|
||||||
"string"
|
|
||||||
) {
|
|
||||||
return `• ${entry}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
`• <@${entry.userId}> — ` +
|
|
||||||
`${entry.achievementName ?? entry.achievementId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"### Changes",
|
|
||||||
...preview
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
if (
|
|
||||||
result.details.length >
|
|
||||||
preview.length
|
|
||||||
) {
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
`*...and ${result.details.length - preview.length} more.*`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
content:
|
embeds: [
|
||||||
lines.join(
|
new EmbedBuilder()
|
||||||
"\n"
|
.setTitle(
|
||||||
|
"🛠️ Profile Reconciliation"
|
||||||
)
|
)
|
||||||
});
|
.setDescription(
|
||||||
|
`${dryRun ? "Dry run for" : "Reconciled"} ${user}.`
|
||||||
} catch (error) {
|
)
|
||||||
console.error(
|
.addFields(
|
||||||
"[ProfileAdmin] Reconciliation failed:",
|
{
|
||||||
error
|
name:
|
||||||
);
|
"New unlocks",
|
||||||
|
value:
|
||||||
|
`**${result.unlocked}**`,
|
||||||
return interaction.editReply({
|
inline:
|
||||||
content:
|
true
|
||||||
"Profile reconciliation failed. Check the bot logs for details."
|
},
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Definitions checked",
|
||||||
|
value:
|
||||||
|
`**${result.checked}**`,
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Event-only skipped",
|
||||||
|
value:
|
||||||
|
`**${result.skippedEventOnly}**`,
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
const summary =
|
||||||
/*
|
await reconcileAllAchievements({
|
||||||
* ============================================================
|
guildId:
|
||||||
* AUDIT
|
interaction.guildId,
|
||||||
* ============================================================
|
dryRun
|
||||||
*/
|
|
||||||
|
|
||||||
async function runAudit(
|
|
||||||
interaction
|
|
||||||
) {
|
|
||||||
await interaction.deferReply({
|
|
||||||
flags:
|
|
||||||
MessageFlags.Ephemeral
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return interaction.editReply({
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle(
|
||||||
|
"🛠️ Profile Reconciliation"
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
`${dryRun ? "Dry run completed" : "Reconciliation completed"} for stored profile users.`
|
||||||
|
)
|
||||||
|
.addFields(
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Users",
|
||||||
|
value:
|
||||||
|
`**${summary.users}**`,
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"New unlocks",
|
||||||
|
value:
|
||||||
|
`**${summary.unlocked}**`,
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Event-only checks skipped",
|
||||||
|
value:
|
||||||
|
`**${summary.skippedEventOnly}**`,
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
const audit =
|
||||||
const result =
|
await auditProfileData(
|
||||||
await auditProfileSystem(
|
|
||||||
interaction.guildId
|
interaction.guildId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const issuePreview =
|
||||||
const lines = [
|
audit.issues
|
||||||
"### 🧾 Profile System Audit",
|
.slice(0, 10)
|
||||||
"",
|
.join("\n");
|
||||||
|
|
||||||
`Profile documents: **${result.profileDocuments ?? 0}**`,
|
|
||||||
`Achievement documents: **${result.achievementDocuments ?? 0}**`,
|
|
||||||
`Active achievement definitions: **${result.activeAchievements ?? 0}**`,
|
|
||||||
`Legacy achievement definitions: **${result.legacyAchievements ?? 0}**`,
|
|
||||||
`Guild badges: **${result.badges ?? 0}**`,
|
|
||||||
`Claimed badges: **${result.claimedBadges ?? 0}**`,
|
|
||||||
`Available badges: **${result.availableBadges ?? 0}**`
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
if (
|
|
||||||
Array.isArray(
|
|
||||||
result.warnings
|
|
||||||
)
|
|
||||||
&&
|
|
||||||
result.warnings.length >
|
|
||||||
0
|
|
||||||
) {
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"### ⚠️ Warnings",
|
|
||||||
|
|
||||||
...result.warnings.map(
|
|
||||||
warning =>
|
|
||||||
`• ${warning}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
lines.push(
|
|
||||||
"",
|
|
||||||
"✅ No obvious profile-system issues were found."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
content:
|
embeds: [
|
||||||
lines.join(
|
new EmbedBuilder()
|
||||||
"\n"
|
.setTitle(
|
||||||
|
"🔎 Profile Data Audit"
|
||||||
)
|
)
|
||||||
});
|
.addFields(
|
||||||
|
{
|
||||||
} catch (error) {
|
name:
|
||||||
console.error(
|
"Stats documents",
|
||||||
"[ProfileAdmin] Audit failed:",
|
value:
|
||||||
error
|
`**${audit.statDocuments}**`,
|
||||||
);
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
return interaction.editReply({
|
{
|
||||||
content:
|
name:
|
||||||
"Profile audit failed. Check the bot logs for details."
|
"Achievement documents",
|
||||||
|
value:
|
||||||
|
`**${audit.achievementDocuments}**`,
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Active achievements",
|
||||||
|
value:
|
||||||
|
`**${audit.activeAchievements}**`,
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Global badges",
|
||||||
|
value:
|
||||||
|
`**${audit.badgeDocuments}** (${audit.claimedBadges} claimed)`,
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Orphan achievement docs",
|
||||||
|
value:
|
||||||
|
`**${audit.orphanAchievementDocs}**`,
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Issues",
|
||||||
|
value:
|
||||||
|
`**${audit.issueCount}**`,
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
issuePreview
|
||||||
|
? `First issues:\n${issuePreview}`
|
||||||
|
: "No structural inconsistencies found."
|
||||||
|
)
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module.exports = {
|
|||||||
"records"
|
"records"
|
||||||
)
|
)
|
||||||
.setDescription(
|
.setDescription(
|
||||||
"View Shai Cookie and Crumb Saucer records"
|
"View Olymp Cookie and Crumb Saucer records"
|
||||||
)
|
)
|
||||||
.addStringOption(
|
.addStringOption(
|
||||||
option =>
|
option =>
|
||||||
|
|||||||
@@ -27,30 +27,30 @@ const HEIST_METHODS = {
|
|||||||
baseChance: 0.65,
|
baseChance: 0.65,
|
||||||
minStealPercent: 0.01,
|
minStealPercent: 0.01,
|
||||||
maxStealPercent: 0.03,
|
maxStealPercent: 0.03,
|
||||||
maxStealAmount: 5000,
|
maxStealAmount: 1500,
|
||||||
minPenaltyPercent: 0.01,
|
minPenaltyPercent: 0.01,
|
||||||
maxPenaltyPercent: 0.04,
|
maxPenaltyPercent: 0.04,
|
||||||
maxPenaltyAmount: 1500
|
maxPenaltyAmount: 750
|
||||||
},
|
},
|
||||||
|
|
||||||
Hack: {
|
Hack: {
|
||||||
baseChance: 0.45,
|
baseChance: 0.45,
|
||||||
minStealPercent: 0.03,
|
minStealPercent: 0.03,
|
||||||
maxStealPercent: 0.06,
|
maxStealPercent: 0.06,
|
||||||
maxStealAmount: 12500,
|
maxStealAmount: 3500,
|
||||||
minPenaltyPercent: 0.03,
|
minPenaltyPercent: 0.03,
|
||||||
maxPenaltyPercent: 0.07,
|
maxPenaltyPercent: 0.07,
|
||||||
maxPenaltyAmount: 4000
|
maxPenaltyAmount: 2000
|
||||||
},
|
},
|
||||||
|
|
||||||
Smash: {
|
Smash: {
|
||||||
baseChance: 0.25,
|
baseChance: 0.25,
|
||||||
minStealPercent: 0.06,
|
minStealPercent: 0.06,
|
||||||
maxStealPercent: 0.12,
|
maxStealPercent: 0.12,
|
||||||
maxStealAmount: 25000,
|
maxStealAmount: 7500,
|
||||||
minPenaltyPercent: 0.06,
|
minPenaltyPercent: 0.06,
|
||||||
maxPenaltyPercent: 0.12,
|
maxPenaltyPercent: 0.12,
|
||||||
maxPenaltyAmount: 10000
|
maxPenaltyAmount: 5000
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+80
-10
@@ -18,7 +18,8 @@ const {
|
|||||||
} = require("./cookieEconomy");
|
} = require("./cookieEconomy");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
recordGameOutcome
|
recordGameOutcome,
|
||||||
|
trackProfileEvent
|
||||||
} = require("./profileSystem");
|
} = require("./profileSystem");
|
||||||
|
|
||||||
const GAME_COLLECTION =
|
const GAME_COLLECTION =
|
||||||
@@ -112,7 +113,9 @@ async function startBlackjack(
|
|||||||
interaction,
|
interaction,
|
||||||
bet,
|
bet,
|
||||||
allowCustomBet = false,
|
allowCustomBet = false,
|
||||||
allIn = false
|
source = "normal",
|
||||||
|
previousAllIn = false,
|
||||||
|
previousBet = 0
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
!allowCustomBet &&
|
!allowCustomBet &&
|
||||||
@@ -196,6 +199,9 @@ async function startBlackjack(
|
|||||||
getCookieBalance(houseId)
|
getCookieBalance(houseId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const allIn =
|
||||||
|
bet === playerBalance;
|
||||||
|
|
||||||
if (playerBalance < bet) {
|
if (playerBalance < bet) {
|
||||||
return temporaryEphemeralReply(
|
return temporaryEphemeralReply(
|
||||||
interaction,
|
interaction,
|
||||||
@@ -276,6 +282,11 @@ async function startBlackjack(
|
|||||||
doubled:
|
doubled:
|
||||||
false,
|
false,
|
||||||
allIn,
|
allIn,
|
||||||
|
vaultAtBet:
|
||||||
|
playerBalance,
|
||||||
|
source,
|
||||||
|
previousAllIn,
|
||||||
|
previousBet,
|
||||||
deck,
|
deck,
|
||||||
playerCards,
|
playerCards,
|
||||||
dealerCards,
|
dealerCards,
|
||||||
@@ -285,6 +296,20 @@ async function startBlackjack(
|
|||||||
|
|
||||||
await games.insertOne(game);
|
await games.insertOne(game);
|
||||||
|
|
||||||
|
await trackProfileEvent(
|
||||||
|
interaction,
|
||||||
|
playerId,
|
||||||
|
{
|
||||||
|
type: "game_start",
|
||||||
|
game: "blackjack",
|
||||||
|
source,
|
||||||
|
bet,
|
||||||
|
allIn,
|
||||||
|
previousAllIn,
|
||||||
|
previousBet
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const playerBlackjack =
|
const playerBlackjack =
|
||||||
isBlackjack(playerCards);
|
isBlackjack(playerCards);
|
||||||
|
|
||||||
@@ -370,7 +395,7 @@ async function startBlackjackAllIn(
|
|||||||
interaction,
|
interaction,
|
||||||
balance,
|
balance,
|
||||||
true,
|
true,
|
||||||
true
|
"all_in"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +430,9 @@ async function runBlackjackBack(
|
|||||||
const bet =
|
const bet =
|
||||||
Number(parts[2]);
|
Number(parts[2]);
|
||||||
|
|
||||||
|
const previousAllIn =
|
||||||
|
parts[3] === "1";
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ownerId !==
|
ownerId !==
|
||||||
interaction.user.id
|
interaction.user.id
|
||||||
@@ -435,7 +463,9 @@ async function runBlackjackBack(
|
|||||||
interaction,
|
interaction,
|
||||||
bet,
|
bet,
|
||||||
true,
|
true,
|
||||||
false
|
"run_it_back",
|
||||||
|
previousAllIn,
|
||||||
|
bet
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,6 +833,32 @@ async function finishBlackjack(
|
|||||||
game,
|
game,
|
||||||
result
|
result
|
||||||
) {
|
) {
|
||||||
|
const { db } = getCollections();
|
||||||
|
|
||||||
|
const claim = await db
|
||||||
|
.collection(GAME_COLLECTION)
|
||||||
|
.updateOne(
|
||||||
|
{
|
||||||
|
gameId: game.gameId,
|
||||||
|
state: "Playing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
state: "Resolving"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (claim.modifiedCount !== 1) {
|
||||||
|
return temporaryEphemeralReply(
|
||||||
|
interaction,
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"This Blackjack hand is already being resolved."
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const houseId =
|
const houseId =
|
||||||
interaction.client.user.id;
|
interaction.client.user.id;
|
||||||
|
|
||||||
@@ -845,6 +901,23 @@ async function finishBlackjack(
|
|||||||
`[Blackjack] House cannot cover payout ${payout} for ${game.userId}`
|
`[Blackjack] House cannot cover payout ${payout} for ${game.userId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.collection(GAME_COLLECTION)
|
||||||
|
.updateOne(
|
||||||
|
{
|
||||||
|
gameId: game.gameId,
|
||||||
|
state: "Resolving"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
state: "Finished",
|
||||||
|
result: "PayoutError",
|
||||||
|
payout: 0,
|
||||||
|
finishedAt: getUnixTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
embeds: [
|
embeds: [
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
@@ -856,7 +929,7 @@ async function finishBlackjack(
|
|||||||
)
|
)
|
||||||
.setFooter({
|
.setFooter({
|
||||||
text:
|
text:
|
||||||
`crumbBlackjackResult:${game.userId}:${game.originalBet}`
|
`crumbBlackjackResult:${game.userId}:${game.originalBet}:${game.allIn ? 1 : 0}`
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
components:
|
components:
|
||||||
@@ -871,9 +944,6 @@ async function finishBlackjack(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { db } =
|
|
||||||
getCollections();
|
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.collection(
|
.collection(
|
||||||
GAME_COLLECTION
|
GAME_COLLECTION
|
||||||
@@ -883,7 +953,7 @@ async function finishBlackjack(
|
|||||||
gameId:
|
gameId:
|
||||||
game.gameId,
|
game.gameId,
|
||||||
state:
|
state:
|
||||||
"Playing"
|
"Resolving"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
@@ -1010,7 +1080,7 @@ async function finishBlackjack(
|
|||||||
)
|
)
|
||||||
.setFooter({
|
.setFooter({
|
||||||
text:
|
text:
|
||||||
`crumbBlackjackResult:${game.userId}:${game.originalBet}`
|
`crumbBlackjackResult:${game.userId}:${game.originalBet}:${game.allIn ? 1 : 0}`
|
||||||
});
|
});
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
|
|||||||
+36
-8
@@ -12,7 +12,8 @@ const {
|
|||||||
} = require("./cookieEconomy");
|
} = require("./cookieEconomy");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
recordGameOutcome
|
recordGameOutcome,
|
||||||
|
trackProfileEvent
|
||||||
} = require("./profileSystem");
|
} = require("./profileSystem");
|
||||||
|
|
||||||
const ROULETTE_BETS = [
|
const ROULETTE_BETS = [
|
||||||
@@ -206,7 +207,10 @@ async function startRouletteAllIn(
|
|||||||
async function showRouletteChoices(
|
async function showRouletteChoices(
|
||||||
interaction,
|
interaction,
|
||||||
bet,
|
bet,
|
||||||
allIn = false
|
requestedAllIn = false,
|
||||||
|
source = "normal",
|
||||||
|
previousAllIn = false,
|
||||||
|
previousBet = 0
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
!Number.isSafeInteger(bet) ||
|
!Number.isSafeInteger(bet) ||
|
||||||
@@ -262,7 +266,7 @@ async function showRouletteChoices(
|
|||||||
})
|
})
|
||||||
.setFooter({
|
.setFooter({
|
||||||
text:
|
text:
|
||||||
`crumbRouletteBet:${interaction.user.id}:${bet}:${allIn ? 1 : 0}`
|
`crumbRouletteBet:${interaction.user.id}:${bet}:${requestedAllIn ? 1 : 0}:${source}:${previousAllIn ? 1 : 0}:${previousBet}`
|
||||||
});
|
});
|
||||||
|
|
||||||
return interaction.update({
|
return interaction.update({
|
||||||
@@ -303,6 +307,9 @@ async function runRouletteBack(
|
|||||||
const bet =
|
const bet =
|
||||||
Number(parts[2]);
|
Number(parts[2]);
|
||||||
|
|
||||||
|
const previousAllIn =
|
||||||
|
parts[3] === "1";
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ownerId !==
|
ownerId !==
|
||||||
interaction.user.id
|
interaction.user.id
|
||||||
@@ -383,8 +390,14 @@ async function playRoulette(
|
|||||||
const bet =
|
const bet =
|
||||||
Number(parts[2]);
|
Number(parts[2]);
|
||||||
|
|
||||||
const allIn =
|
const source =
|
||||||
parts[3] === "1";
|
parts[4] || "normal";
|
||||||
|
|
||||||
|
const previousAllIn =
|
||||||
|
parts[5] === "1";
|
||||||
|
|
||||||
|
const previousBet =
|
||||||
|
Number(parts[6] ?? 0);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ownerId !==
|
ownerId !==
|
||||||
@@ -476,6 +489,23 @@ async function playRoulette(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allIn =
|
||||||
|
bet === playerBalance;
|
||||||
|
|
||||||
|
await trackProfileEvent(
|
||||||
|
interaction,
|
||||||
|
playerId,
|
||||||
|
{
|
||||||
|
type: "game_start",
|
||||||
|
game: "roulette",
|
||||||
|
source,
|
||||||
|
bet,
|
||||||
|
allIn,
|
||||||
|
previousAllIn,
|
||||||
|
previousBet
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
await interaction.update({
|
await interaction.update({
|
||||||
embeds: [
|
embeds: [
|
||||||
buildSpinEmbed(
|
buildSpinEmbed(
|
||||||
@@ -594,8 +624,6 @@ async function playRoulette(
|
|||||||
won &&
|
won &&
|
||||||
choiceKey ===
|
choiceKey ===
|
||||||
"Zero",
|
"Zero",
|
||||||
allIn:
|
|
||||||
won &&
|
|
||||||
allIn
|
allIn
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -664,7 +692,7 @@ async function playRoulette(
|
|||||||
)
|
)
|
||||||
.setFooter({
|
.setFooter({
|
||||||
text:
|
text:
|
||||||
`crumbRouletteResult:${playerId}:${bet}`
|
`crumbRouletteResult:${playerId}:${bet}:${allIn ? 1 : 0}`
|
||||||
});
|
});
|
||||||
|
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
|
|||||||
+21
-3
@@ -13,7 +13,8 @@ const {
|
|||||||
} = require("./cookieEconomy");
|
} = require("./cookieEconomy");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
recordGameOutcome
|
recordGameOutcome,
|
||||||
|
trackProfileEvent
|
||||||
} = require("./profileSystem");
|
} = require("./profileSystem");
|
||||||
|
|
||||||
const SLOT_BETS = [
|
const SLOT_BETS = [
|
||||||
@@ -104,7 +105,8 @@ async function showSlots(interaction) {
|
|||||||
|
|
||||||
async function playSlots(
|
async function playSlots(
|
||||||
interaction,
|
interaction,
|
||||||
bet
|
bet,
|
||||||
|
source = "normal"
|
||||||
) {
|
) {
|
||||||
const ownerId =
|
const ownerId =
|
||||||
getSlotsSessionOwner(
|
getSlotsSessionOwner(
|
||||||
@@ -197,6 +199,17 @@ async function playSlots(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await trackProfileEvent(
|
||||||
|
interaction,
|
||||||
|
playerId,
|
||||||
|
{
|
||||||
|
type: "game_start",
|
||||||
|
game: "slots",
|
||||||
|
source,
|
||||||
|
bet
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
await interaction.update({
|
await interaction.update({
|
||||||
embeds: [
|
embeds: [
|
||||||
buildSpinningEmbed(
|
buildSpinningEmbed(
|
||||||
@@ -321,6 +334,10 @@ async function playSlots(
|
|||||||
? "loss"
|
? "loss"
|
||||||
: "push",
|
: "push",
|
||||||
profit,
|
profit,
|
||||||
|
triplet:
|
||||||
|
outcome.type === "triple"
|
||||||
|
? outcome.symbol
|
||||||
|
: null,
|
||||||
jackpot:
|
jackpot:
|
||||||
outcome.type ===
|
outcome.type ===
|
||||||
"triple" &&
|
"triple" &&
|
||||||
@@ -460,7 +477,8 @@ async function runSlotsBack(
|
|||||||
|
|
||||||
return playSlots(
|
return playSlots(
|
||||||
interaction,
|
interaction,
|
||||||
bet
|
bet,
|
||||||
|
"run_it_back"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+928
-1951
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user