Compare commits

...

3 Commits

Author SHA1 Message Date
DeSqBlocki 338b32430d made achievments easier to discern 2026-08-23 01:00:03 +02:00
DeSqBlocki 9f1a539501 updated readme 2026-08-23 00:52:37 +02:00
DeSqBlocki 425686ec40 added new logic to the new rewards 2026-08-23 00:43:43 +02:00
16 changed files with 1982 additions and 2721 deletions
+326 -46
View File
@@ -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
+30 -27
View File
@@ -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
)
}); });
} }
}; };
+30 -29
View File
@@ -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
)
}); });
} }
}; };
+30 -29
View File
@@ -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
View File
@@ -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
)
}); });
} }
}; };
+30 -29
View File
@@ -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
)
}); });
} }
}; };
+30 -29
View File
@@ -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
View File
@@ -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
)
}); });
} }
}; };
+46 -29
View File
@@ -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
}
);
} }
}; };
+173 -325
View File
@@ -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."
)
]
}); });
} }
} };
+1 -1
View File
@@ -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 =>
+6 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff