1189 lines
23 KiB
JavaScript
1189 lines
23 KiB
JavaScript
const {
|
||
SlashCommandBuilder,
|
||
EmbedBuilder,
|
||
MessageFlags
|
||
} = require("discord.js");
|
||
|
||
const { mClient } = require("../..");
|
||
|
||
|
||
const DATABASE = "neo-db";
|
||
|
||
const COOKIE_COLLECTION = "items_cookies";
|
||
const COOLDOWN_COLLECTION = "cooldowns_cookies";
|
||
|
||
const COLLECT_COOLDOWN = 60 * 60; // 1 hour
|
||
const STEAL_COOLDOWN = 15 * 60; // 15 minutes
|
||
|
||
const STEAL_SUCCESS_CHANCE = 0.50;
|
||
|
||
const ABRA_ID = "378135175059603457";
|
||
const ABRA_MULTIPLIER = 1.5;
|
||
|
||
|
||
module.exports = {
|
||
data: new SlashCommandBuilder()
|
||
.setName("cookies")
|
||
.setDescription("The ShaiBot cookie economy.")
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("collect")
|
||
.setDescription("Collect a fresh batch of cookies.")
|
||
)
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("steal")
|
||
.setDescription("Attempt to steal another user's cookies.")
|
||
.addUserOption(option =>
|
||
option
|
||
.setName("target")
|
||
.setDescription("The unfortunate victim.")
|
||
.setRequired(true)
|
||
)
|
||
)
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("check")
|
||
.setDescription("Check a cookie balance.")
|
||
.addUserOption(option =>
|
||
option
|
||
.setName("target")
|
||
.setDescription("The user whose cookies you want to check.")
|
||
)
|
||
)
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("give")
|
||
.setDescription("Give some of your cookies to another user.")
|
||
.addUserOption(option =>
|
||
option
|
||
.setName("target")
|
||
.setDescription("The recipient.")
|
||
.setRequired(true)
|
||
)
|
||
.addIntegerOption(option =>
|
||
option
|
||
.setName("amount")
|
||
.setDescription("How many cookies to give.")
|
||
.setRequired(true)
|
||
.setMinValue(1)
|
||
)
|
||
)
|
||
|
||
.addSubcommand(subcommand =>
|
||
subcommand
|
||
.setName("leaderboard")
|
||
.setDescription("Show the richest cookie collectors.")
|
||
),
|
||
|
||
async execute(interaction) {
|
||
switch (interaction.options.getSubcommand()) {
|
||
case "collect":
|
||
return collect(interaction);
|
||
|
||
case "steal":
|
||
return steal(interaction);
|
||
|
||
case "check":
|
||
return check(interaction);
|
||
|
||
case "give":
|
||
return give(interaction);
|
||
|
||
case "leaderboard":
|
||
return leaderboard(interaction);
|
||
|
||
default:
|
||
return interaction.reply({
|
||
content: "Unknown subcommand.",
|
||
ephemeral: true
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
|
||
/*
|
||
* DATABASE
|
||
*/
|
||
|
||
function getCollections() {
|
||
const db = mClient.db(DATABASE);
|
||
|
||
return {
|
||
cookies: db.collection(COOKIE_COLLECTION),
|
||
cooldowns: db.collection(COOLDOWN_COLLECTION)
|
||
};
|
||
}
|
||
|
||
|
||
/*
|
||
* COOKIE HELPERS
|
||
*/
|
||
|
||
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
|
||
}
|
||
);
|
||
}
|
||
|
||
|
||
/*
|
||
* COOKIE TRANSFER
|
||
*/
|
||
|
||
async function transferCookies(
|
||
fromUserId,
|
||
toUserId,
|
||
amount
|
||
) {
|
||
const { cookies } =
|
||
getCollections();
|
||
|
||
|
||
/*
|
||
* Atomically debit the sender only if
|
||
* they still have enough cookies.
|
||
*/
|
||
const debit =
|
||
await cookies.updateOne(
|
||
{
|
||
userId:
|
||
fromUserId,
|
||
|
||
cookies: {
|
||
$gte:
|
||
amount
|
||
}
|
||
},
|
||
{
|
||
$inc: {
|
||
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
|
||
}
|
||
},
|
||
{
|
||
upsert:
|
||
true
|
||
}
|
||
);
|
||
|
||
} 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;
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* COOLDOWN HELPERS
|
||
*
|
||
* cooldowns_cookies:
|
||
*
|
||
* {
|
||
* userId: "...",
|
||
* guildId: "...",
|
||
* collect: 1786741200,
|
||
* steal: 1786738500
|
||
* }
|
||
*/
|
||
|
||
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;
|
||
}
|
||
|
||
|
||
/*
|
||
* /cookies collect
|
||
*/
|
||
|
||
async function collect(interaction) {
|
||
const userId = interaction.user.id;
|
||
const guildId = interaction.guildId;
|
||
|
||
const now = getUnixTime();
|
||
|
||
const cooldown = await getCooldown(
|
||
userId,
|
||
guildId,
|
||
"collect"
|
||
);
|
||
|
||
|
||
if (cooldown > now) {
|
||
return interaction.reply({
|
||
content:
|
||
`The batch is still cooking. Come back <t:${cooldown}:R> 🍪`,
|
||
ephemeral: true
|
||
});
|
||
}
|
||
|
||
|
||
let amount = randomInteger(1, 20);
|
||
|
||
|
||
/*
|
||
* Legacy Abra bonus.
|
||
*/
|
||
if (userId === ABRA_ID) {
|
||
amount = Math.ceil(
|
||
amount * ABRA_MULTIPLIER
|
||
);
|
||
}
|
||
|
||
|
||
await changeCookies(
|
||
userId,
|
||
amount
|
||
);
|
||
|
||
|
||
await setCooldown(
|
||
userId,
|
||
guildId,
|
||
"collect",
|
||
COLLECT_COOLDOWN
|
||
);
|
||
|
||
|
||
return interaction.reply({
|
||
content: randomCollectResponse(
|
||
amount,
|
||
interaction.user
|
||
)
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* /cookies steal
|
||
*/
|
||
|
||
async function steal(interaction) {
|
||
const user =
|
||
interaction.user;
|
||
|
||
const target =
|
||
interaction.options.getUser(
|
||
"target",
|
||
true
|
||
);
|
||
|
||
|
||
/*
|
||
* VALIDATION
|
||
*/
|
||
|
||
if (target.id === user.id) {
|
||
return interaction.reply({
|
||
content:
|
||
"C'mon, don't embarrass yourself. You can't steal from yourself.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
if (target.bot) {
|
||
return interaction.reply({
|
||
content:
|
||
"Bots have highly sophisticated Cookie security systems.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* COOLDOWN
|
||
*/
|
||
|
||
const now =
|
||
getUnixTime();
|
||
|
||
|
||
const cooldown =
|
||
await getCooldown(
|
||
user.id,
|
||
interaction.guildId,
|
||
"steal"
|
||
);
|
||
|
||
|
||
if (cooldown > now) {
|
||
return interaction.reply({
|
||
content:
|
||
`Lay low for a while. You can steal again <t:${cooldown}:R>.`,
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* BALANCES
|
||
*/
|
||
|
||
const [
|
||
yourCookies,
|
||
theirCookies
|
||
] =
|
||
await Promise.all([
|
||
getCookieBalance(
|
||
user.id
|
||
),
|
||
|
||
getCookieBalance(
|
||
target.id
|
||
)
|
||
]);
|
||
|
||
|
||
if (yourCookies < 1) {
|
||
return interaction.reply({
|
||
content:
|
||
"You need at least **1 Cookie** to attempt a robbery.",
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
if (theirCookies < 1) {
|
||
return interaction.reply({
|
||
content:
|
||
`Shame on you! ${target} doesn't have any Cookies. D:`,
|
||
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* DYNAMIC SUCCESS CHANCE
|
||
*/
|
||
|
||
const successChance =
|
||
getStealChance(
|
||
yourCookies,
|
||
theirCookies
|
||
);
|
||
|
||
|
||
const success =
|
||
Math.random() <
|
||
successChance;
|
||
|
||
|
||
/*
|
||
* SUCCESS
|
||
*/
|
||
|
||
if (success) {
|
||
|
||
/*
|
||
* Roll a weighted percentage
|
||
* of the target's entire vault.
|
||
*/
|
||
|
||
const {
|
||
amount,
|
||
percentage
|
||
} =
|
||
getStealAmount(
|
||
theirCookies
|
||
);
|
||
|
||
|
||
try {
|
||
await transferCookies(
|
||
target.id,
|
||
user.id,
|
||
amount
|
||
);
|
||
|
||
} catch (error) {
|
||
|
||
/*
|
||
* 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.`,
|
||
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
throw error;
|
||
}
|
||
|
||
|
||
/*
|
||
* Apply cooldown after robbery.
|
||
*/
|
||
|
||
const nextSteal =
|
||
await setCooldown(
|
||
user.id,
|
||
interaction.guildId,
|
||
"steal",
|
||
STEAL_COOLDOWN
|
||
);
|
||
|
||
|
||
const actualPercentage =
|
||
(
|
||
amount /
|
||
theirCookies *
|
||
100
|
||
);
|
||
|
||
|
||
return interaction.reply({
|
||
content:
|
||
`${randomStealSuccessResponse(amount, target)}\n` +
|
||
`🍪 You made off with **${actualPercentage.toFixed(1)}%** of their vault!\n` +
|
||
`🎲 **Success Chance:** ${formatPercentage(successChance)}\n` +
|
||
`⏱️ Next robbery <t:${nextSteal}:R>`
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* FAILURE
|
||
*
|
||
* Penalty is also percentage based.
|
||
*
|
||
* Roll between roughly 1–8% of the
|
||
* attacker's current vault.
|
||
*/
|
||
|
||
const penaltyPercentage =
|
||
randomFloat(
|
||
0.01,
|
||
0.08
|
||
);
|
||
|
||
|
||
const penalty =
|
||
Math.max(
|
||
1,
|
||
|
||
Math.min(
|
||
yourCookies,
|
||
|
||
Math.floor(
|
||
yourCookies *
|
||
penaltyPercentage
|
||
)
|
||
)
|
||
);
|
||
|
||
|
||
try {
|
||
await transferCookies(
|
||
user.id,
|
||
target.id,
|
||
penalty
|
||
);
|
||
|
||
} catch (error) {
|
||
|
||
if (
|
||
error.message ===
|
||
"INSUFFICIENT_COOKIES"
|
||
) {
|
||
return interaction.reply({
|
||
content:
|
||
"Your Cookie balance changed before the robbery could finish.",
|
||
|
||
flags:
|
||
MessageFlags.Ephemeral
|
||
});
|
||
}
|
||
|
||
|
||
throw error;
|
||
}
|
||
|
||
|
||
/*
|
||
* Apply cooldown.
|
||
*/
|
||
|
||
const nextSteal =
|
||
await setCooldown(
|
||
user.id,
|
||
interaction.guildId,
|
||
"steal",
|
||
STEAL_COOLDOWN
|
||
);
|
||
|
||
|
||
const actualPenaltyPercentage =
|
||
(
|
||
penalty /
|
||
yourCookies *
|
||
100
|
||
);
|
||
|
||
|
||
return interaction.reply({
|
||
content:
|
||
`${randomStealFailResponse(penalty, target)}\n` +
|
||
`💸 You lost **${actualPenaltyPercentage.toFixed(1)}%** of your vault.\n` +
|
||
`🎲 **Success Chance:** ${formatPercentage(successChance)}\n` +
|
||
`⏱️ Next robbery <t:${nextSteal}:R>`
|
||
});
|
||
}
|
||
|
||
/*
|
||
* /cookies check
|
||
*/
|
||
|
||
async function check(interaction) {
|
||
const target =
|
||
interaction.options.getUser("target")
|
||
?? interaction.user;
|
||
|
||
|
||
const balance =
|
||
await getCookieBalance(target.id);
|
||
|
||
|
||
const mood =
|
||
balance === 0
|
||
? "😔"
|
||
: "🍪";
|
||
|
||
|
||
return interaction.reply({
|
||
content:
|
||
`${target} has **${balance.toLocaleString()} Cookie${balance === 1 ? "" : "s"}**! ${mood}`
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* /cookies give
|
||
*/
|
||
|
||
async function give(interaction) {
|
||
const user = interaction.user;
|
||
|
||
const target = interaction.options.getUser(
|
||
"target",
|
||
true
|
||
);
|
||
|
||
const amount = interaction.options.getInteger(
|
||
"amount",
|
||
true
|
||
);
|
||
|
||
|
||
if (target.id === user.id) {
|
||
return interaction.reply({
|
||
content:
|
||
"You cannot give Cookies to yourself.",
|
||
ephemeral: true
|
||
});
|
||
}
|
||
|
||
|
||
if (target.bot) {
|
||
return interaction.reply({
|
||
content:
|
||
"Bots don't need Cookies. Allegedly.",
|
||
ephemeral: true
|
||
});
|
||
}
|
||
|
||
|
||
try {
|
||
await transferCookies(
|
||
user.id,
|
||
target.id,
|
||
amount
|
||
);
|
||
} catch (error) {
|
||
if (error.message === "INSUFFICIENT_COOKIES") {
|
||
return interaction.reply({
|
||
content:
|
||
"We don't run a charity here. You don't have enough Cookies!",
|
||
ephemeral: true
|
||
});
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
|
||
|
||
return interaction.reply({
|
||
content:
|
||
`${user} shared **${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}** with ${target} 🍪`
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* /cookies leaderboard
|
||
*/
|
||
|
||
async function leaderboard(interaction) {
|
||
const { cookies } = getCollections();
|
||
|
||
|
||
const results = await cookies
|
||
.find({
|
||
cookies: {
|
||
$gt: 0
|
||
}
|
||
})
|
||
.sort({
|
||
cookies: -1
|
||
})
|
||
.limit(10)
|
||
.toArray();
|
||
|
||
|
||
const description = results.length
|
||
? results
|
||
.map((record, index) => {
|
||
const medal = getLeaderboardMedal(index);
|
||
|
||
return (
|
||
`${medal} <@${record.userId}> — ` +
|
||
`**${record.cookies.toLocaleString()}** 🍪`
|
||
);
|
||
})
|
||
.join("\n")
|
||
: "Nobody has collected any Cookies yet.";
|
||
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setTitle("🍪 Cookie Leaderboard")
|
||
.setDescription(description)
|
||
.setTimestamp();
|
||
|
||
|
||
if (interaction.guild?.iconURL()) {
|
||
embed.setThumbnail(
|
||
interaction.guild.iconURL({
|
||
size: 256
|
||
})
|
||
);
|
||
}
|
||
|
||
|
||
return interaction.reply({
|
||
embeds: [embed]
|
||
});
|
||
}
|
||
|
||
|
||
/*
|
||
* COLLECT RESPONSES
|
||
*/
|
||
|
||
function randomCollectResponse(amount, user) {
|
||
const cookies =
|
||
`**${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}**`;
|
||
|
||
|
||
const responses = [
|
||
`${user}, I made you ${cookies} 🍪`,
|
||
`${user}, here's your ${cookies} 🍪 I ate the rest, sorry >.<`,
|
||
`Only ${cookies} 🍪 for you today ${user}... Abra stole the rest :c`,
|
||
`${user}, would you like a glass of milk with your ${cookies}? 🥛`,
|
||
`${cookies} 🍪 Careful, ${user}! They're still warm :3`,
|
||
`${cookies} 🍪 Best batch yet!`,
|
||
`${user}, these ${cookies} I bequeath unto thine own self upon this day.`,
|
||
`${user}, your ${cookies} are fresh out of the oven. Try not to burn your tongue!`,
|
||
`${user}, I bribed the Cookie Monster to spare these ${cookies} just for you.`,
|
||
`${user}, Santa came early! You got ${cookies}. Milk not included.`,
|
||
`${user}, your ${cookies} are artisanal, gluten-free, and slightly cursed.`,
|
||
`A wild baker appeared! They hand ${user} ${cookies} 🍪`,
|
||
`${user}, Abra said these ${cookies} are "totally not stolen"... 🤔`,
|
||
`${user}, here's your ${cookies} — watch out, they may be booby-trapped.`
|
||
];
|
||
|
||
|
||
return randomItem(responses);
|
||
}
|
||
|
||
|
||
/*
|
||
* STEAL SUCCESS RESPONSES
|
||
*/
|
||
|
||
function randomStealSuccessResponse(amount, target) {
|
||
const cookies =
|
||
`**${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}**`;
|
||
|
||
|
||
const responses = [
|
||
`You've successfully stolen ${cookies} from ${target}!`,
|
||
`Ninja mode activated. You snatched ${cookies} from ${target} without leaving a crumb behind.`,
|
||
`Abra taught you well — ${cookies} from ${target} are now yours!`,
|
||
`Like a phantom in the night, you took ${cookies} from ${target}.`,
|
||
`You just pulled the sweetest heist: ${cookies} from ${target}!`,
|
||
`Mission complete. ${target} is ${cookies} poorer.`,
|
||
`You distracted ${target} with a dance and swiped ${cookies}.`,
|
||
`Clean getaway! ${target} won't notice the missing ${cookies} until it's too late.`,
|
||
`The Cookie Vault has been breached. ${cookies} taken from ${target}.`,
|
||
`You emerged from the shadows carrying ${cookies} from ${target}.`
|
||
];
|
||
|
||
|
||
return randomItem(responses);
|
||
}
|
||
|
||
|
||
/*
|
||
* STEAL FAILURE RESPONSES
|
||
*/
|
||
|
||
function randomStealFailResponse(amount, target) {
|
||
const cookies =
|
||
`**${amount.toLocaleString()} Cookie${amount === 1 ? "" : "s"}**`;
|
||
|
||
|
||
const responses = [
|
||
`You were caught red-handed by ${target} and lost ${cookies} as a penalty!`,
|
||
`Fail! ${target} caught you mid-bite — you dropped ${cookies}.`,
|
||
`Not your best moment... You lost ${cookies} to ${target} in the struggle.`,
|
||
`Stealth level: 0. You handed ${cookies} to ${target} as an apology.`,
|
||
`The Cookie jar was booby-trapped! You gave ${cookies} to ${target} while running away.`,
|
||
`Ouch. The guard dog barked, and now ${target} has your ${cookies}.`,
|
||
`You tripped over your own shoelace — ${target} picked up ${cookies}.`,
|
||
`Caught by the Cookie Police! ${cookies} confiscated and given to ${target}.`,
|
||
`Your pockets had holes... ${target} found the ${cookies} you dropped.`
|
||
];
|
||
|
||
|
||
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
|
||
*/
|
||
|
||
function getUnixTime() {
|
||
return Math.floor(
|
||
Date.now() / 1000
|
||
);
|
||
}
|
||
|
||
|
||
function randomInteger(min, max) {
|
||
return Math.floor(
|
||
Math.random() * (max - min + 1)
|
||
) + min;
|
||
}
|
||
|
||
|
||
function randomItem(items) {
|
||
return items[
|
||
Math.floor(
|
||
Math.random() * items.length
|
||
)
|
||
];
|
||
}
|
||
|
||
|
||
function formatPercentage(value) {
|
||
return `${Math.round(value * 100)}%`;
|
||
}
|
||
|
||
|
||
function getLeaderboardMedal(index) {
|
||
switch (index) {
|
||
case 0:
|
||
return "🥇";
|
||
|
||
case 1:
|
||
return "🥈";
|
||
|
||
case 2:
|
||
return "🥉";
|
||
|
||
default:
|
||
return `**${index + 1}.**`;
|
||
}
|
||
} |