Compare commits

..

39 Commits

Author SHA1 Message Date
DeSqBlocki 6e3e1620d1 added more documentation 2026-09-13 22:10:34 +02:00
DeSqBlocki dd4d6e4817 added achievements for new steal logic 2026-09-13 21:56:18 +02:00
DeSqBlocki b441de286c added ai rulebook 2026-09-13 21:56:01 +02:00
DeSqBlocki 2ca18aae06 updated steal logic 2026-09-13 21:14:56 +02:00
DeSqBlocki 1ef2295c07 snapshot_poker_phase5 2026-09-08 21:49:43 +02:00
DeSqBlocki 6342c4569b snapshot_poker_ui_ux_fix_again_fuckmylife 2026-09-08 21:12:34 +02:00
DeSqBlocki c61e34c241 snapshot_poker_uiux_cleanup_2 2026-09-08 21:05:52 +02:00
DeSqBlocki 82cef1b34a snapshot_poker_uiux_cleanup 2026-09-08 20:51:58 +02:00
DeSqBlocki 136a5902c0 snapshot_poker_phase4_hotfix 2026-09-08 20:17:19 +02:00
DeSqBlocki 903a2f3081 snapshot_poker_phase3 2026-09-08 19:37:02 +02:00
DeSqBlocki 995a2317b9 small changes to UI/UX 2026-08-27 21:35:06 +02:00
DeSqBlocki e3dbb11e12 snapshot migration pre-phase 6, stable 2026-08-27 21:14:27 +02:00
DeSqBlocki 14a059034c snapshot migration subPhase 5.5 2026-08-27 19:50:12 +02:00
DeSqBlocki 9710fc65bf snapshot migration phase 5 2026-08-27 19:34:16 +02:00
DeSqBlocki 42fc848f8c snapshot migration phase 4 2026-08-27 19:19:58 +02:00
DeSqBlocki c2ac7737c0 snapshot rollout phase 1 2026-08-27 18:52:56 +02:00
DeSqBlocki 6a519f3a2d changed achievment to work without followup 2026-08-23 01:19:22 +02:00
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
DeSqBlocki 4d858554be made collect logic actually connect to the utils 2026-08-23 00:21:52 +02:00
DeSqBlocki 9c867b1adb added bankruptcy 2026-08-23 00:09:13 +02:00
DeSqBlocki 030b4b8883 updated profile and centralized functions 2026-08-22 23:33:20 +02:00
DeSqBlocki 08d1a8c5cd updated badges 2026-08-22 22:46:40 +02:00
DeSqBlocki 9e0870c1ed snapshot 2026-08-22 22:29:16 +02:00
DeSqBlocki ca823726e3 added more achievments 2026-08-22 22:10:41 +02:00
DeSqBlocki ae35f61522 added cookie collect button 2026-08-22 20:59:25 +02:00
DeSqBlocki 985f481fae fixed some copy pasted issues 2026-08-22 20:40:25 +02:00
DeSqBlocki e437c051b9 profile update with badges and achievements 2026-08-22 20:27:42 +02:00
DeSqBlocki c630543f9e fixed runitback for slots 2026-08-22 20:15:25 +02:00
DeSqBlocki 9847e476b8 Merge branch 'main' of https://git.desq-gaming.de/admin_rb/ShaiBot 2026-08-22 20:14:09 +02:00
DeSqBlocki 8aeab3fb5d added base profile update 2026-08-22 20:13:47 +02:00
DeSqBlocki 81ef97370c added runtiback and allin 2026-08-22 19:52:47 +02:00
DeSqBlocki 1fd73538fa added ReadMe 2026-08-18 21:12:32 +02:00
DeSqBlocki 32ce20bd72 added roulette 2026-08-18 21:03:07 +02:00
DeSqBlocki 0d512960a7 added blackjack 2026-08-18 20:44:51 +02:00
DeSqBlocki 0407f4d7f1 crumleStrike Roles snapshot 2026-08-18 20:31:03 +02:00
DeSqBlocki 0f48f09bd9 refactored and moved 2026-08-18 20:07:41 +02:00
DeSqBlocki b9a887259a boos crumble snapshot 2026-08-18 19:45:43 +02:00
132 changed files with 38739 additions and 4670 deletions
+3
View File
@@ -1,2 +1,5 @@
node_modules node_modules
.env .env
backup/
*.bak
tests/
+2220
View File
File diff suppressed because it is too large Load Diff
+289
View File
@@ -0,0 +1,289 @@
# ShaiBot
ShaiBot is a modular Discord community bot built around a persistent
**Cookie economy**, interactive games, player progression, and
multiplayer activities.
The bot's main entertainment hub is the **Crumb Saucer**, where players
can collect and wager Cookies, play casino-style games, attempt Vault
Heists, recover through Bankruptcy, and sit down at a multiplayer Texas
Hold'em table.
ShaiBot also includes persistent player profiles, achievements,
guild-unique badges, cooperative encounters, raid planning, and lighter
community features.
> **Status:** Active development and stabilization. The major economy,
> profile, achievement, and Poker overhaul is live; current work is
> focused primarily on testing, balancing, and polish.
## Highlights
- 🍪 Persistent Cookie economy
- 🎰 Crumb Saucer game hub
- 🎰 Slots, Blackjack, Roulette, Vault Heist, and Bankruptcy
- ♠️ Persistent 2--6 player Texas Hold'em
- 🏴 Player-to-player Cookie stealing
- 👤 Lifetime player profiles and statistics
- 🏆 Personal achievements
- 🎖️ Guild-unique global badges
- 🔁 All-In and Run It Back progression
- ⚔️ Cooperative Crumble Strike encounters
- 📅 Raid planning and community utilities
- 💾 Mongo-backed persistence and recovery
## The Crumb Saucer
The **Crumb Saucer** is the main home for ShaiBot's Cookie-powered
games.
### 🍪 Cookie Bakery
Collect Cookies using the same shared collection system as
`/cookies collect`.
### 🎰 Slots
Spin for symbol combinations, jackpots, All-In wagers, and Run It Back
streaks.
### 🃏 Blackjack
Includes Hit, Stand, Double, natural Blackjack, pushes, All-In, and Run
It Back.
### 🎡 Roulette
A compact European Roulette game using numbers `0–36`, with Red, Black,
Odd, Even, and Zero bets.
### 🏦 Vault Heist
Attempt to steal from ShaiBot's house vault using different risk/reward
approaches.
### 🆘 Bankruptcy
A recovery mechanic for players who need a fresh start, with profile
progression for rebuilding afterward.
### ♠️ Shai Poker
A persistent multiplayer Texas Hold'em game for **2--6 players**.
Current defaults:
``` text
Buy-in: 1,000 Cookies
Blinds: 10 / 20
Turn timer: 90 seconds
Pot model: single pot
```
Poker supports private hole cards, public community cards,
Fold/Check/Call/Bet/Raise/All-In, automatic timeout folding, restart
recovery, host cancellation, safe buy-in/cash-out settlement, Poker
statistics, achievements, and guild-first badges.
Side pots, tournaments, bots, rake, and player-facing equity assistance
are intentionally outside the current V1 design.
## Player Profiles
Profiles provide the permanent history behind ShaiBot's progression
systems.
They track things such as:
- Game wins and losses
- Per-game statistics
- Cookie gains and losses
- Giving and stealing
- Daily activity and streaks
- Social progression
- Run It Back streaks
- Poker history and hand accomplishments
- Achievements
- Global badges
Completed multiplayer game documents do not need to become permanent
history when the meaningful result already belongs in the profile
system.
## Achievements & Global Badges
**Achievements** are personal progression. Any eligible player can earn
them.
**Global badges** are guild-unique distinctions, usually awarded to the
first player in a guild to accomplish something notable.
Progression spans the Cookie economy, stealing, Crumb Saucer games,
activity, social interactions, Poker, and special cross-game challenges.
## Cookie Stealing
Player-to-player stealing is separate from Vault Heist.
The current system accounts for wealth differences, recent pressure on
the victim, percentage-based loot, hard caps, and an hourly exposure
budget.
Detailed balancing and mechanics are documented in
[`COOKIE_LOGIC.md`](COOKIE_LOGIC.md).
## ⚔️ Crumble Strike
Crumble Strike is ShaiBot's cooperative boss-battle system.
Players recruit a party, choose combat roles, commit Cookies, and work
together using actions such as Strike, Defend, Heal, Revive, and Limit
Break.
Encounters use persistent state because they can span many players and
interactions.
## 📅 Raid Planner & Community Features
ShaiBot also includes raid-planning tools and smaller social/community
commands alongside its economy and progression systems.
------------------------------------------------------------------------
# Installation
## Requirements
- Node.js
- MongoDB
- A Discord bot application/token
- Dependencies listed in `package.json`
Clone and install:
``` bash
git clone https://git.desq-gaming.de/Shaiwase/ShaiBot.git
cd ShaiBot
npm install
```
Create `.env` in the repository root:
``` env
D_TOKEN=YOUR_DISCORD_BOT_TOKEN
M_URI=YOUR_MONGODB_CONNECTION_STRING
M_DB=YOUR_DATABASE_NAME
# Optional
BOT_AUTHOR_ID=YOUR_DISCORD_USER_ID
PROFILE_TIMEZONE=Europe/Berlin
```
Never commit `.env`.
Start normally:
``` bash
node index.js
```
Or with PM2:
``` bash
pm2 start index.js --name ShaiBot
```
A typical update cycle is:
``` bash
node --check path/to/changed-file.js
pm2 restart ShaiBot
pm2 logs ShaiBot --lines 100
```
## MongoDB
ShaiBot does not require MongoDB multi-document transactions for its
core economy.
The bot instead relies on atomic updates, unique indexes, optimistic
concurrency, idempotent operations, and recovery logic where
appropriate.
If your Mongo deployment does not support retryable writes, your
connection string may require:
``` text
retryWrites=false
```
Never commit database credentials.
------------------------------------------------------------------------
# Documentation
The central README intentionally stays high-level. Detailed
implementation rules live in focused manuals:
------------------------------------------------------------------------------------
Document Purpose
------------------------------------------------ -----------------------------------
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) Runtime structure, handlers,
utilities, Mongo persistence,
concurrency, and recovery
[`docs/ECONOMY.md`](docs/ECONOMY.md) Cookie balances, transfers,
cooldowns, house vault, settlement,
and idempotency
[`docs/PROGRESSION.md`](docs/PROGRESSION.md) Profiles, achievements, badges,
activity, social progression, and
maintenance
[`docs/GAMES.md`](docs/GAMES.md) Crumb Saucer games, Poker, Crumble
Strike, and important game rules
[`COOKIE_LOGIC.md`](COOKIE_LOGIC.md) Detailed PvP Cookie stealing rules
and balancing
[`AGENTS.md`](AGENTS.md) Repository-wide safety and
architecture guide for AI coding
agents
------------------------------------------------------------------------------------
The current repository code remains the final source of truth.
------------------------------------------------------------------------
# Development Principles
ShaiBot generally favors:
- Shared utilities instead of duplicated game/economy logic
- Thin Discord command/button wrappers
- Persistent authoritative state where recovery matters
- Atomic and idempotent economy operations
- Centralized profile progression
- Public game experiences with private sensitive information
- Small explicit solutions over unnecessary abstraction
For stateful changes, always consider duplicate interactions,
simultaneous users, insufficient-balance races, and bot restarts during
settlement.
## Security
Keep at least the following out of version control:
``` gitignore
node_modules/
.env
*.log
```
Rotate Discord or MongoDB credentials immediately if they are exposed.
## License
See the repository's current license information, if present.
+11
View File
@@ -0,0 +1,11 @@
const {
closeInteractionSession
} = require("../../utils/interactionSession");
module.exports = {
name: "sessionClose",
async execute(interaction) {
return closeInteractionSession(interaction);
}
};
-18
View File
@@ -1,18 +0,0 @@
const {
performBossAction
} = require(
"../../utils/cookieBoss"
);
module.exports = {
name:
"cookieBossBurst",
async execute(interaction) {
return performBossAction(
interaction,
"Burst"
);
}
};
-18
View File
@@ -1,18 +0,0 @@
const {
performBossAction
} = require(
"../../utils/cookieBoss"
);
module.exports = {
name:
"cookieBossLimit",
async execute(interaction) {
return performBossAction(
interaction,
"Limit"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
showCrumbSaucer
} = require(
"../../utils/crumbSaucer"
);
module.exports = {
name:
"crumbSaucerBack",
async execute(interaction) {
return showCrumbSaucer(
interaction
);
}
};
+91
View File
@@ -0,0 +1,91 @@
const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
MessageFlags
} = require("discord.js");
const {
declareBankruptcy
} = require("../../utils/bankruptcy");
const {
requireInteractionSession
} = require("../../utils/interactionSession");
module.exports = {
name: "crumbSaucerBankruptcy",
async execute(interaction) {
const session = await requireInteractionSession(interaction);
if (!session) {
return;
}
const result = await declareBankruptcy(interaction);
if (!result.ok) {
if (result.reason === "COOLDOWN") {
return interaction.reply({
content:
`Bankruptcy assistance is unavailable until <t:${result.nextAvailableAt}:R>.`,
flags: MessageFlags.Ephemeral
});
}
if (result.reason === "BALANCE_NOT_ZERO") {
return interaction.reply({
content:
`Bankruptcy can only be declared at exactly **0 Cookies**. Your current balance is **${result.balance.toLocaleString()}**.`,
flags: MessageFlags.Ephemeral
});
}
return interaction.reply({
content: "Bankruptcy could not be declared.",
flags: MessageFlags.Ephemeral
});
}
const embed = new EmbedBuilder()
.setTitle("🍪 Bankruptcy Assistance")
.setDescription(
[
`${interaction.user} declared bankruptcy and received a starter kit.`,
"",
`**+${result.amount.toLocaleString()} Cookies**`,
"",
"Spend them wisely. Or at least memorably."
].join("\n")
)
.addFields({
name: "Next Eligibility",
value: `<t:${result.nextAvailableAt}:R>`
})
.setFooter({
text: `crumbSaucer:${interaction.user.id}`
});
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId("crumbSaucerBack")
.setLabel("Back to Crumb Saucer")
.setEmoji("◀️")
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId("sessionClose")
.setLabel("Close")
.setEmoji("✖️")
.setStyle(ButtonStyle.Secondary)
);
return interaction.update({
embeds: [embed],
components: [row]
});
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
showBlackjack
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbSaucerBlackjack",
async execute(
interaction
) {
return showBlackjack(
interaction
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
startBlackjackAllIn
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackAllIn",
async execute(
interaction
) {
return startBlackjackAllIn(
interaction
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
startBlackjack
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackBet10",
async execute(interaction) {
return startBlackjack(
interaction,
10
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
startBlackjack
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackBet100",
async execute(interaction) {
return startBlackjack(
interaction,
100
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
startBlackjack
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackBet1000",
async execute(interaction) {
return startBlackjack(
interaction,
1000
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
startBlackjack
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackBet50",
async execute(interaction) {
return startBlackjack(
interaction,
50
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
startBlackjack
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackBet500",
async execute(interaction) {
return startBlackjack(
interaction,
500
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
doubleBlackjack
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackDouble",
async execute(
interaction
) {
return doubleBlackjack(
interaction
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
hitBlackjack
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackHit",
async execute(
interaction
) {
return hitBlackjack(
interaction
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
runBlackjackBack
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackRunItBack",
async execute(
interaction
) {
return runBlackjackBack(
interaction
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
standBlackjack
} = require(
"../../utils/crumbBlackjack"
);
module.exports = {
name:
"crumbBlackjackStand",
async execute(
interaction
) {
return standBlackjack(
interaction
);
}
};
+91
View File
@@ -0,0 +1,91 @@
const {
EmbedBuilder
} = require(
"discord.js"
);
const {
temporaryEphemeralReply
} = require(
"../../utils/cookieEconomy"
);
const {
getCrumbSaucerSessionOwner
} = require(
"../../utils/crumbSaucer"
);
function delay(
ms
) {
return new Promise(
resolve =>
setTimeout(
resolve,
ms
)
);
}
module.exports = {
name:
"crumbSaucerClose",
async execute(
interaction
) {
const ownerId =
getCrumbSaucerSessionOwner(
interaction
);
if (
ownerId !==
interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This isn't your Crumb Saucer session."
}
);
}
const embed =
new EmbedBuilder()
.setTitle(
"Closed"
)
.setDescription(
"Closing Crumb Saucer..."
);
await interaction.update({
embeds: [
embed
],
components: []
});
await delay(
1500
);
try {
await interaction.deleteReply();
}
catch {}
}
};
+20
View File
@@ -0,0 +1,20 @@
const {
collectCookiesFromSaucer
} = require(
"../../utils/crumbCollect"
);
module.exports = {
name:
"crumbSaucerCollect",
async execute(
interaction
) {
return collectCookiesFromSaucer(
interaction
);
}
};
+20
View File
@@ -0,0 +1,20 @@
const {
showCollectPage
} = require(
"../../utils/crumbCollect"
);
module.exports = {
name:
"crumbSaucerCollectMenu",
async execute(
interaction
) {
return showCollectPage(
interaction
);
}
};
+301
View File
@@ -0,0 +1,301 @@
const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle
} = require("discord.js");
const {
getCookieBalance,
getCooldown,
getUnixTime,
temporaryEphemeralReply
} = require(
"../../utils/cookieEconomy"
);
const {
HEIST_COOLDOWN
} = require(
"../../utils/cookieHeist"
);
module.exports = {
name:
"crumbSaucerHeist",
async execute(interaction) {
/*
* Verify ownership of the Crumb Saucer menu.
*/
const footer =
interaction.message
.embeds[0]
?.footer
?.text;
const [
type,
ownerId
] =
footer?.split(":")
?? [];
if (
type !== "crumbSaucer" ||
ownerId !== interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This isn't your Crumb Saucer session."
}
);
}
/*
* ====================================================
* SHARED HEIST COOLDOWN PRE-CHECK
* ====================================================
*/
const now =
getUnixTime();
const cooldown =
await getCooldown(
interaction.user.id,
interaction.guildId,
"heist"
);
if (
cooldown >
now
) {
return temporaryEphemeralReply(
interaction,
{
content:
`🏦 The ShaiBot Vault is still on alert.\nYou can attempt another heist <t:${cooldown}:R>.`
},
6000
);
}
/*
* Balances
*/
const vaultId =
interaction.client.user.id;
const [
vault,
player
] =
await Promise.all([
getCookieBalance(
vaultId
),
getCookieBalance(
interaction.user.id
)
]);
if (
player <
1
) {
return temporaryEphemeralReply(
interaction,
{
content:
"You need at least **1 Cookie** before attempting a vault heist."
}
);
}
if (
vault <
1
) {
return temporaryEphemeralReply(
interaction,
{
content:
"The ShaiBot Vault is currently empty."
}
);
}
/*
* Heist menu
*/
const embed =
new EmbedBuilder()
.setTitle(
"🏦 ShaiBot Cookie Vault"
)
.setDescription(
[
`**Vault Balance:** ${vault.toLocaleString()} 🍪`,
`**Security:** ${getSecurityLabel(vault)}`,
"",
"Choose your approach.",
"",
"🥷 **Sneak**",
"High chance • Small haul • Small fine",
"",
"💻 **Hack**",
"Medium chance • Medium haul • Medium fine",
"",
"💥 **Smash**",
"Low chance • Large haul • Large fine"
].join("\n")
)
.setFooter({
text:
`cookieHeist:${interaction.user.id}`
});
const heistButtons =
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"cookieHeistSneak"
)
.setLabel(
"Sneak"
)
.setEmoji(
"🥷"
)
.setStyle(
ButtonStyle.Primary
),
new ButtonBuilder()
.setCustomId(
"cookieHeistHack"
)
.setLabel(
"Hack"
)
.setEmoji(
"💻"
)
.setStyle(
ButtonStyle.Secondary
),
new ButtonBuilder()
.setCustomId(
"cookieHeistSmash"
)
.setLabel(
"Smash"
)
.setEmoji(
"💥"
)
.setStyle(
ButtonStyle.Danger
)
);
const backButton =
new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId(
"crumbSaucerBack"
)
.setLabel(
"Back to Crumb Saucer"
)
.setEmoji(
"◀️"
)
.setStyle(
ButtonStyle.Secondary
)
);
return interaction.update({
embeds: [
embed
],
components: [
heistButtons,
backButton
]
});
}
};
/*
* SECURITY LABEL
*/
function getSecurityLabel(
vault
) {
if (
vault <
1000
) {
return "🟢 Low";
}
if (
vault <
10000
) {
return "🟡 Moderate";
}
if (
vault <
50000
) {
return "🟠 High";
}
if (
vault <
250000
) {
return "🔴 Very High";
}
return "🟣 Maximum";
}
+27
View File
@@ -0,0 +1,27 @@
const {
buildPokerHubEmbed,
buildPokerHubComponents,
deferPokerReply
} = require("../../utils/pokerDiscord");
module.exports = {
name:
"crumbSaucerPoker",
async execute(
interaction
) {
await deferPokerReply(
interaction
);
return interaction.editReply({
embeds: [
buildPokerHubEmbed()
],
components:
buildPokerHubComponents()
});
}
};
+58
View File
@@ -0,0 +1,58 @@
const {
buildProfileOverview,
buildProfileButtons
} = require("../../utils/profileSystem");
const {
temporaryEphemeralReply
} = require("../../utils/cookieEconomy");
module.exports = {
name: "crumbSaucerProfile",
async execute(interaction) {
const footer =
interaction.message
?.embeds?.[0]
?.footer
?.text;
const [
type,
ownerId
] = footer?.split(":") ?? [];
if (
type !== "crumbSaucer" ||
ownerId !== interaction.user.id
) {
return temporaryEphemeralReply(
interaction,
{
content:
"This isn't your Crumb Saucer session."
}
);
}
const embed =
await buildProfileOverview(
interaction.guildId,
interaction.user,
interaction.user.id
);
return interaction.update({
embeds: [embed],
components:
buildProfileButtons(
"overview",
0,
1
)
});
}
};
+17
View File
@@ -0,0 +1,17 @@
const {
showRoulette
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbSaucerRoulette",
async execute(interaction) {
return showRoulette(
interaction
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
startRouletteAllIn
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteAllIn",
async execute(
interaction
) {
return startRouletteAllIn(
interaction
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
chooseRouletteBet
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteBet10",
async execute(interaction) {
return chooseRouletteBet(
interaction,
10
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
chooseRouletteBet
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteBet100",
async execute(interaction) {
return chooseRouletteBet(
interaction,
100
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
chooseRouletteBet
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteBet1000",
async execute(interaction) {
return chooseRouletteBet(
interaction,
1000
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
chooseRouletteBet
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteBet50",
async execute(interaction) {
return chooseRouletteBet(
interaction,
50
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
chooseRouletteBet
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteBet500",
async execute(interaction) {
return chooseRouletteBet(
interaction,
500
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
playRoulette
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteBlack",
async execute(interaction) {
return playRoulette(
interaction,
"Black"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
playRoulette
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteEven",
async execute(interaction) {
return playRoulette(
interaction,
"Even"
);
}
};
@@ -1,18 +1,18 @@
const { const {
joinBoss playRoulette
} = require( } = require(
"../../utils/cookieBoss" "../../utils/crumbRoulette"
); );
module.exports = { module.exports = {
name: name:
"cookieBossJoinChampion", "crumbRouletteOdd",
async execute(interaction) { async execute(interaction) {
return joinBoss( return playRoulette(
interaction, interaction,
"Champion" "Odd"
); );
} }
}; };
+18
View File
@@ -0,0 +1,18 @@
const {
playRoulette
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteRed",
async execute(interaction) {
return playRoulette(
interaction,
"Red"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
runRouletteBack
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteRunItBack",
async execute(
interaction
) {
return runRouletteBack(
interaction
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
playRoulette
} = require(
"../../utils/crumbRoulette"
);
module.exports = {
name:
"crumbRouletteZero",
async execute(interaction) {
return playRoulette(
interaction,
"Zero"
);
}
};
@@ -1,16 +1,17 @@
const { const {
startBossBattle showSlots
} = require( } = require(
"../../utils/cookieBoss" "../../utils/crumbSlots.js"
); );
module.exports = { module.exports = {
name: name:
"cookieBossStart", "crumbSaucerSlots",
async execute(interaction) { async execute(interaction) {
return startBossBattle( return showSlots(
interaction interaction
); );
} }
@@ -1,18 +1,19 @@
const { const {
joinBoss playSlots
} = require( } = require(
"../../utils/cookieBoss" "../../utils/crumbSlots.js"
); );
module.exports = { module.exports = {
name: name:
"cookieBossJoinScout", "crumbSaucerSlots10",
async execute(interaction) { async execute(interaction) {
return joinBoss( return playSlots(
interaction, interaction,
"Scout" 10
); );
} }
}; };
@@ -1,18 +1,19 @@
const { const {
joinBoss playSlots
} = require( } = require(
"../../utils/cookieBoss" "../../utils/crumbSlots.js"
); );
module.exports = { module.exports = {
name: name:
"cookieBossJoinFighter", "crumbSaucerSlots100",
async execute(interaction) { async execute(interaction) {
return joinBoss( return playSlots(
interaction, interaction,
"Fighter" 100
); );
} }
}; };
+19
View File
@@ -0,0 +1,19 @@
const {
playSlots
} = require(
"../../utils/crumbSlots.js"
);
module.exports = {
name:
"crumbSaucerSlots1000",
async execute(interaction) {
return playSlots(
interaction,
1000
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
playSlots
} = require(
"../../utils/crumbSlots.js"
);
module.exports = {
name:
"crumbSaucerSlots50",
async execute(interaction) {
return playSlots(
interaction,
50
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
playSlots
} = require(
"../../utils/crumbSlots.js"
);
module.exports = {
name:
"crumbSaucerSlots500",
async execute(interaction) {
return playSlots(
interaction,
500
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
runSlotsBack
} = require(
"../../utils/crumbSlots.js"
);
module.exports = {
name:
"crumbSaucerSlotsRunItBack",
async execute(
interaction
) {
return runSlotsBack(
interaction
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
performCrumbleStrikeAction
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeDefend",
async execute(interaction) {
return performCrumbleStrikeAction(
interaction,
"Defend"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
performCrumbleStrikeAction
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeHeal",
async execute(interaction) {
return performCrumbleStrikeAction(
interaction,
"Heal"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobAstrologian",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Healer",
"Astrologian"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobBard",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"DPS",
"Bard"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobBlackMage",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"DPS",
"BlackMage"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobDarkKnight",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Tank",
"DarkKnight"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobDragoon",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"DPS",
"Dragoon"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobPaladin",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Tank",
"Paladin"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobScholar",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Healer",
"Scholar"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobWarrior",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Tank",
"Warrior"
);
}
};
+19
View File
@@ -0,0 +1,19 @@
const {
chooseCrumbleStrikeJob
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeJobWhiteMage",
async execute(interaction) {
return chooseCrumbleStrikeJob(
interaction,
"Healer",
"WhiteMage"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
performCrumbleStrikeAction
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeLimitBreak",
async execute(interaction) {
return performCrumbleStrikeAction(
interaction,
"LimitBreak"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
performCrumbleStrikeAction
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeRevive",
async execute(interaction) {
return performCrumbleStrikeAction(
interaction,
"Revive"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
chooseCrumbleStrikeRole
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeRoleDPS",
async execute(interaction) {
return chooseCrumbleStrikeRole(
interaction,
"DPS"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
chooseCrumbleStrikeRole
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeRoleHealer",
async execute(interaction) {
return chooseCrumbleStrikeRole(
interaction,
"Healer"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
chooseCrumbleStrikeRole
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeRoleTank",
async execute(interaction) {
return chooseCrumbleStrikeRole(
interaction,
"Tank"
);
}
};
+17
View File
@@ -0,0 +1,17 @@
const {
startCrumbleStrikeBattle
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeStart",
async execute(interaction) {
return startCrumbleStrikeBattle(
interaction
);
}
};
@@ -1,16 +1,16 @@
const { const {
performBossAction performCrumbleStrikeAction
} = require( } = require(
"../../utils/cookieBoss" "../../utils/crumbleStrike"
); );
module.exports = { module.exports = {
name: name:
"cookieBossStrike", "crumbleStrikeStrike",
async execute(interaction) { async execute(interaction) {
return performBossAction( return performCrumbleStrikeAction(
interaction, interaction,
"Strike" "Strike"
); );
+18
View File
@@ -0,0 +1,18 @@
const {
confirmCrumbleStrikeJoin
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeTierAdventurer",
async execute(interaction) {
return confirmCrumbleStrikeJoin(
interaction,
"Adventurer"
);
}
};
+18
View File
@@ -0,0 +1,18 @@
const {
confirmCrumbleStrikeJoin
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeTierSprout",
async execute(interaction) {
return confirmCrumbleStrikeJoin(
interaction,
"Sprout"
);
}
};
@@ -0,0 +1,18 @@
const {
confirmCrumbleStrikeJoin
} = require(
"../../utils/crumbleStrike"
);
module.exports = {
name:
"crumbleStrikeTierWarriorOfLight",
async execute(interaction) {
return confirmCrumbleStrikeJoin(
interaction,
"WarriorOfLight"
);
}
};
+17
View File
@@ -0,0 +1,17 @@
const {
executePokerAction
} = require("../../utils/pokerInteractions");
module.exports = {
name: "pokerAllIn",
async execute(interaction) {
return executePokerAction(
interaction,
{
type: "all_in"
},
"✅ All-In."
);
}
};
+47
View File
@@ -0,0 +1,47 @@
const {
getPokerService
} = require("../../utils/crumbPoker");
const {
deferPokerReply,
editPokerError,
buildJoinPicker
} = require("../../utils/pokerDiscord");
module.exports = {
name:
"pokerBrowse",
async execute(
interaction
) {
await deferPokerReply(
interaction
);
try {
const tables =
await getPokerService()
.listJoinableTables({
guildId:
interaction.guildId,
channelId:
interaction.channelId
});
return interaction.editReply(
buildJoinPicker(
tables
)
);
}
catch (error) {
return editPokerError(
interaction,
error
);
}
}
};
+17
View File
@@ -0,0 +1,17 @@
const {
executePokerAction
} = require("../../utils/pokerInteractions");
module.exports = {
name: "pokerCall",
async execute(interaction) {
return executePokerAction(
interaction,
{
type: "call"
},
"✅ Called."
);
}
};
+85
View File
@@ -0,0 +1,85 @@
const {
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle
} = require("discord.js");
const {
getPokerService
} = require("../../utils/crumbPoker");
const {
POKER_TABLE_FOOTER_PREFIX,
deferPokerReply,
editPokerError,
getPokerGameIdFromInteraction
} = require("../../utils/pokerDiscord");
module.exports = {
name: "pokerCancel",
async execute(interaction) {
await deferPokerReply(interaction);
const gameId =
getPokerGameIdFromInteraction(interaction);
if (!gameId) {
return interaction.editReply({
content: "❌ This Poker table is no longer active.",
embeds: [],
components: []
});
}
try {
const game =
await getPokerService().getTable({ gameId });
if (
String(game.data.ownerId) !==
String(interaction.user.id)
) {
return interaction.editReply({
content: "❌ Only the table owner can cancel this Poker table.",
embeds: [],
components: []
});
}
return interaction.editReply({
content: "",
embeds: [
new EmbedBuilder()
.setTitle("🛑 Cancel Poker Table?")
.setDescription(
[
"All seated players will be safely cashed out.",
"",
"This cannot be undone."
].join("\n")
)
.setFooter({
text: `${POKER_TABLE_FOOTER_PREFIX}:${gameId}`
})
],
components: [
new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId("pokerCancelConfirm")
.setLabel("Cancel Table")
.setEmoji("🛑")
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId("pokerCancelKeep")
.setLabel("Keep Table")
.setStyle(ButtonStyle.Secondary)
)
]
});
} catch (error) {
return editPokerError(interaction, error);
}
}
};
+50
View File
@@ -0,0 +1,50 @@
const {
getPokerService
} = require("../../utils/crumbPoker");
const {
deferPokerReply,
editPokerError,
getPokerGameIdFromInteraction,
refreshPokerTableMessage
} = require("../../utils/pokerDiscord");
module.exports = {
name: "pokerCancelConfirm",
async execute(interaction) {
await deferPokerReply(interaction);
const gameId =
getPokerGameIdFromInteraction(interaction);
if (!gameId) {
return interaction.editReply({
content: "❌ This Poker table is no longer active.",
embeds: [],
components: []
});
}
try {
const game =
await getPokerService().cancelTable({
gameId,
userId: interaction.user.id
});
await refreshPokerTableMessage(
interaction.client,
game
);
try {
await interaction.deleteReply();
} catch {}
return;
} catch (error) {
return editPokerError(interaction, error);
}
}
};
+17
View File
@@ -0,0 +1,17 @@
const {
deferPokerReply
} = require("../../utils/pokerDiscord");
module.exports = {
name: "pokerCancelKeep",
async execute(interaction) {
await deferPokerReply(interaction);
try {
await interaction.deleteReply();
} catch {}
return;
}
};
+151
View File
@@ -0,0 +1,151 @@
const {
cardToString
} = require("../../utils/pokerEngine");
const {
getPokerService
} = require("../../utils/crumbPoker");
const {
POKER_CARD_RESPONSE_LIFETIME,
deferPokerReply,
editPokerError,
getPokerGameIdFromInteraction
} = require("../../utils/pokerDiscord");
module.exports = {
name:
"pokerCards",
async execute(
interaction
) {
await deferPokerReply(
interaction
);
const gameId =
getPokerGameIdFromInteraction(
interaction
);
if (!gameId) {
return interaction.editReply({
content:
"❌ This Poker table is no longer active.",
embeds: [],
components: []
});
}
try {
const service =
getPokerService();
const [
cards,
game
] =
await Promise.all([
service.viewCards({
gameId,
userId:
interaction.user.id
}),
service.getTable({
gameId
})
]);
const hand =
game.data.hand;
const player =
game.data.players.find(
candidate =>
String(candidate.userId) ===
String(interaction.user.id)
);
const currentPlayer =
hand?.players?.find(
candidate =>
candidate.seat ===
hand.currentSeat
);
const isYourTurn =
Boolean(
player
&&
hand
&&
hand.currentSeat ===
player.seat
);
const communityCards =
hand?.communityCards ?? [];
const lines = [
"### 🃏 Your Cards",
`**${cards.map(cardToString).join(" ")}**`,
"",
"**Community:** " +
(communityCards.length
? communityCards
.map(cardToString)
.join(" ")
: "No community cards yet."),
""
];
if (isYourTurn) {
lines.push(
"👉 **It's YOUR TURN.**",
game.data.turnExpiresAt
? `⏱️ Auto-folds <t:${game.data.turnExpiresAt}:R>.`
: "⏱️ Your turn timer is active."
);
}
else if (currentPlayer) {
lines.push(
`Waiting for <@${currentPlayer.userId}> to act.`
);
}
else {
lines.push(
"The hand is not currently waiting on a player action."
);
}
await interaction.editReply({
content:
lines.join("\n"),
embeds: [],
components: []
});
setTimeout(
() => {
interaction
.deleteReply()
.catch(() => {});
},
POKER_CARD_RESPONSE_LIFETIME
);
return;
}
catch (error) {
return editPokerError(
interaction,
error
);
}
}
};
+67
View File
@@ -0,0 +1,67 @@
const {
getPokerService
} = require("../../utils/crumbPoker");
const {
deferPokerReply,
editPokerError,
getPokerGameIdFromInteraction,
refreshPokerTableMessage
} = require("../../utils/pokerDiscord");
module.exports = {
name:
"pokerCashOut",
async execute(
interaction
) {
await deferPokerReply(
interaction
);
const gameId =
getPokerGameIdFromInteraction(
interaction
);
if (!gameId) {
return interaction.editReply({
content:
"❌ This Poker table is no longer active.",
embeds: [],
components: []
});
}
try {
const result =
await getPokerService()
.cashOut({
gameId,
userId:
interaction.user.id
});
await refreshPokerTableMessage(
interaction.client,
result.game
);
return interaction.editReply({
content:
`✅ Cashed out **${result.amount.toLocaleString()} Cookies** back to your vault.`,
embeds: [],
components: []
});
}
catch (error) {
return editPokerError(
interaction,
error
);
}
}
};
+17
View File
@@ -0,0 +1,17 @@
const {
executePokerAction
} = require("../../utils/pokerInteractions");
module.exports = {
name: "pokerCheck",
async execute(interaction) {
return executePokerAction(
interaction,
{
type: "check"
},
"✅ Checked."
);
}
};
+81
View File
@@ -0,0 +1,81 @@
const {
getPokerService
} = require("../../utils/crumbPoker");
const {
deferPokerReply,
editPokerError,
publishPokerTable,
buildOpenTableLink
} = require("../../utils/pokerDiscord");
module.exports = {
name:
"pokerCreate",
async execute(
interaction
) {
await deferPokerReply(
interaction
);
const service =
getPokerService();
let game = null;
try {
game =
await service.createTable({
guildId:
interaction.guildId,
channelId:
interaction.channelId,
ownerId:
interaction.user.id
});
const published =
await publishPokerTable(
interaction,
game
);
return interaction.editReply({
content:
`✅ Poker table created. **${published.game.data.buyIn.toLocaleString()} Cookies** were moved into your Poker stack.`,
embeds: [],
components:
buildOpenTableLink(
published.game
)
});
}
catch (error) {
if (game) {
try {
await service.cashOut({
gameId:
game.gameId,
userId:
interaction.user.id
});
}
catch (refundError) {
console.error(
`[Poker] Failed to refund unpublished table ${game.gameId}:`,
refundError
);
}
}
return editPokerError(
interaction,
error
);
}
}
};
+17
View File
@@ -0,0 +1,17 @@
const {
executePokerAction
} = require("../../utils/pokerInteractions");
module.exports = {
name: "pokerFold",
async execute(interaction) {
return executePokerAction(
interaction,
{
type: "fold"
},
"✅ Folded."
);
}
};
+73
View File
@@ -0,0 +1,73 @@
const {
getPokerService
} = require("../../utils/crumbPoker");
const {
deferPokerReply,
editPokerError,
getPokerGameIdFromInteraction,
refreshPokerTableMessage,
buildOpenTableLink
} = require("../../utils/pokerDiscord");
module.exports = {
name:
"pokerJoinTable",
async execute(
interaction
) {
await deferPokerReply(
interaction
);
const gameId =
getPokerGameIdFromInteraction(
interaction
);
if (!gameId) {
return interaction.editReply({
content:
"❌ This Poker table is no longer active.",
embeds: [],
components: []
});
}
try {
const game =
await getPokerService()
.joinTable({
gameId,
guildId:
interaction.guildId,
userId:
interaction.user.id
});
await refreshPokerTableMessage(
interaction.client,
game
);
return interaction.editReply({
content:
`✅ Joined the Poker table for **${game.data.buyIn.toLocaleString()} Cookies**.`,
embeds: [],
components:
buildOpenTableLink(
game
)
});
}
catch (error) {
return editPokerError(
interaction,
error
);
}
}
};
+66
View File
@@ -0,0 +1,66 @@
const {
getPokerService
} = require("../../utils/crumbPoker");
const {
deferPokerReply,
editPokerError,
refreshPokerTableMessage,
buildOpenTableLink
} = require("../../utils/pokerDiscord");
module.exports = {
name:
"pokerMyTable",
async execute(
interaction
) {
await deferPokerReply(
interaction
);
try {
const game =
await getPokerService()
.getMyTable({
guildId:
interaction.guildId,
userId:
interaction.user.id
});
if (!game) {
return interaction.editReply({
content:
"❌ You are not currently seated at a Poker table.",
embeds: [],
components: []
});
}
await refreshPokerTableMessage(
interaction.client,
game
);
return interaction.editReply({
content:
"♠️ Your active Poker table:",
embeds: [],
components:
buildOpenTableLink(
game
)
});
}
catch (error) {
return editPokerError(
interaction,
error
);
}
}
};
+13
View File
@@ -0,0 +1,13 @@
const {
executeMinimumRaise
} = require("../../utils/pokerInteractions");
module.exports = {
name: "pokerRaise",
async execute(interaction) {
return executeMinimumRaise(
interaction
);
}
};
+65
View File
@@ -0,0 +1,65 @@
const {
getPokerService
} = require("../../utils/crumbPoker");
const {
deferPokerReply,
editPokerError,
getPokerGameIdFromInteraction,
refreshPokerTableMessage
} = require("../../utils/pokerDiscord");
module.exports = {
name:
"pokerRefresh",
async execute(
interaction
) {
await deferPokerReply(
interaction
);
const gameId =
getPokerGameIdFromInteraction(
interaction
);
if (!gameId) {
return interaction.editReply({
content:
"❌ This Poker table is no longer active.",
embeds: [],
components: []
});
}
try {
const game =
await getPokerService()
.getTable({
gameId
});
await refreshPokerTableMessage(
interaction.client,
game
);
return interaction.editReply({
content:
"✅ Poker table refreshed.",
embeds: [],
components: []
});
}
catch (error) {
return editPokerError(
interaction,
error
);
}
}
};
+67
View File
@@ -0,0 +1,67 @@
const {
getPokerService
} = require("../../utils/crumbPoker");
const {
deferPokerReply,
editPokerError,
getPokerGameIdFromInteraction,
refreshPokerTableMessage
} = require("../../utils/pokerDiscord");
module.exports = {
name:
"pokerStart",
async execute(
interaction
) {
await deferPokerReply(
interaction
);
const gameId =
getPokerGameIdFromInteraction(
interaction
);
if (!gameId) {
return interaction.editReply({
content:
"❌ This Poker table is no longer active.",
embeds: [],
components: []
});
}
try {
const game =
await getPokerService()
.startTableHand({
gameId,
userId:
interaction.user.id
});
await refreshPokerTableMessage(
interaction.client,
game
);
return interaction.editReply({
content:
`✅ Hand **#${game.data.handNumber}** started. Cards have been dealt — use **🃏 Cards** to view your hand.`,
embeds: [],
components: []
});
}
catch (error) {
return editPokerError(
interaction,
error
);
}
}
};
+54
View File
@@ -0,0 +1,54 @@
const {
buildAchievementsEmbed,
buildProfileButtons,
getProfileContextFromFooter,
requireProfileOwner
} = require(
"../../utils/profileSystem"
);
module.exports = {
name:
"profileAchievements",
async execute(
interaction
) {
const context =
await requireProfileOwner(
interaction
);
if (!context) {
return;
}
const user =
await interaction.client.users.fetch(
context.profileUserId
);
const embed =
await buildAchievementsEmbed(
interaction.guildId,
user,
context.sessionOwnerId,
0
);
const pageContext =
getProfileContextFromFooter(
embed.data.footer?.text
);
return interaction.update({
embeds: [embed],
components:
buildProfileButtons(
"achievements",
pageContext?.page ?? 0,
pageContext?.pageCount ?? 1
)
});
}
};
+54
View File
@@ -0,0 +1,54 @@
const {
buildAchievementsEmbed,
buildProfileButtons,
getProfileContextFromFooter,
requireProfileOwner
} = require(
"../../utils/profileSystem"
);
module.exports = {
name:
"profileAchievementsLeft",
async execute(
interaction
) {
const context =
await requireProfileOwner(
interaction
);
if (!context) {
return;
}
const user =
await interaction.client.users.fetch(
context.profileUserId
);
const embed =
await buildAchievementsEmbed(
interaction.guildId,
user,
context.sessionOwnerId,
context.page - 1
);
const pageContext =
getProfileContextFromFooter(
embed.data.footer?.text
);
return interaction.update({
embeds: [embed],
components:
buildProfileButtons(
"achievements",
pageContext?.page ?? 0,
pageContext?.pageCount ?? 1
)
});
}
};
+54
View File
@@ -0,0 +1,54 @@
const {
buildAchievementsEmbed,
buildProfileButtons,
getProfileContextFromFooter,
requireProfileOwner
} = require(
"../../utils/profileSystem"
);
module.exports = {
name:
"profileAchievementsRight",
async execute(
interaction
) {
const context =
await requireProfileOwner(
interaction
);
if (!context) {
return;
}
const user =
await interaction.client.users.fetch(
context.profileUserId
);
const embed =
await buildAchievementsEmbed(
interaction.guildId,
user,
context.sessionOwnerId,
context.page + 1
);
const pageContext =
getProfileContextFromFooter(
embed.data.footer?.text
);
return interaction.update({
embeds: [embed],
components:
buildProfileButtons(
"achievements",
pageContext?.page ?? 0,
pageContext?.pageCount ?? 1
)
});
}
};
+54
View File
@@ -0,0 +1,54 @@
const {
buildBadgesEmbed,
buildProfileButtons,
getProfileContextFromFooter,
requireProfileOwner
} = require(
"../../utils/profileSystem"
);
module.exports = {
name:
"profileBadges",
async execute(
interaction
) {
const context =
await requireProfileOwner(
interaction
);
if (!context) {
return;
}
const user =
await interaction.client.users.fetch(
context.profileUserId
);
const embed =
await buildBadgesEmbed(
interaction.guildId,
user,
context.sessionOwnerId,
0
);
const pageContext =
getProfileContextFromFooter(
embed.data.footer?.text
);
return interaction.update({
embeds: [embed],
components:
buildProfileButtons(
"badges",
pageContext?.page ?? 0,
pageContext?.pageCount ?? 1
)
});
}
};
+54
View File
@@ -0,0 +1,54 @@
const {
buildBadgesEmbed,
buildProfileButtons,
getProfileContextFromFooter,
requireProfileOwner
} = require(
"../../utils/profileSystem"
);
module.exports = {
name:
"profileBadgesLeft",
async execute(
interaction
) {
const context =
await requireProfileOwner(
interaction
);
if (!context) {
return;
}
const user =
await interaction.client.users.fetch(
context.profileUserId
);
const embed =
await buildBadgesEmbed(
interaction.guildId,
user,
context.sessionOwnerId,
context.page - 1
);
const pageContext =
getProfileContextFromFooter(
embed.data.footer?.text
);
return interaction.update({
embeds: [embed],
components:
buildProfileButtons(
"badges",
pageContext?.page ?? 0,
pageContext?.pageCount ?? 1
)
});
}
};
+54
View File
@@ -0,0 +1,54 @@
const {
buildBadgesEmbed,
buildProfileButtons,
getProfileContextFromFooter,
requireProfileOwner
} = require(
"../../utils/profileSystem"
);
module.exports = {
name:
"profileBadgesRight",
async execute(
interaction
) {
const context =
await requireProfileOwner(
interaction
);
if (!context) {
return;
}
const user =
await interaction.client.users.fetch(
context.profileUserId
);
const embed =
await buildBadgesEmbed(
interaction.guildId,
user,
context.sessionOwnerId,
context.page + 1
);
const pageContext =
getProfileContextFromFooter(
embed.data.footer?.text
);
return interaction.update({
embeds: [embed],
components:
buildProfileButtons(
"badges",
pageContext?.page ?? 0,
pageContext?.pageCount ?? 1
)
});
}
};
+60
View File
@@ -0,0 +1,60 @@
const {
EmbedBuilder
} = require(
"discord.js"
);
const {
requireProfileOwner
} = require(
"../../utils/profileSystem"
);
function delay(
ms
) {
return new Promise(
resolve =>
setTimeout(
resolve,
ms
)
);
}
module.exports = {
name:
"profileClose",
async execute(
interaction
) {
const context =
await requireProfileOwner(
interaction
);
if (!context) {
return;
}
await interaction.update({
embeds: [
new EmbedBuilder()
.setTitle(
"Profile Closed"
)
.setDescription(
"Closing profile..."
)
],
components: []
});
await delay(1200);
try {
await interaction.deleteReply();
} catch {}
}
};
+47
View File
@@ -0,0 +1,47 @@
const {
buildProfileOverview,
buildProfileButtons,
requireProfileOwner
} = require(
"../../utils/profileSystem"
);
module.exports = {
name:
"profileOverview",
async execute(
interaction
) {
const context =
await requireProfileOwner(
interaction
);
if (!context) {
return;
}
const user =
await interaction.client.users.fetch(
context.profileUserId
);
const embed =
await buildProfileOverview(
interaction.guildId,
user,
context.sessionOwnerId
);
return interaction.update({
embeds: [embed],
components:
buildProfileButtons(
"overview",
0,
1
)
});
}
};
+47
View File
@@ -0,0 +1,47 @@
const {
buildStatsEmbed,
buildProfileButtons,
requireProfileOwner
} = require(
"../../utils/profileSystem"
);
module.exports = {
name:
"profileStats",
async execute(
interaction
) {
const context =
await requireProfileOwner(
interaction
);
if (!context) {
return;
}
const user =
await interaction.client.users.fetch(
context.profileUserId
);
const embed =
await buildStatsEmbed(
interaction.guildId,
user,
context.sessionOwnerId
);
return interaction.update({
embeds: [embed],
components:
buildProfileButtons(
"stats",
0,
1
)
});
}
};
+64
View File
@@ -0,0 +1,64 @@
const {
SlashCommandBuilder,
EmbedBuilder,
MessageFlags
} = require("discord.js");
const {
declareBankruptcy
} = require("../../utils/bankruptcy");
module.exports = {
data: new SlashCommandBuilder()
.setName("bankruptcy")
.setDescription(
"Declare bankruptcy at exactly 0 Cookies and receive a starter kit."
),
async execute(interaction) {
const result = await declareBankruptcy(interaction);
if (!result.ok) {
if (result.reason === "COOLDOWN") {
return interaction.reply({
content:
`Bankruptcy assistance is unavailable until <t:${result.nextAvailableAt}:R>.`,
flags: MessageFlags.Ephemeral
});
}
if (result.reason === "BALANCE_NOT_ZERO") {
return interaction.reply({
content:
`You may only declare bankruptcy at exactly **0 Cookies**. Your current balance is **${result.balance.toLocaleString()}**.`,
flags: MessageFlags.Ephemeral
});
}
return interaction.reply({
content: "Bankruptcy could not be declared.",
flags: MessageFlags.Ephemeral
});
}
const embed = new EmbedBuilder()
.setTitle("🍪 Bankruptcy Declared")
.setDescription(
[
`${interaction.user} has been issued a fresh starter kit.`,
"",
`**+${result.amount.toLocaleString()} Cookies**`,
"",
"The Cookie economy believes in second chances."
].join("\n")
)
.addFields({
name: "Next Eligibility",
value: `<t:${result.nextAvailableAt}:R>`
});
return interaction.reply({
embeds: [embed]
});
}
};
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
const {
SlashCommandBuilder
} = require("discord.js");
const {
showCrumbSaucer
} = require("../../utils/crumbSaucer");
module.exports = {
data: new SlashCommandBuilder()
.setName("crumbsaucer")
.setDescription(
"Visit the Crumb Saucer."
),
async execute(interaction) {
return showCrumbSaucer(
interaction
);
}
};
File diff suppressed because it is too large Load Diff
+63
View File
@@ -0,0 +1,63 @@
const {
SlashCommandBuilder
} = require(
"discord.js"
);
const {
buildProfileOverview,
buildProfileButtons
} = require(
"../../utils/profileSystem"
);
module.exports = {
data:
new SlashCommandBuilder()
.setName(
"profile"
)
.setDescription(
"View a player profile"
)
.addUserOption(
option =>
option
.setName(
"user"
)
.setDescription(
"Player to view"
)
),
async execute(
interaction
) {
const user =
interaction.options.getUser(
"user"
) ??
interaction.user;
const sessionOwnerId =
interaction.user.id;
const embed =
await buildProfileOverview(
interaction.guildId,
user,
sessionOwnerId
);
return interaction.reply({
embeds: [embed],
components:
buildProfileButtons(
"overview",
0,
1
)
});
}
};
+629
View File
@@ -0,0 +1,629 @@
const {
SlashCommandBuilder,
MessageFlags,
EmbedBuilder
} = require("discord.js");
const {
reconcileUserAchievements,
reconcileAllAchievements,
grantMaintenanceReward,
getMaintenanceRewardStatus,
auditProfileData,
DEFAULT_MAINTENANCE_REWARD_ID,
DEFAULT_MAINTENANCE_REWARD_AMOUNT
} = require(
"../../utils/profileMaintenance"
);
const {
requireAdminAccess
} = require(
"../../utils/permissions"
);
module.exports = {
data:
new SlashCommandBuilder()
.setName(
"profileadmin"
)
.setDescription(
"Administrative profile maintenance tools"
)
.addSubcommand(
subcommand =>
subcommand
.setName(
"reconcile"
)
.setDescription(
"Backfill database-trackable achievements"
)
.addUserOption(
option =>
option
.setName(
"user"
)
.setDescription(
"Only reconcile this user; omit for everyone"
)
)
.addBooleanOption(
option =>
option
.setName(
"dry-run"
)
.setDescription(
"Show what would change without writing it"
)
)
)
.addSubcommand(
subcommand =>
subcommand
.setName(
"audit"
)
.setDescription(
"Audit stored profile statistics for inconsistencies"
)
)
.addSubcommand(
subcommand =>
subcommand
.setName(
"maintenance-reward"
)
.setDescription(
"Grant an idempotent maintenance reward to stored profile users"
)
.addStringOption(
option =>
option
.setName(
"reward-id"
)
.setDescription(
`Unique reward ID; defaults to ${DEFAULT_MAINTENANCE_REWARD_ID}`
)
)
.addIntegerOption(
option =>
option
.setName(
"amount"
)
.setDescription(
`Cookies per player; defaults to ${DEFAULT_MAINTENANCE_REWARD_AMOUNT}`
)
.setMinValue(
1
)
)
.addBooleanOption(
option =>
option
.setName(
"dry-run"
)
.setDescription(
"Preview recipients without granting Cookies"
)
)
)
.addSubcommand(
subcommand =>
subcommand
.setName(
"maintenance-status"
)
.setDescription(
"Show maintenance reward claim status"
)
.addStringOption(
option =>
option
.setName(
"reward-id"
)
.setDescription(
`Reward ID; defaults to ${DEFAULT_MAINTENANCE_REWARD_ID}`
)
)
),
async execute(
interaction
) {
/*
* Allows:
*
* - Discord Administrators
* - BOT_AUTHOR_ID from .env
*/
if (
!await requireAdminAccess(
interaction
)
) {
return;
}
const subcommand =
interaction.options.getSubcommand();
await interaction.deferReply({
flags:
MessageFlags.Ephemeral
});
try {
if (
subcommand ===
"reconcile"
) {
const user =
interaction.options.getUser(
"user"
);
const dryRun =
interaction.options.getBoolean(
"dry-run"
) ?? false;
if (user) {
const result =
await reconcileUserAchievements({
guildId:
interaction.guildId,
userId:
user.id,
dryRun
});
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setTitle(
"🛠️ Profile Reconciliation"
)
.setDescription(
`${dryRun ? "Dry run for" : "Reconciled"} ${user}.`
)
.addFields(
{
name:
"New unlocks",
value:
`**${result.unlocked}**`,
inline:
true
},
{
name:
"Definitions checked",
value:
`**${result.checked}**`,
inline:
true
},
{
name:
"Event-only skipped",
value:
`**${result.skippedEventOnly}**`,
inline:
true
}
)
]
});
}
const summary =
await reconcileAllAchievements({
guildId:
interaction.guildId,
dryRun
});
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
}
)
]
});
}
if (
subcommand ===
"maintenance-reward"
) {
const rewardId =
interaction.options.getString(
"reward-id"
)
??
DEFAULT_MAINTENANCE_REWARD_ID;
const amount =
interaction.options.getInteger(
"amount"
)
??
DEFAULT_MAINTENANCE_REWARD_AMOUNT;
/*
* Safer admin default:
*
* If omitted, maintenance-reward previews
* instead of paying immediately.
*
* Pass dry-run:false explicitly to execute.
*/
const dryRun =
interaction.options.getBoolean(
"dry-run"
)
??
true;
const summary =
await grantMaintenanceReward({
guildId:
interaction.guildId,
rewardId,
amount,
dryRun
});
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setTitle(
dryRun
? "🧪 Maintenance Reward Dry Run"
: "🎁 Maintenance Reward"
)
.setDescription(
dryRun
? "No Cookies were granted. Run again with `dry-run:false` to execute this reward."
: `Maintenance compensation **${rewardId}** has been processed.`
)
.addFields(
{
name:
"Reward ID",
value:
`\`${summary.rewardId}\``,
inline:
false
},
{
name:
"Per player",
value:
`**${summary.amount.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
"Stored profiles",
value:
`**${summary.users}**`,
inline:
true
},
{
name:
dryRun
? "Would receive"
: "Granted",
value:
`**${dryRun ? summary.eligible : summary.granted}**`,
inline:
true
},
{
name:
"Already claimed",
value:
`**${summary.alreadyClaimed}**`,
inline:
true
},
{
name:
"Failed",
value:
`**${summary.failed}**`,
inline:
true
}
)
]
});
}
if (
subcommand ===
"maintenance-status"
) {
const rewardId =
interaction.options.getString(
"reward-id"
)
??
DEFAULT_MAINTENANCE_REWARD_ID;
const status =
await getMaintenanceRewardStatus({
guildId:
interaction.guildId,
rewardId
});
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setTitle(
"🎁 Maintenance Reward Status"
)
.addFields(
{
name:
"Reward ID",
value:
`\`${status.rewardId}\``,
inline:
false
},
{
name:
"Default amount",
value:
`**${status.amount.toLocaleString()} 🍪**`,
inline:
true
},
{
name:
"Stored profiles",
value:
`**${status.users}**`,
inline:
true
},
{
name:
"Claimed",
value:
`**${status.claimed}**`,
inline:
true
},
{
name:
"Remaining",
value:
`**${status.remaining}**`,
inline:
true
}
)
]
});
}
const audit =
await auditProfileData(
interaction.guildId
);
const issuePreview =
audit.issues
.slice(
0,
10
)
.join(
"\n"
);
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setTitle(
"🔎 Profile Data Audit"
)
.addFields(
{
name:
"Stats documents",
value:
`**${audit.statDocuments}**`,
inline:
true
},
{
name:
"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."
)
]
});
}
catch (error) {
console.error(
`[ProfileAdmin] ${subcommand} failed:`,
error
);
return interaction.editReply({
embeds: [
new EmbedBuilder()
.setTitle(
"❌ Profile Admin Error"
)
.setDescription(
"The maintenance action failed. Check the bot logs for details."
)
]
});
}
}
};

Some files were not shown because too many files have changed in this diff Show More