added more steal logic

This commit is contained in:
DeSqBlocki
2026-08-15 08:44:24 +02:00
parent 5df3430242
commit 8bdd6868e8
+475 -130
View File
@@ -1,6 +1,7 @@
const { const {
SlashCommandBuilder, SlashCommandBuilder,
EmbedBuilder EmbedBuilder,
MessageFlags
} = require("discord.js"); } = require("discord.js");
const { mClient } = require("../.."); const { mClient } = require("../..");
@@ -143,8 +144,13 @@ async function getCookieBalance(userId) {
} }
async function changeCookies(userId, amount, session = undefined) { async function changeCookies(
const { cookies } = getCollections(); userId,
amount
) {
const { cookies } =
getCollections();
return cookies.updateOne( return cookies.updateOne(
{ {
@@ -152,15 +158,17 @@ async function changeCookies(userId, amount, session = undefined) {
}, },
{ {
$inc: { $inc: {
cookies: amount cookies:
amount
}, },
$setOnInsert: { $setOnInsert: {
userId userId
} }
}, },
{ {
upsert: true, upsert:
session true
} }
); );
} }
@@ -168,70 +176,108 @@ async function changeCookies(userId, amount, session = undefined) {
/* /*
* COOKIE TRANSFER * COOKIE TRANSFER
*
* This uses a transaction so both balances update together.
*
* MongoDB transactions require Atlas or a replica set.
*/ */
async function transferCookies(fromUserId, toUserId, amount) { async function transferCookies(
const { cookies } = getCollections(); fromUserId,
toUserId,
amount
) {
const { cookies } =
getCollections();
const session = mClient.startSession();
try { /*
await session.withTransaction(async () => { * Atomically debit the sender only if
* they still have enough cookies.
*/
const debit =
await cookies.updateOne(
{
userId:
fromUserId,
/* cookies: {
* Only debit if the sender still has enough cookies. $gte:
* amount
* This protects against two commands being executed }
* at almost the same time. },
*/ {
const debit = await cookies.updateOne( $inc: {
{ cookies:
userId: fromUserId, -amount
cookies: {
$gte: amount
}
},
{
$inc: {
cookies: -amount
}
},
{
session
} }
);
if (debit.modifiedCount !== 1) {
throw new Error("INSUFFICIENT_COOKIES");
} }
);
if (
debit.modifiedCount !== 1
) {
throw new Error(
"INSUFFICIENT_COOKIES"
);
}
/*
* Credit the recipient.
*/
try {
await cookies.updateOne(
{
userId:
toUserId
},
{
$inc: {
cookies:
amount
},
$setOnInsert: {
userId:
toUserId
}
},
{
upsert:
true
}
);
} catch (error) {
/*
* Credit failed.
*
* Attempt to restore the sender's
* cookies so the transfer doesn't
* destroy currency.
*/
try {
await cookies.updateOne( await cookies.updateOne(
{ {
userId: toUserId userId:
fromUserId
}, },
{ {
$inc: { $inc: {
cookies: amount cookies:
}, amount
$setOnInsert: {
userId: toUserId
} }
},
{
upsert: true,
session
} }
); );
});
} finally { } catch (rollbackError) {
await session.endSession(); console.error(
"CRITICAL: Cookie transfer rollback failed:",
rollbackError
);
}
throw error;
} }
} }
@@ -365,77 +411,91 @@ async function collect(interaction) {
*/ */
async function steal(interaction) { async function steal(interaction) {
const user = interaction.user; const user =
interaction.user;
const target = interaction.options.getUser( const target =
"target", interaction.options.getUser(
true "target",
); true
);
/* /*
* Can't rob yourself. * VALIDATION
*/ */
if (target.id === user.id) { if (target.id === user.id) {
return interaction.reply({ return interaction.reply({
content: content:
"C'mon, don't embarrass yourself. You can't steal from yourself.", "C'mon, don't embarrass yourself. You can't steal from yourself.",
ephemeral: true flags:
MessageFlags.Ephemeral
}); });
} }
/*
* Don't let people farm bot accounts.
*/
if (target.bot) { if (target.bot) {
return interaction.reply({ return interaction.reply({
content: content:
"Bots have highly sophisticated cookie security systems.", "Bots have highly sophisticated Cookie security systems.",
ephemeral: true flags:
MessageFlags.Ephemeral
}); });
} }
const now = getUnixTime();
/* /*
* Check steal cooldown before doing anything expensive. * COOLDOWN
*/ */
const cooldown = await getCooldown(
user.id, const now =
interaction.guildId, getUnixTime();
"steal"
);
const cooldown =
await getCooldown(
user.id,
interaction.guildId,
"steal"
);
if (cooldown > now) { if (cooldown > now) {
return interaction.reply({ return interaction.reply({
content: content:
`Lay low for a while. You can steal again <t:${cooldown}:R>.`, `Lay low for a while. You can steal again <t:${cooldown}:R>.`,
ephemeral: true flags:
MessageFlags.Ephemeral
}); });
} }
/* /*
* Fetch both balances concurrently. * BALANCES
*/ */
const [ const [
yourCookies, yourCookies,
theirCookies theirCookies
] = await Promise.all([ ] =
getCookieBalance(user.id), await Promise.all([
getCookieBalance(target.id) getCookieBalance(
]); user.id
),
getCookieBalance(
target.id
)
]);
if (yourCookies < 1) { if (yourCookies < 1) {
return interaction.reply({ return interaction.reply({
content: content:
"You need at least **1 Cookie** to attempt a robbery.", "You need at least **1 Cookie** to attempt a robbery.",
ephemeral: true flags:
MessageFlags.Ephemeral
}); });
} }
@@ -444,35 +504,47 @@ async function steal(interaction) {
return interaction.reply({ return interaction.reply({
content: content:
`Shame on you! ${target} doesn't have any Cookies. D:`, `Shame on you! ${target} doesn't have any Cookies. D:`,
ephemeral: true
flags:
MessageFlags.Ephemeral
}); });
} }
/* /*
* Chaos mode: * DYNAMIC SUCCESS CHANCE
* fixed 50% steal chance.
*/ */
const successChance =
getStealChance(
yourCookies,
theirCookies
);
const success = const success =
Math.random() < STEAL_SUCCESS_CHANCE; Math.random() <
successChance;
/* /*
* Successful robbery. * SUCCESS
*/ */
if (success) {
const maxSteal = Math.max(
1,
Math.ceil(theirCookies / 4)
);
const amount = Math.min( if (success) {
theirCookies,
randomInteger( /*
1, * Roll a weighted percentage
maxSteal * of the target's entire vault.
) */
);
const {
amount,
percentage
} =
getStealAmount(
theirCookies
);
try { try {
@@ -481,53 +553,93 @@ async function steal(interaction) {
user.id, user.id,
amount amount
); );
} catch (error) { } catch (error) {
if (error.message === "INSUFFICIENT_COOKIES") {
/*
* Target may have spent/given/stolen
* cookies between our balance check
* and the actual transfer.
*/
if (
error.message ===
"INSUFFICIENT_COOKIES"
) {
return interaction.reply({ return interaction.reply({
content: content:
`${target}'s Cookie balance changed before you could finish the robbery.`, `${target}'s Cookie balance changed before you could finish the robbery.`,
ephemeral: true
flags:
MessageFlags.Ephemeral
}); });
} }
throw error; throw error;
} }
const nextSteal = await setCooldown( /*
user.id, * Apply cooldown after robbery.
interaction.guildId, */
"steal",
STEAL_COOLDOWN const nextSteal =
); await setCooldown(
user.id,
interaction.guildId,
"steal",
STEAL_COOLDOWN
);
const actualPercentage =
(
amount /
theirCookies *
100
);
return interaction.reply({ return interaction.reply({
content: content:
`${randomStealSuccessResponse(amount, target)}\n` + `${randomStealSuccessResponse(amount, target)}\n` +
`**Success Chance:** ${formatPercentage(STEAL_SUCCESS_CHANCE)} • ` + `🍪 You made off with **${actualPercentage.toFixed(1)}%** of their vault!\n` +
`Next robbery <t:${nextSteal}:R>` `🎲 **Success Chance:** ${formatPercentage(successChance)}\n` +
`⏱️ Next robbery <t:${nextSteal}:R>`
}); });
} }
/* /*
* Failed robbery. * FAILURE
* *
* Lose between 1 and roughly 1/6 of your balance. * Penalty is also percentage based.
*
* Roll between roughly 1–8% of the
* attacker's current vault.
*/ */
const maxPenalty = Math.max(
1,
Math.ceil(yourCookies / 6)
);
const penalty = Math.min( const penaltyPercentage =
yourCookies, randomFloat(
randomInteger( 0.01,
0.08
);
const penalty =
Math.max(
1, 1,
maxPenalty
) Math.min(
); yourCookies,
Math.floor(
yourCookies *
penaltyPercentage
)
)
);
try { try {
@@ -536,36 +648,57 @@ async function steal(interaction) {
target.id, target.id,
penalty penalty
); );
} catch (error) { } catch (error) {
if (error.message === "INSUFFICIENT_COOKIES") {
if (
error.message ===
"INSUFFICIENT_COOKIES"
) {
return interaction.reply({ return interaction.reply({
content: content:
"Your Cookie balance changed before the robbery could finish.", "Your Cookie balance changed before the robbery could finish.",
ephemeral: true
flags:
MessageFlags.Ephemeral
}); });
} }
throw error; throw error;
} }
const nextSteal = await setCooldown( /*
user.id, * Apply cooldown.
interaction.guildId, */
"steal",
STEAL_COOLDOWN const nextSteal =
); await setCooldown(
user.id,
interaction.guildId,
"steal",
STEAL_COOLDOWN
);
const actualPenaltyPercentage =
(
penalty /
yourCookies *
100
);
return interaction.reply({ return interaction.reply({
content: content:
`${randomStealFailResponse(penalty, target)}\n` + `${randomStealFailResponse(penalty, target)}\n` +
`**Success Chance:** ${formatPercentage(STEAL_SUCCESS_CHANCE)} • ` + `💸 You lost **${actualPenaltyPercentage.toFixed(1)}%** of your vault.\n` +
`Next robbery <t:${nextSteal}:R>` `🎲 **Success Chance:** ${formatPercentage(successChance)}\n` +
`⏱️ Next robbery <t:${nextSteal}:R>`
}); });
} }
/* /*
* /cookies check * /cookies check
*/ */
@@ -793,7 +926,219 @@ function randomStealFailResponse(amount, target) {
return randomItem(responses); return randomItem(responses);
} }
/*
* STEAL HELPERS
*/
function getStealChance(
attackerCookies,
targetCookies
) {
const MIN_CHANCE =
0.15;
const MAX_CHANCE =
0.70;
const BASE_CHANCE =
0.50;
/*
* Wealthy targets are harder to rob.
*
* Logarithmic scaling prevents massive
* vaults from becoming completely immune.
*/
const targetPenalty =
Math.log10(
targetCookies + 1
)
*
0.06;
/*
* Compare attacker wealth to victim.
*/
const wealthRatio =
attackerCookies /
Math.max(
targetCookies,
1
);
/*
* Catch-up mechanic.
*
* If the attacker has less than the target,
* they gain up to +18 percentage points.
*/
const povertyBonus =
wealthRatio < 1
? (
1 -
wealthRatio
)
*
0.18
: 0;
/*
* Anti-bullying penalty.
*
* Rich attackers targeting poorer users
* become progressively less successful.
*/
const bullyPenalty =
wealthRatio > 1
? Math.min(
Math.log10(
wealthRatio + 1
)
*
0.12,
0.20
)
: 0;
const chance =
BASE_CHANCE
-
targetPenalty
+
povertyBonus
-
bullyPenalty;
/*
* Always keep robbery possible,
* but never make it nearly guaranteed.
*/
return Math.max(
MIN_CHANCE,
Math.min(
MAX_CHANCE,
chance
)
);
}
function getStealAmount(
targetCookies
) {
const MAX_STEAL_AMOUNT =
5000;
const percentage =
getStealPercentage();
const calculatedAmount =
Math.floor(
targetCookies *
percentage
);
const amount =
Math.max(
1,
Math.min(
calculatedAmount,
targetCookies,
MAX_STEAL_AMOUNT
)
);
return {
amount,
percentage
};
}
function getStealPercentage() {
const roll =
Math.random();
/*
* 50% chance:
* steal 1–3%
*/
if (roll < 0.50) {
return randomFloat(
0.01,
0.03
);
}
/*
* 30% chance:
* steal 3–5%
*/
if (roll < 0.80) {
return randomFloat(
0.03,
0.05
);
}
/*
* 15% chance:
* steal 5–8%
*/
if (roll < 0.95) {
return randomFloat(
0.05,
0.08
);
}
/*
* 5% jackpot:
* steal 8–10%
*/
return randomFloat(
0.08,
0.10
);
}
function randomFloat(
min,
max
) {
return (
Math.random() *
(max - min)
+
min
);
}
/* /*
* GENERIC HELPERS * GENERIC HELPERS