added more steal logic
This commit is contained in:
+452
-107
@@ -1,6 +1,7 @@
|
||||
const {
|
||||
SlashCommandBuilder,
|
||||
EmbedBuilder
|
||||
EmbedBuilder,
|
||||
MessageFlags
|
||||
} = require("discord.js");
|
||||
|
||||
const { mClient } = require("../..");
|
||||
@@ -143,8 +144,13 @@ async function getCookieBalance(userId) {
|
||||
}
|
||||
|
||||
|
||||
async function changeCookies(userId, amount, session = undefined) {
|
||||
const { cookies } = getCollections();
|
||||
async function changeCookies(
|
||||
userId,
|
||||
amount
|
||||
) {
|
||||
const { cookies } =
|
||||
getCollections();
|
||||
|
||||
|
||||
return cookies.updateOne(
|
||||
{
|
||||
@@ -152,15 +158,17 @@ async function changeCookies(userId, amount, session = undefined) {
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
cookies: amount
|
||||
cookies:
|
||||
amount
|
||||
},
|
||||
|
||||
$setOnInsert: {
|
||||
userId
|
||||
}
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
session
|
||||
upsert:
|
||||
true
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -168,70 +176,108 @@ async function changeCookies(userId, amount, session = undefined) {
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
const { cookies } = getCollections();
|
||||
async function transferCookies(
|
||||
fromUserId,
|
||||
toUserId,
|
||||
amount
|
||||
) {
|
||||
const { cookies } =
|
||||
getCollections();
|
||||
|
||||
const session = mClient.startSession();
|
||||
|
||||
try {
|
||||
await session.withTransaction(async () => {
|
||||
|
||||
/*
|
||||
* Only debit if the sender still has enough cookies.
|
||||
*
|
||||
* This protects against two commands being executed
|
||||
* at almost the same time.
|
||||
* Atomically debit the sender only if
|
||||
* they still have enough cookies.
|
||||
*/
|
||||
const debit = await cookies.updateOne(
|
||||
{
|
||||
userId: fromUserId,
|
||||
cookies: {
|
||||
$gte: amount
|
||||
}
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
cookies: -amount
|
||||
}
|
||||
},
|
||||
{
|
||||
session
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
if (debit.modifiedCount !== 1) {
|
||||
throw new Error("INSUFFICIENT_COOKIES");
|
||||
}
|
||||
|
||||
|
||||
const debit =
|
||||
await cookies.updateOne(
|
||||
{
|
||||
userId: toUserId
|
||||
userId:
|
||||
fromUserId,
|
||||
|
||||
cookies: {
|
||||
$gte:
|
||||
amount
|
||||
}
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
cookies: amount
|
||||
cookies:
|
||||
-amount
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
if (
|
||||
debit.modifiedCount !== 1
|
||||
) {
|
||||
throw new Error(
|
||||
"INSUFFICIENT_COOKIES"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Credit the recipient.
|
||||
*/
|
||||
try {
|
||||
await cookies.updateOne(
|
||||
{
|
||||
userId:
|
||||
toUserId
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
cookies:
|
||||
amount
|
||||
},
|
||||
|
||||
$setOnInsert: {
|
||||
userId: toUserId
|
||||
userId:
|
||||
toUserId
|
||||
}
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
session
|
||||
upsert:
|
||||
true
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
} finally {
|
||||
await session.endSession();
|
||||
} catch (error) {
|
||||
|
||||
/*
|
||||
* Credit failed.
|
||||
*
|
||||
* Attempt to restore the sender's
|
||||
* cookies so the transfer doesn't
|
||||
* destroy currency.
|
||||
*/
|
||||
try {
|
||||
await cookies.updateOne(
|
||||
{
|
||||
userId:
|
||||
fromUserId
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
cookies:
|
||||
amount
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
} catch (rollbackError) {
|
||||
console.error(
|
||||
"CRITICAL: Cookie transfer rollback failed:",
|
||||
rollbackError
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,45 +411,50 @@ async function collect(interaction) {
|
||||
*/
|
||||
|
||||
async function steal(interaction) {
|
||||
const user = interaction.user;
|
||||
const user =
|
||||
interaction.user;
|
||||
|
||||
const target = interaction.options.getUser(
|
||||
const target =
|
||||
interaction.options.getUser(
|
||||
"target",
|
||||
true
|
||||
);
|
||||
|
||||
|
||||
/*
|
||||
* Can't rob yourself.
|
||||
* VALIDATION
|
||||
*/
|
||||
|
||||
if (target.id === user.id) {
|
||||
return interaction.reply({
|
||||
content:
|
||||
"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) {
|
||||
return interaction.reply({
|
||||
content:
|
||||
"Bots have highly sophisticated cookie security systems.",
|
||||
ephemeral: true
|
||||
"Bots have highly sophisticated Cookie security systems.",
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const now = getUnixTime();
|
||||
|
||||
|
||||
/*
|
||||
* Check steal cooldown before doing anything expensive.
|
||||
* COOLDOWN
|
||||
*/
|
||||
const cooldown = await getCooldown(
|
||||
|
||||
const now =
|
||||
getUnixTime();
|
||||
|
||||
|
||||
const cooldown =
|
||||
await getCooldown(
|
||||
user.id,
|
||||
interaction.guildId,
|
||||
"steal"
|
||||
@@ -414,20 +465,28 @@ async function steal(interaction) {
|
||||
return interaction.reply({
|
||||
content:
|
||||
`Lay low for a while. You can steal again <t:${cooldown}:R>.`,
|
||||
ephemeral: true
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Fetch both balances concurrently.
|
||||
* BALANCES
|
||||
*/
|
||||
|
||||
const [
|
||||
yourCookies,
|
||||
theirCookies
|
||||
] = await Promise.all([
|
||||
getCookieBalance(user.id),
|
||||
getCookieBalance(target.id)
|
||||
] =
|
||||
await Promise.all([
|
||||
getCookieBalance(
|
||||
user.id
|
||||
),
|
||||
|
||||
getCookieBalance(
|
||||
target.id
|
||||
)
|
||||
]);
|
||||
|
||||
|
||||
@@ -435,7 +494,8 @@ async function steal(interaction) {
|
||||
return interaction.reply({
|
||||
content:
|
||||
"You need at least **1 Cookie** to attempt a robbery.",
|
||||
ephemeral: true
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
@@ -444,34 +504,46 @@ async function steal(interaction) {
|
||||
return interaction.reply({
|
||||
content:
|
||||
`Shame on you! ${target} doesn't have any Cookies. D:`,
|
||||
ephemeral: true
|
||||
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Chaos mode:
|
||||
* fixed 50% steal chance.
|
||||
* DYNAMIC SUCCESS CHANCE
|
||||
*/
|
||||
|
||||
const successChance =
|
||||
getStealChance(
|
||||
yourCookies,
|
||||
theirCookies
|
||||
);
|
||||
|
||||
|
||||
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(
|
||||
theirCookies,
|
||||
randomInteger(
|
||||
1,
|
||||
maxSteal
|
||||
)
|
||||
if (success) {
|
||||
|
||||
/*
|
||||
* Roll a weighted percentage
|
||||
* of the target's entire vault.
|
||||
*/
|
||||
|
||||
const {
|
||||
amount,
|
||||
percentage
|
||||
} =
|
||||
getStealAmount(
|
||||
theirCookies
|
||||
);
|
||||
|
||||
|
||||
@@ -481,20 +553,39 @@ async function steal(interaction) {
|
||||
user.id,
|
||||
amount
|
||||
);
|
||||
|
||||
} 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({
|
||||
content:
|
||||
`${target}'s Cookie balance changed before you could finish the robbery.`,
|
||||
ephemeral: true
|
||||
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
const nextSteal = await setCooldown(
|
||||
/*
|
||||
* Apply cooldown after robbery.
|
||||
*/
|
||||
|
||||
const nextSteal =
|
||||
await setCooldown(
|
||||
user.id,
|
||||
interaction.guildId,
|
||||
"steal",
|
||||
@@ -502,30 +593,51 @@ async function steal(interaction) {
|
||||
);
|
||||
|
||||
|
||||
const actualPercentage =
|
||||
(
|
||||
amount /
|
||||
theirCookies *
|
||||
100
|
||||
);
|
||||
|
||||
|
||||
return interaction.reply({
|
||||
content:
|
||||
`${randomStealSuccessResponse(amount, target)}\n` +
|
||||
`**Success Chance:** ${formatPercentage(STEAL_SUCCESS_CHANCE)} • ` +
|
||||
`Next robbery <t:${nextSteal}:R>`
|
||||
`🍪 You made off with **${actualPercentage.toFixed(1)}%** of their vault!\n` +
|
||||
`🎲 **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 penaltyPercentage =
|
||||
randomFloat(
|
||||
0.01,
|
||||
0.08
|
||||
);
|
||||
|
||||
const penalty = Math.min(
|
||||
yourCookies,
|
||||
randomInteger(
|
||||
|
||||
const penalty =
|
||||
Math.max(
|
||||
1,
|
||||
maxPenalty
|
||||
|
||||
Math.min(
|
||||
yourCookies,
|
||||
|
||||
Math.floor(
|
||||
yourCookies *
|
||||
penaltyPercentage
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -536,20 +648,33 @@ async function steal(interaction) {
|
||||
target.id,
|
||||
penalty
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
if (error.message === "INSUFFICIENT_COOKIES") {
|
||||
|
||||
if (
|
||||
error.message ===
|
||||
"INSUFFICIENT_COOKIES"
|
||||
) {
|
||||
return interaction.reply({
|
||||
content:
|
||||
"Your Cookie balance changed before the robbery could finish.",
|
||||
ephemeral: true
|
||||
|
||||
flags:
|
||||
MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
const nextSteal = await setCooldown(
|
||||
/*
|
||||
* Apply cooldown.
|
||||
*/
|
||||
|
||||
const nextSteal =
|
||||
await setCooldown(
|
||||
user.id,
|
||||
interaction.guildId,
|
||||
"steal",
|
||||
@@ -557,15 +682,23 @@ async function steal(interaction) {
|
||||
);
|
||||
|
||||
|
||||
const actualPenaltyPercentage =
|
||||
(
|
||||
penalty /
|
||||
yourCookies *
|
||||
100
|
||||
);
|
||||
|
||||
|
||||
return interaction.reply({
|
||||
content:
|
||||
`${randomStealFailResponse(penalty, target)}\n` +
|
||||
`**Success Chance:** ${formatPercentage(STEAL_SUCCESS_CHANCE)} • ` +
|
||||
`Next robbery <t:${nextSteal}:R>`
|
||||
`💸 You lost **${actualPenaltyPercentage.toFixed(1)}%** of your vault.\n` +
|
||||
`🎲 **Success Chance:** ${formatPercentage(successChance)}\n` +
|
||||
`⏱️ Next robbery <t:${nextSteal}:R>`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* /cookies check
|
||||
*/
|
||||
@@ -793,7 +926,219 @@ function randomStealFailResponse(amount, target) {
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user