Compare commits
2 Commits
5df3430242
...
24d5b395ca
| Author | SHA1 | Date | |
|---|---|---|---|
| 24d5b395ca | |||
| 8bdd6868e8 |
@@ -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"
|
||||
);
|
||||
}
|
||||
};
|
||||
+1664
-526
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 { MongoClient } = require('mongodb')
|
||||
require('dotenv').config()
|
||||
const mClient = new MongoClient(process.env.M_URI)
|
||||
exports.mClient = mClient;
|
||||
const fs = require('node:fs')
|
||||
const {
|
||||
Client,
|
||||
GatewayIntentBits,
|
||||
Collection,
|
||||
Partials
|
||||
} = require("discord.js");
|
||||
|
||||
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
|
||||
const {
|
||||
MongoClient
|
||||
} = require("mongodb");
|
||||
|
||||
client.commands = new Collection() // slash commands collection
|
||||
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();
|
||||
const fs = require("node:fs");
|
||||
|
||||
fs.readdirSync('./handlers').forEach((handler) => {
|
||||
require(`./handlers/${handler}`)(client)
|
||||
});
|
||||
require("dotenv").config();
|
||||
|
||||
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