new games snapshot
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
const {
|
||||||
|
performBossAction
|
||||||
|
} = require(
|
||||||
|
"../../utils/cookieBoss"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
"cookieBossBurst",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
return performBossAction(
|
||||||
|
interaction,
|
||||||
|
"Burst"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const {
|
||||||
|
joinBoss
|
||||||
|
} = require(
|
||||||
|
"../../utils/cookieBoss"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
"cookieBossJoinChampion",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
return joinBoss(
|
||||||
|
interaction,
|
||||||
|
"Champion"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const {
|
||||||
|
joinBoss
|
||||||
|
} = require(
|
||||||
|
"../../utils/cookieBoss"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
"cookieBossJoinFighter",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
return joinBoss(
|
||||||
|
interaction,
|
||||||
|
"Fighter"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const {
|
||||||
|
joinBoss
|
||||||
|
} = require(
|
||||||
|
"../../utils/cookieBoss"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
"cookieBossJoinScout",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
return joinBoss(
|
||||||
|
interaction,
|
||||||
|
"Scout"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const {
|
||||||
|
performBossAction
|
||||||
|
} = require(
|
||||||
|
"../../utils/cookieBoss"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
"cookieBossLimit",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
return performBossAction(
|
||||||
|
interaction,
|
||||||
|
"Limit"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const {
|
||||||
|
startBossBattle
|
||||||
|
} = require(
|
||||||
|
"../../utils/cookieBoss"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
"cookieBossStart",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
return startBossBattle(
|
||||||
|
interaction
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const {
|
||||||
|
performBossAction
|
||||||
|
} = require(
|
||||||
|
"../../utils/cookieBoss"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
"cookieBossStrike",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
return performBossAction(
|
||||||
|
interaction,
|
||||||
|
"Strike"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const {
|
||||||
|
resolveCookieHeist
|
||||||
|
} = require(
|
||||||
|
"../../utils/cookieHeist"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
"cookieHeistHack",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
return resolveCookieHeist(
|
||||||
|
interaction,
|
||||||
|
"Hack"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const {
|
||||||
|
resolveCookieHeist
|
||||||
|
} = require(
|
||||||
|
"../../utils/cookieHeist"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
"cookieHeistSmash",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
return resolveCookieHeist(
|
||||||
|
interaction,
|
||||||
|
"Smash"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const {
|
||||||
|
resolveCookieHeist
|
||||||
|
} = require(
|
||||||
|
"../../utils/cookieHeist"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name:
|
||||||
|
"cookieHeistSneak",
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
return resolveCookieHeist(
|
||||||
|
interaction,
|
||||||
|
"Sneak"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
+1491
-698
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,230 @@
|
|||||||
|
const {
|
||||||
|
getCollections,
|
||||||
|
getUnixTime
|
||||||
|
} = require("../utils/cookieEconomy");
|
||||||
|
|
||||||
|
const {
|
||||||
|
activateBoss,
|
||||||
|
failBoss,
|
||||||
|
cancelBossAndRefund
|
||||||
|
} = require("../utils/cookieBoss");
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* How often we check persistent boss timers.
|
||||||
|
*
|
||||||
|
* Discord timestamps handle the visible countdown,
|
||||||
|
* so checking once per minute is plenty.
|
||||||
|
*/
|
||||||
|
const CHECK_INTERVAL = 60 * 1000;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Your index.js automatically loads every file
|
||||||
|
* inside /handlers and passes the Discord client:
|
||||||
|
*
|
||||||
|
* require(`./handlers/${handler}`)(client)
|
||||||
|
*/
|
||||||
|
module.exports = (client) => {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run shortly after startup.
|
||||||
|
*
|
||||||
|
* This catches boss events whose timers expired
|
||||||
|
* while ShaiBot was offline/restarting.
|
||||||
|
*/
|
||||||
|
setTimeout(() => {
|
||||||
|
processBossEvents(client);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Persistent boss event watcher.
|
||||||
|
*/
|
||||||
|
setInterval(() => {
|
||||||
|
processBossEvents(client);
|
||||||
|
}, CHECK_INTERVAL);
|
||||||
|
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[CookieBoss] Boss event watcher initialized"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check all currently pending Cookie Boss events.
|
||||||
|
*/
|
||||||
|
async function processBossEvents(client) {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
db
|
||||||
|
} = getCollections();
|
||||||
|
|
||||||
|
|
||||||
|
const events =
|
||||||
|
db.collection(
|
||||||
|
"items_events"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const now =
|
||||||
|
getUnixTime();
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ==========================================
|
||||||
|
* JOINING PHASE
|
||||||
|
* ==========================================
|
||||||
|
*
|
||||||
|
* Recruitment lasts for 30 minutes.
|
||||||
|
*
|
||||||
|
* If 5+ players joined:
|
||||||
|
* start the battle automatically.
|
||||||
|
*
|
||||||
|
* If fewer than 5 joined:
|
||||||
|
* cancel the boss and refund everyone.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const joiningEvents =
|
||||||
|
await events
|
||||||
|
.find({
|
||||||
|
eventType:
|
||||||
|
"CookieBoss",
|
||||||
|
|
||||||
|
state:
|
||||||
|
"Joining",
|
||||||
|
|
||||||
|
joinUntil: {
|
||||||
|
$lte: now
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
|
||||||
|
for (
|
||||||
|
const event
|
||||||
|
of joiningEvents
|
||||||
|
) {
|
||||||
|
const participantCount =
|
||||||
|
Object.keys(
|
||||||
|
event.participants ?? {}
|
||||||
|
).length;
|
||||||
|
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[CookieBoss] Recruitment expired for ${event.eventId} ` +
|
||||||
|
`(${participantCount}/${event.minimumParticipants} players)`
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enough players joined.
|
||||||
|
*
|
||||||
|
* Start the fight automatically.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
participantCount >=
|
||||||
|
event.minimumParticipants
|
||||||
|
) {
|
||||||
|
const started =
|
||||||
|
await activateBoss(
|
||||||
|
client,
|
||||||
|
event.eventId
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (started) {
|
||||||
|
console.log(
|
||||||
|
`[CookieBoss] Battle ${event.eventId} started automatically`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Not enough players.
|
||||||
|
*
|
||||||
|
* Nobody actually entered combat,
|
||||||
|
* so refund all committed Cookies.
|
||||||
|
*/
|
||||||
|
await cancelBossAndRefund(
|
||||||
|
client,
|
||||||
|
event.eventId
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[CookieBoss] Battle ${event.eventId} cancelled and refunded`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ==========================================
|
||||||
|
* ACTIVE BATTLES
|
||||||
|
* ==========================================
|
||||||
|
*
|
||||||
|
* If the battle timer expires while the
|
||||||
|
* boss still has HP remaining, the raid fails.
|
||||||
|
*
|
||||||
|
* All Cookies held in the encounter escrow
|
||||||
|
* are transferred to the ShaiBot vault.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const activeEvents =
|
||||||
|
await events
|
||||||
|
.find({
|
||||||
|
eventType:
|
||||||
|
"CookieBoss",
|
||||||
|
|
||||||
|
state:
|
||||||
|
"Active",
|
||||||
|
|
||||||
|
battleEndsAt: {
|
||||||
|
$lte: now
|
||||||
|
},
|
||||||
|
|
||||||
|
"boss.health": {
|
||||||
|
$gt: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
|
||||||
|
for (
|
||||||
|
const event
|
||||||
|
of activeEvents
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`[CookieBoss] Battle ${event.eventId} expired ` +
|
||||||
|
`with ${event.boss.health}/${event.boss.maxHealth} HP remaining`
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
await failBoss(
|
||||||
|
client,
|
||||||
|
event.eventId
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[CookieBoss] Battle ${event.eventId} failed`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
/*
|
||||||
|
* Never let an exception kill the interval.
|
||||||
|
*
|
||||||
|
* The next watcher tick will simply try again.
|
||||||
|
*/
|
||||||
|
console.error(
|
||||||
|
"[CookieBoss] Boss watcher error:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,141 @@
|
|||||||
const { Client, GatewayIntentBits, Collection, Partials } = require('discord.js')
|
const {
|
||||||
const { MongoClient } = require('mongodb')
|
Client,
|
||||||
require('dotenv').config()
|
GatewayIntentBits,
|
||||||
const mClient = new MongoClient(process.env.M_URI)
|
Collection,
|
||||||
exports.mClient = mClient;
|
Partials
|
||||||
const fs = require('node:fs')
|
} = require("discord.js");
|
||||||
|
|
||||||
const client = new Client({
|
const {
|
||||||
intents: [
|
MongoClient
|
||||||
GatewayIntentBits.Guilds,
|
} = require("mongodb");
|
||||||
GatewayIntentBits.GuildMembers,
|
|
||||||
GatewayIntentBits.GuildMessages,
|
|
||||||
GatewayIntentBits.GuildPresences,
|
|
||||||
GatewayIntentBits.GuildMessageReactions,
|
|
||||||
GatewayIntentBits.GuildVoiceStates,
|
|
||||||
GatewayIntentBits.DirectMessages,
|
|
||||||
GatewayIntentBits.MessageContent,
|
|
||||||
|
|
||||||
],
|
|
||||||
partials: [
|
|
||||||
Partials.Channel,
|
|
||||||
Partials.Message,
|
|
||||||
Partials.User,
|
|
||||||
Partials.GuildMember,
|
|
||||||
Partials.Reaction
|
|
||||||
]
|
|
||||||
});
|
|
||||||
exports.client = client
|
|
||||||
|
|
||||||
client.commands = new Collection() // slash commands collection
|
const fs = require("node:fs");
|
||||||
client.legacyCommands = new Collection() // legacy commands collection
|
|
||||||
client.aliases = new Collection() // list of command aliases
|
|
||||||
client.buttons = new Collection() // list of buttons
|
|
||||||
client.selectMenus = new Collection() // list of selectMenus
|
|
||||||
client.modals = new Collection();
|
|
||||||
|
|
||||||
fs.readdirSync('./handlers').forEach((handler) => {
|
require("dotenv").config();
|
||||||
require(`./handlers/${handler}`)(client)
|
|
||||||
});
|
|
||||||
|
|
||||||
client.login(process.env.D_TOKEN)
|
|
||||||
|
/*
|
||||||
|
* MONGODB
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mClient =
|
||||||
|
new MongoClient(
|
||||||
|
process.env.M_URI
|
||||||
|
);
|
||||||
|
|
||||||
|
exports.mClient =
|
||||||
|
mClient;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* DISCORD CLIENT
|
||||||
|
*/
|
||||||
|
|
||||||
|
const client =
|
||||||
|
new Client({
|
||||||
|
intents: [
|
||||||
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildMembers,
|
||||||
|
GatewayIntentBits.GuildMessages,
|
||||||
|
GatewayIntentBits.GuildPresences,
|
||||||
|
GatewayIntentBits.GuildMessageReactions,
|
||||||
|
GatewayIntentBits.GuildVoiceStates,
|
||||||
|
GatewayIntentBits.DirectMessages,
|
||||||
|
GatewayIntentBits.MessageContent
|
||||||
|
],
|
||||||
|
|
||||||
|
partials: [
|
||||||
|
Partials.Channel,
|
||||||
|
Partials.Message,
|
||||||
|
Partials.User,
|
||||||
|
Partials.GuildMember,
|
||||||
|
Partials.Reaction
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
exports.client =
|
||||||
|
client;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* COLLECTIONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
client.commands =
|
||||||
|
new Collection();
|
||||||
|
|
||||||
|
client.legacyCommands =
|
||||||
|
new Collection();
|
||||||
|
|
||||||
|
client.aliases =
|
||||||
|
new Collection();
|
||||||
|
|
||||||
|
client.buttons =
|
||||||
|
new Collection();
|
||||||
|
|
||||||
|
client.selectMenus =
|
||||||
|
new Collection();
|
||||||
|
|
||||||
|
client.modals =
|
||||||
|
new Collection();
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* START APPLICATION
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Connect Mongo first.
|
||||||
|
*/
|
||||||
|
|
||||||
|
await mClient.connect();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"MongoDB connected"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load handlers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const handlerFiles =
|
||||||
|
fs.readdirSync(
|
||||||
|
"./handlers"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
for (
|
||||||
|
const handler
|
||||||
|
of handlerFiles
|
||||||
|
) {
|
||||||
|
require(
|
||||||
|
`./handlers/${handler}`
|
||||||
|
)(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Login Discord.
|
||||||
|
*/
|
||||||
|
|
||||||
|
await client.login(
|
||||||
|
process.env.D_TOKEN
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Failed to start ShaiBot:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
start();
|
||||||
+1693
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,423 @@
|
|||||||
|
const {
|
||||||
|
MessageFlags
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
|
const { mClient } = require("../index");
|
||||||
|
|
||||||
|
|
||||||
|
const DATABASE = "neo-db";
|
||||||
|
|
||||||
|
const COOKIE_COLLECTION = "items_cookies";
|
||||||
|
const COOLDOWN_COLLECTION = "cooldowns_cookies";
|
||||||
|
|
||||||
|
|
||||||
|
function getCollections() {
|
||||||
|
const db = mClient.db(DATABASE);
|
||||||
|
|
||||||
|
return {
|
||||||
|
db,
|
||||||
|
cookies: db.collection(COOKIE_COLLECTION),
|
||||||
|
cooldowns: db.collection(COOLDOWN_COLLECTION)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getCookieBalance(userId) {
|
||||||
|
const { cookies } = getCollections();
|
||||||
|
|
||||||
|
const result = await cookies.findOne(
|
||||||
|
{
|
||||||
|
userId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projection: {
|
||||||
|
_id: 0,
|
||||||
|
cookies: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result?.cookies ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function changeCookies(
|
||||||
|
userId,
|
||||||
|
amount
|
||||||
|
) {
|
||||||
|
const { cookies } = getCollections();
|
||||||
|
|
||||||
|
return cookies.updateOne(
|
||||||
|
{
|
||||||
|
userId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$inc: {
|
||||||
|
cookies: amount
|
||||||
|
},
|
||||||
|
|
||||||
|
$setOnInsert: {
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
upsert: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function transferCookies(
|
||||||
|
fromUserId,
|
||||||
|
toUserId,
|
||||||
|
amount
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!Number.isInteger(amount) ||
|
||||||
|
amount <= 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"INVALID_COOKIE_AMOUNT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const { cookies } = getCollections();
|
||||||
|
|
||||||
|
|
||||||
|
const debit =
|
||||||
|
await cookies.updateOne(
|
||||||
|
{
|
||||||
|
userId: fromUserId,
|
||||||
|
|
||||||
|
cookies: {
|
||||||
|
$gte: amount
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$inc: {
|
||||||
|
cookies: -amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
debit.modifiedCount !== 1
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"INSUFFICIENT_COOKIES"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cookies.updateOne(
|
||||||
|
{
|
||||||
|
userId: toUserId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$inc: {
|
||||||
|
cookies: amount
|
||||||
|
},
|
||||||
|
|
||||||
|
$setOnInsert: {
|
||||||
|
userId: toUserId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
upsert: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
await cookies.updateOne(
|
||||||
|
{
|
||||||
|
userId: fromUserId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$inc: {
|
||||||
|
cookies: amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (rollbackError) {
|
||||||
|
console.error(
|
||||||
|
"CRITICAL: Cookie rollback failed:",
|
||||||
|
rollbackError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getCooldown(
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
type
|
||||||
|
) {
|
||||||
|
const { cooldowns } = getCollections();
|
||||||
|
|
||||||
|
const result =
|
||||||
|
await cooldowns.findOne(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
guildId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projection: {
|
||||||
|
_id: 0,
|
||||||
|
[type]: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result?.[type] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function setCooldown(
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
type,
|
||||||
|
duration
|
||||||
|
) {
|
||||||
|
const { cooldowns } = getCollections();
|
||||||
|
|
||||||
|
const now = getUnixTime();
|
||||||
|
const next = now + duration;
|
||||||
|
|
||||||
|
|
||||||
|
await cooldowns.updateOne(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
guildId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
[type]: next
|
||||||
|
},
|
||||||
|
|
||||||
|
$setOnInsert: {
|
||||||
|
userId,
|
||||||
|
guildId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
upsert: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Atomically claim a cooldown.
|
||||||
|
*
|
||||||
|
* Useful for button games where a double-click
|
||||||
|
* could otherwise resolve twice.
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function claimCooldown(
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
type,
|
||||||
|
duration
|
||||||
|
) {
|
||||||
|
const { cooldowns } = getCollections();
|
||||||
|
|
||||||
|
const now = getUnixTime();
|
||||||
|
const next = now + duration;
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result =
|
||||||
|
await cooldowns.updateOne(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
[type]: {
|
||||||
|
$lte: now
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[type]: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
[type]: next
|
||||||
|
},
|
||||||
|
|
||||||
|
$setOnInsert: {
|
||||||
|
userId,
|
||||||
|
guildId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
upsert: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimed:
|
||||||
|
result.modifiedCount === 1 ||
|
||||||
|
result.upsertedCount === 1,
|
||||||
|
|
||||||
|
next
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
/*
|
||||||
|
* A unique {userId, guildId} index may cause
|
||||||
|
* a duplicate-key error if an active cooldown
|
||||||
|
* already exists.
|
||||||
|
*/
|
||||||
|
if (error?.code === 11000) {
|
||||||
|
const active =
|
||||||
|
await getCooldown(
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
type
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimed: false,
|
||||||
|
next: active
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function temporaryEphemeralReply(
|
||||||
|
interaction,
|
||||||
|
options,
|
||||||
|
deleteAfter = 5000
|
||||||
|
) {
|
||||||
|
await interaction.reply({
|
||||||
|
...options,
|
||||||
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
await interaction.deleteReply();
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
deleteAfter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function temporaryEphemeralFollowUp(
|
||||||
|
interaction,
|
||||||
|
options,
|
||||||
|
deleteAfter = 8000
|
||||||
|
) {
|
||||||
|
const message =
|
||||||
|
await interaction.followUp({
|
||||||
|
...options,
|
||||||
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
await message.delete();
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
deleteAfter
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getUnixTime() {
|
||||||
|
return Math.floor(
|
||||||
|
Date.now() / 1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function randomInteger(
|
||||||
|
min,
|
||||||
|
max
|
||||||
|
) {
|
||||||
|
return Math.floor(
|
||||||
|
Math.random() *
|
||||||
|
(max - min + 1)
|
||||||
|
) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function randomFloat(
|
||||||
|
min,
|
||||||
|
max
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
Math.random() *
|
||||||
|
(max - min)
|
||||||
|
) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function randomItem(items) {
|
||||||
|
return items[
|
||||||
|
Math.floor(
|
||||||
|
Math.random() *
|
||||||
|
items.length
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function formatPercentage(value) {
|
||||||
|
return `${Math.round(value * 100)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DATABASE,
|
||||||
|
COOKIE_COLLECTION,
|
||||||
|
COOLDOWN_COLLECTION,
|
||||||
|
|
||||||
|
getCollections,
|
||||||
|
getCookieBalance,
|
||||||
|
changeCookies,
|
||||||
|
transferCookies,
|
||||||
|
|
||||||
|
getCooldown,
|
||||||
|
setCooldown,
|
||||||
|
claimCooldown,
|
||||||
|
|
||||||
|
temporaryEphemeralReply,
|
||||||
|
temporaryEphemeralFollowUp,
|
||||||
|
|
||||||
|
getUnixTime,
|
||||||
|
randomInteger,
|
||||||
|
randomFloat,
|
||||||
|
randomItem,
|
||||||
|
formatPercentage
|
||||||
|
};
|
||||||
@@ -0,0 +1,836 @@
|
|||||||
|
const {
|
||||||
|
EmbedBuilder
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
|
const {
|
||||||
|
getCookieBalance,
|
||||||
|
transferCookies,
|
||||||
|
claimCooldown,
|
||||||
|
temporaryEphemeralReply,
|
||||||
|
randomFloat,
|
||||||
|
formatPercentage
|
||||||
|
} = require("./cookieEconomy");
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* CONFIG
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HEIST_COOLDOWN =
|
||||||
|
60 * 60; // 1 hour
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Each approach has a different risk/reward profile.
|
||||||
|
*
|
||||||
|
* Sneak:
|
||||||
|
* High success chance
|
||||||
|
* Small reward
|
||||||
|
* Small failure penalty
|
||||||
|
*
|
||||||
|
* Hack:
|
||||||
|
* Medium success chance
|
||||||
|
* Medium reward
|
||||||
|
* Medium failure penalty
|
||||||
|
*
|
||||||
|
* Smash:
|
||||||
|
* Low success chance
|
||||||
|
* Large reward
|
||||||
|
* Large failure penalty
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HEIST_METHODS = {
|
||||||
|
|
||||||
|
Sneak: {
|
||||||
|
baseChance:
|
||||||
|
0.65,
|
||||||
|
|
||||||
|
minStealPercent:
|
||||||
|
0.01,
|
||||||
|
|
||||||
|
maxStealPercent:
|
||||||
|
0.03,
|
||||||
|
|
||||||
|
maxStealAmount:
|
||||||
|
1500,
|
||||||
|
|
||||||
|
minPenaltyPercent:
|
||||||
|
0.01,
|
||||||
|
|
||||||
|
maxPenaltyPercent:
|
||||||
|
0.04
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
Hack: {
|
||||||
|
baseChance:
|
||||||
|
0.45,
|
||||||
|
|
||||||
|
minStealPercent:
|
||||||
|
0.03,
|
||||||
|
|
||||||
|
maxStealPercent:
|
||||||
|
0.06,
|
||||||
|
|
||||||
|
maxStealAmount:
|
||||||
|
3500,
|
||||||
|
|
||||||
|
minPenaltyPercent:
|
||||||
|
0.03,
|
||||||
|
|
||||||
|
maxPenaltyPercent:
|
||||||
|
0.07
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
Smash: {
|
||||||
|
baseChance:
|
||||||
|
0.25,
|
||||||
|
|
||||||
|
minStealPercent:
|
||||||
|
0.06,
|
||||||
|
|
||||||
|
maxStealPercent:
|
||||||
|
0.12,
|
||||||
|
|
||||||
|
maxStealAmount:
|
||||||
|
7500,
|
||||||
|
|
||||||
|
minPenaltyPercent:
|
||||||
|
0.06,
|
||||||
|
|
||||||
|
maxPenaltyPercent:
|
||||||
|
0.12
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* RESOLVE HEIST
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function resolveCookieHeist(
|
||||||
|
interaction,
|
||||||
|
methodName
|
||||||
|
) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The original /cookies heist command stores
|
||||||
|
* the player ID inside the embed footer:
|
||||||
|
*
|
||||||
|
* cookieHeist:123456789
|
||||||
|
*
|
||||||
|
* This prevents somebody else from pressing
|
||||||
|
* another player's heist buttons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const footer =
|
||||||
|
interaction.message
|
||||||
|
.embeds[0]
|
||||||
|
?.footer
|
||||||
|
?.text;
|
||||||
|
|
||||||
|
|
||||||
|
const [
|
||||||
|
type,
|
||||||
|
ownerId
|
||||||
|
] =
|
||||||
|
footer
|
||||||
|
?.split(":")
|
||||||
|
??
|
||||||
|
[];
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
type !== "cookieHeist" ||
|
||||||
|
ownerId !== interaction.user.id
|
||||||
|
) {
|
||||||
|
return temporaryEphemeralReply(
|
||||||
|
interaction,
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"This isn't your heist. Start your own with `/cookies heist`."
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Validate selected method.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const method =
|
||||||
|
HEIST_METHODS[
|
||||||
|
methodName
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
if (!method) {
|
||||||
|
return temporaryEphemeralReply(
|
||||||
|
interaction,
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"That heist approach doesn't exist."
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================================================
|
||||||
|
* CLAIM COOLDOWN
|
||||||
|
* ========================================================
|
||||||
|
*
|
||||||
|
* This is atomic so rapid double-clicks can't
|
||||||
|
* resolve the same heist multiple times.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const cooldown =
|
||||||
|
await claimCooldown(
|
||||||
|
interaction.user.id,
|
||||||
|
interaction.guildId,
|
||||||
|
"heist",
|
||||||
|
HEIST_COOLDOWN
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (!cooldown.claimed) {
|
||||||
|
return temporaryEphemeralReply(
|
||||||
|
interaction,
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
`You can attempt another vault heist <t:${cooldown.next}:R>.`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================================================
|
||||||
|
* LOCK THE HEIST MESSAGE
|
||||||
|
* ========================================================
|
||||||
|
*
|
||||||
|
* Remove all buttons immediately.
|
||||||
|
*/
|
||||||
|
|
||||||
|
await interaction.update({
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle(
|
||||||
|
"🏦 Vault Heist in Progress..."
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
getHeistProgressText(
|
||||||
|
methodName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================================================
|
||||||
|
* GET BALANCES
|
||||||
|
* ========================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
const playerId =
|
||||||
|
interaction.user.id;
|
||||||
|
|
||||||
|
const vaultId =
|
||||||
|
interaction.client.user.id;
|
||||||
|
|
||||||
|
|
||||||
|
const [
|
||||||
|
playerCookies,
|
||||||
|
vaultCookies
|
||||||
|
] =
|
||||||
|
await Promise.all([
|
||||||
|
|
||||||
|
getCookieBalance(
|
||||||
|
playerId
|
||||||
|
),
|
||||||
|
|
||||||
|
getCookieBalance(
|
||||||
|
vaultId
|
||||||
|
)
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Something may have changed between opening
|
||||||
|
* the heist menu and pressing the button.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (
|
||||||
|
playerCookies < 1 ||
|
||||||
|
vaultCookies < 1
|
||||||
|
) {
|
||||||
|
return interaction.editReply({
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle(
|
||||||
|
"🏦 Heist Aborted"
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
"There weren't enough Cookies available to resolve the heist."
|
||||||
|
)
|
||||||
|
],
|
||||||
|
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Small dramatic pause.
|
||||||
|
*
|
||||||
|
* Only one edit happens after this, so this is
|
||||||
|
* comfortably within Discord rate limits.
|
||||||
|
*/
|
||||||
|
|
||||||
|
await sleep(
|
||||||
|
1200
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================================================
|
||||||
|
* VAULT DIFFICULTY
|
||||||
|
* ========================================================
|
||||||
|
*
|
||||||
|
* The richer ShaiBot's vault becomes,
|
||||||
|
* the harder it is to rob.
|
||||||
|
*
|
||||||
|
* Penalty is capped at 15 percentage points.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const securityPenalty =
|
||||||
|
getVaultSecurityPenalty(
|
||||||
|
vaultCookies
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const successChance =
|
||||||
|
Math.max(
|
||||||
|
0.10,
|
||||||
|
|
||||||
|
method.baseChance -
|
||||||
|
securityPenalty
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const success =
|
||||||
|
Math.random() <
|
||||||
|
successChance;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================================================
|
||||||
|
* SUCCESS
|
||||||
|
* ========================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Roll percentage of the current vault.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const percent =
|
||||||
|
randomFloat(
|
||||||
|
method.minStealPercent,
|
||||||
|
method.maxStealPercent
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Apply both:
|
||||||
|
*
|
||||||
|
* percentage roll
|
||||||
|
* AND
|
||||||
|
* hard maximum
|
||||||
|
*
|
||||||
|
* This prevents a massive vault from producing
|
||||||
|
* absurd payouts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const amount =
|
||||||
|
Math.max(
|
||||||
|
1,
|
||||||
|
|
||||||
|
Math.min(
|
||||||
|
|
||||||
|
Math.floor(
|
||||||
|
vaultCookies *
|
||||||
|
percent
|
||||||
|
),
|
||||||
|
|
||||||
|
method.maxStealAmount,
|
||||||
|
|
||||||
|
vaultCookies
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ShaiBot pays the robber.
|
||||||
|
*/
|
||||||
|
|
||||||
|
await transferCookies(
|
||||||
|
vaultId,
|
||||||
|
playerId,
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const remaining =
|
||||||
|
await getCookieBalance(
|
||||||
|
vaultId
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const actualPercentage =
|
||||||
|
(
|
||||||
|
amount /
|
||||||
|
vaultCookies
|
||||||
|
) *
|
||||||
|
100;
|
||||||
|
|
||||||
|
|
||||||
|
const embed =
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle(
|
||||||
|
"💥 Vault Breached!"
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
getSuccessText(
|
||||||
|
methodName,
|
||||||
|
interaction.user,
|
||||||
|
amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addFields(
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Approach",
|
||||||
|
|
||||||
|
value:
|
||||||
|
methodName,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Success Chance",
|
||||||
|
|
||||||
|
value:
|
||||||
|
`**${formatPercentage(successChance)}**`,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Vault Taken",
|
||||||
|
|
||||||
|
value:
|
||||||
|
`**${actualPercentage.toFixed(2)}%**`,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Cookies Stolen",
|
||||||
|
|
||||||
|
value:
|
||||||
|
`**${amount.toLocaleString()} 🍪**`,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Vault Remaining",
|
||||||
|
|
||||||
|
value:
|
||||||
|
`**${remaining.toLocaleString()} 🍪**`,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Next Heist",
|
||||||
|
|
||||||
|
value:
|
||||||
|
`<t:${cooldown.next}:R>`,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return interaction.editReply({
|
||||||
|
embeds: [
|
||||||
|
embed
|
||||||
|
],
|
||||||
|
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ========================================================
|
||||||
|
* FAILURE
|
||||||
|
* ========================================================
|
||||||
|
*
|
||||||
|
* Failure fines the player.
|
||||||
|
*
|
||||||
|
* Unlike normal player stealing, the fine goes
|
||||||
|
* directly into ShaiBot's vault.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const penaltyPercent =
|
||||||
|
randomFloat(
|
||||||
|
method.minPenaltyPercent,
|
||||||
|
method.maxPenaltyPercent
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const penalty =
|
||||||
|
Math.max(
|
||||||
|
1,
|
||||||
|
|
||||||
|
Math.min(
|
||||||
|
|
||||||
|
Math.floor(
|
||||||
|
playerCookies *
|
||||||
|
penaltyPercent
|
||||||
|
),
|
||||||
|
|
||||||
|
playerCookies
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
await transferCookies(
|
||||||
|
playerId,
|
||||||
|
vaultId,
|
||||||
|
penalty
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const [
|
||||||
|
playerAfter,
|
||||||
|
vaultAfter
|
||||||
|
] =
|
||||||
|
await Promise.all([
|
||||||
|
|
||||||
|
getCookieBalance(
|
||||||
|
playerId
|
||||||
|
),
|
||||||
|
|
||||||
|
getCookieBalance(
|
||||||
|
vaultId
|
||||||
|
)
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
const embed =
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle(
|
||||||
|
"🚨 HEIST FAILED"
|
||||||
|
)
|
||||||
|
.setDescription(
|
||||||
|
getFailureText(
|
||||||
|
methodName,
|
||||||
|
interaction.user,
|
||||||
|
penalty
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addFields(
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Approach",
|
||||||
|
|
||||||
|
value:
|
||||||
|
methodName,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Success Chance",
|
||||||
|
|
||||||
|
value:
|
||||||
|
`**${formatPercentage(successChance)}**`,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Fine",
|
||||||
|
|
||||||
|
value:
|
||||||
|
`**${penalty.toLocaleString()} 🍪**`,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Your Vault",
|
||||||
|
|
||||||
|
value:
|
||||||
|
`**${playerAfter.toLocaleString()} 🍪**`,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"House Vault",
|
||||||
|
|
||||||
|
value:
|
||||||
|
`**${vaultAfter.toLocaleString()} 🍪**`,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name:
|
||||||
|
"Next Heist",
|
||||||
|
|
||||||
|
value:
|
||||||
|
`<t:${cooldown.next}:R>`,
|
||||||
|
|
||||||
|
inline:
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return interaction.editReply({
|
||||||
|
embeds: [
|
||||||
|
embed
|
||||||
|
],
|
||||||
|
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* VAULT SECURITY
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getVaultSecurityPenalty(
|
||||||
|
vaultCookies
|
||||||
|
) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Logarithmic scaling means the vault gets harder
|
||||||
|
* as it becomes richer without becoming impossible.
|
||||||
|
*
|
||||||
|
* Examples approximately:
|
||||||
|
*
|
||||||
|
* 100 -> 5%
|
||||||
|
* 1,000 -> 7.5%
|
||||||
|
* 10,000 -> 10%
|
||||||
|
* 100,000 -> 12.5%
|
||||||
|
* 1,000,000 -> 15%
|
||||||
|
*/
|
||||||
|
|
||||||
|
return Math.min(
|
||||||
|
|
||||||
|
Math.log10(
|
||||||
|
vaultCookies + 1
|
||||||
|
) *
|
||||||
|
0.025,
|
||||||
|
|
||||||
|
0.15
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* FLAVOR TEXT
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getHeistProgressText(
|
||||||
|
methodName
|
||||||
|
) {
|
||||||
|
|
||||||
|
switch (
|
||||||
|
methodName
|
||||||
|
) {
|
||||||
|
|
||||||
|
case "Sneak":
|
||||||
|
return [
|
||||||
|
"🥷 You slip through the service entrance...",
|
||||||
|
"",
|
||||||
|
"The Cookie Vault guards haven't noticed you yet."
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
|
||||||
|
case "Hack":
|
||||||
|
return [
|
||||||
|
"💻 You connect to the vault terminal...",
|
||||||
|
"",
|
||||||
|
"Several extremely suspicious progress bars appear."
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
|
||||||
|
case "Smash":
|
||||||
|
return [
|
||||||
|
"💥 You charge directly at the vault...",
|
||||||
|
"",
|
||||||
|
"Subtlety has been abandoned."
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "The heist is underway...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getSuccessText(
|
||||||
|
methodName,
|
||||||
|
user,
|
||||||
|
amount
|
||||||
|
) {
|
||||||
|
|
||||||
|
const amountText =
|
||||||
|
`**${amount.toLocaleString()} Cookies**`;
|
||||||
|
|
||||||
|
|
||||||
|
switch (
|
||||||
|
methodName
|
||||||
|
) {
|
||||||
|
|
||||||
|
case "Sneak":
|
||||||
|
return (
|
||||||
|
`🥷 ${user} slipped through security and quietly escaped with ${amountText}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
case "Hack":
|
||||||
|
return (
|
||||||
|
`💻 ${user} convinced the vault terminal that ${amountText} belonged to them.`
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
case "Smash":
|
||||||
|
return (
|
||||||
|
`💥 ${user} smashed through the Cookie Vault and escaped with ${amountText}!`
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
`${user} escaped with ${amountText}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getFailureText(
|
||||||
|
methodName,
|
||||||
|
user,
|
||||||
|
penalty
|
||||||
|
) {
|
||||||
|
|
||||||
|
const penaltyText =
|
||||||
|
`**${penalty.toLocaleString()} Cookies**`;
|
||||||
|
|
||||||
|
|
||||||
|
switch (
|
||||||
|
methodName
|
||||||
|
) {
|
||||||
|
|
||||||
|
case "Sneak":
|
||||||
|
return [
|
||||||
|
`🚨 ${user} stepped directly on the world's loudest Cookie wrapper.`,
|
||||||
|
"",
|
||||||
|
`The ShaiBot Vault collected a ${penaltyText} fine.`
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
|
||||||
|
case "Hack":
|
||||||
|
return [
|
||||||
|
`🚨 ${user} entered the password incorrectly one too many times.`,
|
||||||
|
"",
|
||||||
|
`The ShaiBot Vault collected a ${penaltyText} security fee.`
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
|
||||||
|
case "Smash":
|
||||||
|
return [
|
||||||
|
`🚨 ${user} discovered that the vault door is significantly stronger than expected.`,
|
||||||
|
"",
|
||||||
|
`Repairs cost ${penaltyText}. Naturally, you're paying.`
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
`The heist failed and cost ${user} ${penaltyText}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================
|
||||||
|
* UTIL
|
||||||
|
* ============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(
|
||||||
|
resolve =>
|
||||||
|
setTimeout(
|
||||||
|
resolve,
|
||||||
|
ms
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
resolveCookieHeist
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user