Compare commits

...

2 Commits

Author SHA1 Message Date
DeSqBlocki e3dbb11e12 snapshot migration pre-phase 6, stable 2026-08-27 21:14:27 +02:00
DeSqBlocki 14a059034c snapshot migration subPhase 5.5 2026-08-27 19:50:12 +02:00
10 changed files with 1223 additions and 149 deletions
+1
View File
@@ -1,3 +1,4 @@
node_modules node_modules
.env .env
backup/ backup/
*.bak
+34 -23
View File
@@ -451,8 +451,8 @@ async function steal(interaction) {
interaction, interaction,
user.id, user.id,
{ {
type: type: "steal_success",
"steal_success", source: "steal",
amount, amount,
targetUserId: targetUserId:
target.id target.id
@@ -464,9 +464,11 @@ async function steal(interaction) {
interaction, interaction,
user.id, user.id,
{ {
type: type: "cookie_gain",
"cookie_gain", source: "steal",
amount amount,
targetUserId:
target.id
} }
); );
@@ -479,22 +481,23 @@ async function steal(interaction) {
interaction, interaction,
target.id, target.id,
{ {
type: type: "stolen_from_me",
"stolen_from_me", source: "steal",
amount, amount,
thiefUserId: thiefUserId:
user.id user.id
} }
); );
await trackProfileEvent( await trackProfileEvent(
interaction, interaction,
target.id, target.id,
{ {
type: type: "cookie_loss",
"cookie_loss", source: "steal",
amount amount,
thiefUserId:
user.id
} }
); );
@@ -657,10 +660,12 @@ async function steal(interaction) {
interaction, interaction,
user.id, user.id,
{ {
type: type: "cookie_loss",
"cookie_loss", source: "steal_penalty",
amount: amount:
penalty penalty,
targetUserId:
target.id
} }
); );
@@ -669,10 +674,12 @@ async function steal(interaction) {
interaction, interaction,
target.id, target.id,
{ {
type: type: "cookie_gain",
"cookie_gain", source: "steal_penalty",
amount: amount:
penalty penalty,
thiefUserId:
user.id
} }
); );
@@ -906,9 +913,11 @@ async function give(interaction) {
interaction, interaction,
user.id, user.id,
{ {
type: type: "cookies_given",
"cookies_given", source: "give",
amount amount,
targetUserId:
target.id
} }
); );
@@ -921,9 +930,11 @@ async function give(interaction) {
interaction, interaction,
target.id, target.id,
{ {
type: type: "cookie_gain",
"cookie_gain", source: "give",
amount amount,
senderUserId:
user.id
} }
); );
+12
View File
@@ -166,6 +166,18 @@ async function declareBankruptcy(interaction, userId = interaction.user.id) {
userId, userId,
{ {
type: "cookie_gain", type: "cookie_gain",
source: "bankruptcy",
amount:
BANKRUPTCY_GRANT
}
);
await trackProfileEvent(
interaction,
userId,
{
type: "bankruptcy",
source: "bankruptcy",
amount: BANKRUPTCY_GRANT amount: BANKRUPTCY_GRANT
} }
); );
+10 -3
View File
@@ -269,17 +269,24 @@ async function resolveCookieHeist(
} }
); );
/*
* ============================================================
* HEIST
* ============================================================
*/
await trackProfileEvent( await trackProfileEvent(
interaction, interaction,
playerId, playerId,
{ {
type: type: "vault_steal",
"vault_steal", source: "heist",
game: "heist",
result: "win",
amount amount
} }
); );
const [ const [
playerAfter, playerAfter,
vaultAfter vaultAfter
+1 -1
View File
@@ -303,7 +303,7 @@ async function startBlackjack(
type: "game_start", type: "game_start",
game: "blackjack", game: "blackjack",
source, source,
bet, wager: bet,
allIn, allIn,
previousAllIn, previousAllIn,
previousBet previousBet
+1
View File
@@ -92,6 +92,7 @@ async function collectCookies(
userId, userId,
{ {
type: "cookie_gain", type: "cookie_gain",
source: "collect",
amount amount
} }
); );
+1 -1
View File
@@ -502,7 +502,7 @@ async function playRoulette(
type: "game_start", type: "game_start",
game: "roulette", game: "roulette",
source, source,
bet, wager: bet,
allIn, allIn,
previousAllIn, previousAllIn,
previousBet previousBet
+8 -2
View File
@@ -106,7 +106,9 @@ async function showSlots(interaction) {
async function playSlots( async function playSlots(
interaction, interaction,
bet, bet,
source = "normal" source = "normal",
previousAllIn = false,
previousBet = 0
) { ) {
const ownerId = const ownerId =
getSlotsSessionOwner( getSlotsSessionOwner(
@@ -206,7 +208,11 @@ async function playSlots(
type: "game_start", type: "game_start",
game: "slots", game: "slots",
source, source,
bet wager: bet,
allIn:
bet === playerBalance,
previousAllIn,
previousBet
} }
); );
+360 -2
View File
@@ -6,10 +6,18 @@ const {
} = require("./profileSystem"); } = require("./profileSystem");
const { const {
getUnixTime getUnixTime,
changeCookies
} = require("./cookieEconomy"); } = require("./cookieEconomy");
const DEFAULT_MAINTENANCE_REWARD_ID =
"maintenance_2026_08_27";
const DEFAULT_MAINTENANCE_REWARD_AMOUNT =
5000;
function getActiveDefinitions( function getActiveDefinitions(
achievementConfig achievementConfig
) { ) {
@@ -356,6 +364,292 @@ async function reconcileAllAchievements({
} }
async function grantMaintenanceReward({
guildId,
rewardId =
DEFAULT_MAINTENANCE_REWARD_ID,
amount =
DEFAULT_MAINTENANCE_REWARD_AMOUNT,
dryRun = false
}) {
if (!guildId) {
throw new Error(
"guildId is required."
);
}
const normalizedRewardId =
String(
rewardId ?? ""
).trim();
if (!normalizedRewardId) {
throw new Error(
"rewardId is required."
);
}
const normalizedAmount =
Number(
amount
);
if (
!Number.isSafeInteger(
normalizedAmount
)
||
normalizedAmount <= 0
) {
throw new Error(
"Maintenance reward amount must be a positive safe integer."
);
}
const {
stats
} =
getProfileCollections();
const userIds =
await stats.distinct(
"userId",
{
guildId
}
);
const summary = {
guildId,
rewardId:
normalizedRewardId,
amount:
normalizedAmount,
users:
userIds.length,
eligible:
0,
granted:
0,
alreadyClaimed:
0,
failed:
0,
dryRun,
results: []
};
for (const userId of userIds) {
const existing =
await stats.findOne(
{
userId,
guildId
},
{
projection: {
"maintenance.rewardsClaimed":
1
}
}
);
const rewardsClaimed =
Array.isArray(
existing
?.maintenance
?.rewardsClaimed
)
? existing
.maintenance
.rewardsClaimed
: [];
if (
rewardsClaimed.includes(
normalizedRewardId
)
) {
summary.alreadyClaimed++;
summary.results.push({
userId,
status:
"already_claimed"
});
continue;
}
summary.eligible++;
if (dryRun) {
summary.results.push({
userId,
status:
"would_grant"
});
console.log(
`[ProfileMaintenance] Dry run: would grant ${normalizedAmount} Cookies to ${userId} for ${normalizedRewardId}.`
);
continue;
}
const reservation =
await stats.updateOne(
{
userId,
guildId,
"maintenance.rewardsClaimed": {
$ne:
normalizedRewardId
}
},
{
$addToSet: {
"maintenance.rewardsClaimed":
normalizedRewardId
}
}
);
if (
reservation.modifiedCount !==
1
) {
summary.alreadyClaimed++;
summary.results.push({
userId,
status:
"already_claimed"
});
continue;
}
try {
await changeCookies(
userId,
normalizedAmount
);
summary.granted++;
summary.results.push({
userId,
status:
"granted"
});
console.log(
`[ProfileMaintenance] Granted ${normalizedAmount} Cookies to ${userId} for ${normalizedRewardId}.`
);
}
catch (error) {
await stats.updateOne(
{
userId,
guildId
},
{
$pull: {
"maintenance.rewardsClaimed":
normalizedRewardId
}
}
);
summary.failed++;
summary.results.push({
userId,
status:
"failed",
error:
error?.message ??
String(error)
});
console.error(
`[ProfileMaintenance] Failed to grant ${normalizedAmount} Cookies to ${userId} for ${normalizedRewardId}:`,
error
);
}
}
console.log(
`[ProfileMaintenance] Maintenance reward ${normalizedRewardId}: ${summary.granted} granted, ${summary.alreadyClaimed} already claimed, ${summary.failed} failed, ${summary.users} stored profile user(s) checked.`
);
return summary;
}
async function getMaintenanceRewardStatus({
guildId,
rewardId =
DEFAULT_MAINTENANCE_REWARD_ID
}) {
if (!guildId) {
throw new Error(
"guildId is required."
);
}
const normalizedRewardId =
String(
rewardId ?? ""
).trim();
if (!normalizedRewardId) {
throw new Error(
"rewardId is required."
);
}
const {
stats
} =
getProfileCollections();
const [
users,
claimed
] =
await Promise.all([
stats.countDocuments({
guildId
}),
stats.countDocuments({
guildId,
"maintenance.rewardsClaimed":
normalizedRewardId
})
]);
return {
guildId,
rewardId:
normalizedRewardId,
amount:
DEFAULT_MAINTENANCE_REWARD_AMOUNT,
users,
claimed,
remaining:
Math.max(
0,
users - claimed
)
};
}
async function auditProfileData( async function auditProfileData(
guildId guildId
) { ) {
@@ -783,6 +1077,65 @@ async function auditProfileData(
} }
/*
* ----------------------------------------------------
* MAINTENANCE REWARDS
* ----------------------------------------------------
*/
const rewardsClaimed =
document
.maintenance
?.rewardsClaimed;
if (
rewardsClaimed !==
undefined
&&
!Array.isArray(
rewardsClaimed
)
) {
issues.push(
`${document.userId}: maintenance.rewardsClaimed is not an array.`
);
}
else if (
Array.isArray(
rewardsClaimed
)
) {
const uniqueRewards =
new Set(
rewardsClaimed
);
if (
uniqueRewards.size !==
rewardsClaimed.length
) {
issues.push(
`${document.userId}: maintenance.rewardsClaimed contains duplicate reward IDs.`
);
}
if (
rewardsClaimed.some(
rewardId =>
typeof rewardId !==
"string"
||
!rewardId.trim()
)
) {
issues.push(
`${document.userId}: maintenance.rewardsClaimed contains an invalid reward ID.`
);
}
}
/* /*
* ---------------------------------------------------- * ----------------------------------------------------
* SCHEMA VERSION * SCHEMA VERSION
@@ -1407,5 +1760,10 @@ function isValidWeekKey(
module.exports = { module.exports = {
reconcileUserAchievements, reconcileUserAchievements,
reconcileAllAchievements, reconcileAllAchievements,
auditProfileData grantMaintenanceReward,
getMaintenanceRewardStatus,
auditProfileData,
DEFAULT_MAINTENANCE_REWARD_ID,
DEFAULT_MAINTENANCE_REWARD_AMOUNT
}; };
+702 -24
View File
@@ -409,6 +409,145 @@ async function ensureProfileSystem() {
); );
} }
async function evaluateSocialRelationshipFlags(
userId,
guildId,
event
) {
const {
stats
} =
getProfileCollections();
const document =
await stats.findOne({
userId,
guildId
});
if (!document) {
return;
}
const social =
document.social ?? {};
const set = {};
/*
* ========================================================
* ET TU, CRUMB?
* ========================================================
*
* Successfully steal from somebody who has
* previously gifted Cookies to you.
*/
if (
event.type ===
"steal_success"
&&
event.targetUserId
&&
Array.isArray(
social.receivedFrom
)
&&
social.receivedFrom.includes(
event.targetUserId
)
) {
set[
"special.etTuCrumb"
] = 1;
}
/*
* ========================================================
* NO HARD FEELINGS
* ========================================================
*
* Give Cookies to somebody who has previously
* successfully stolen from you.
*/
if (
event.type ===
"cookies_given"
&&
event.targetUserId
&&
Array.isArray(
social.successfullyStolenBy
)
&&
social.successfullyStolenBy.includes(
event.targetUserId
)
) {
set[
"special.noHardFeelings"
] = 1;
}
/*
* ========================================================
* COOKIE LAUNDERING
* ========================================================
*
* Give Cookies to somebody you have previously
* successfully stolen from.
*/
if (
event.type ===
"cookies_given"
&&
event.targetUserId
&&
Array.isArray(
social.successfullyStolenFrom
)
&&
social.successfullyStolenFrom.includes(
event.targetUserId
)
) {
set[
"special.cookieLaundering"
] = 1;
}
if (
Object.keys(
set
).length ===
0
) {
return;
}
await stats.updateOne(
{
userId,
guildId
},
{
$set:
set
}
);
}
function getDefaultStats(userId, guildId) { function getDefaultStats(userId, guildId) {
return { return {
userId, userId,
@@ -486,8 +625,21 @@ function getDefaultStats(userId, guildId) {
bestLoss: 0 bestLoss: 0
}, },
social: {
giftedTo: [],
receivedFrom: [],
successfullyStolenFrom: [],
successfullyStolenBy: []
},
special: { special: {
allInRunItBack: 0 allInRunItBack: 0,
bankruptcyCount: 0,
bankruptcyRecoveryBest: 0,
etTuCrumb: 0,
noHardFeelings: 0,
cookieLaundering: 0
}, },
activity: { activity: {
@@ -517,6 +669,22 @@ function getDefaultStats(userId, guildId) {
evening: 0 evening: 0
} }
}, },
daily: {
dateKey: null,
gamesPlayed: [],
gamesWon: [],
gamesLost: [],
collectedCookies: false,
gambledAfterCollect: false,
wonAnyGame: false,
lostAnyGame: false,
gaveCookies: false,
stoleCookies: false
},
profileSchemaVersion: 2 profileSchemaVersion: 2
}; };
@@ -590,16 +758,54 @@ async function trackProfileEvent(
switch (event.type) { switch (event.type) {
case "cookie_gain": case "cookie_gain":
await stats.updateOne( await stats.updateOne(
{ userId, guildId }, {
userId,
guildId
},
{ {
$inc: { $inc: {
"economy.cookiesObtainedTotal": "economy.cookiesObtainedTotal":
event.amount event.amount
},
...(
event.source === "give" &&
event.senderUserId
? {
$addToSet: {
"social.receivedFrom":
event.senderUserId
},
$set: {
"special.lastGiftSenderId":
event.senderUserId
}
}
: {}
)
}
);
break;
case "bankruptcy":
await stats.updateOne(
{
userId,
guildId
},
{
$inc: {
"special.bankruptcyCount":
1
},
$set: {
"special.bankruptcyRecoveryBest":
0
} }
} }
); );
break; break;
case "cookie_loss": case "cookie_loss":
await stats.updateOne( await stats.updateOne(
{ userId, guildId }, { userId, guildId },
@@ -614,48 +820,87 @@ async function trackProfileEvent(
case "cookies_given": case "cookies_given":
await stats.updateOne( await stats.updateOne(
{ userId, guildId }, {
userId,
guildId
},
{ {
$inc: { $inc: {
"economy.cookiesGivenTotal": "economy.cookiesGivenTotal":
event.amount event.amount
},
...(event.targetUserId
? {
$addToSet: {
"social.giftedTo":
event.targetUserId
},
$set: {
"special.lastGiftTargetId":
event.targetUserId
} }
} }
: {})
}
); );
break; break;
case "steal_success": case "steal_success":
await stats.updateOne( await stats.updateOne(
{ userId, guildId }, {
userId,
guildId
},
{ {
$inc: { $inc: {
"economy.stolenTotal": "economy.stolenTotal":
event.amount event.amount
}, },
$addToSet: { $addToSet: {
"unique.playersStolenFrom": "unique.playersStolenFrom":
event.targetUserId,
"social.successfullyStolenFrom":
event.targetUserId
},
$set: {
"special.lastSuccessfulStealTargetId":
event.targetUserId event.targetUserId
} }
} }
); );
break; break;
case "stolen_from_me": case "stolen_from_me":
await stats.updateOne( await stats.updateOne(
{ userId, guildId }, {
userId,
guildId
},
{ {
$inc: { $inc: {
"economy.stolenFromMeTotal": "economy.stolenFromMeTotal":
event.amount event.amount
}, },
$addToSet: { $addToSet: {
"unique.playersStolenBy": "unique.playersStolenBy":
event.thiefUserId,
"social.successfullyStolenBy":
event.thiefUserId
},
$set: {
"special.lastSuccessfulThiefId":
event.thiefUserId event.thiefUserId
} }
} }
); );
break; break;
case "vault_steal": case "vault_steal":
await stats.updateOne( await stats.updateOne(
{ userId, guildId }, { userId, guildId },
@@ -694,6 +939,53 @@ async function trackProfileEvent(
return; return;
} }
if (
[
"steal_success",
"cookies_given"
].includes(
event.type
)
) {
await evaluateSocialRelationshipFlags(
userId,
guildId,
event
);
}
if (
[
"cookie_gain",
"cookie_loss",
"game_result",
"bankruptcy"
].includes(
event.type
)
) {
await updateBankruptcyRecovery(
userId,
guildId
);
}
if (
event.type ===
"game_result"
) {
await trackDailyGameState(
userId,
guildId,
event
);
}
await trackDailyNarrativeState(
userId,
guildId,
event
);
const updatedStats = const updatedStats =
await getProfileStats( await getProfileStats(
userId, userId,
@@ -804,18 +1096,22 @@ async function trackGameStart(
const set = {}; const set = {};
const previousBet = Number( const previousWager = Number(
event.previousBet ?? 0 event.previousWager ??
event.previousBet ??
0
); );
const currentBet = Number( const currentWager = Number(
event.bet ?? 0 event.wager ??
event.bet ??
0
); );
const chainedAllIn = const chainedAllIn =
event.previousAllIn === true && event.previousAllIn === true &&
previousBet > 0 && previousWager > 0 &&
currentBet === previousBet; currentWager === previousWager;
if (chainedAllIn) { if (chainedAllIn) {
set["special.allInRunItBack"] = 1; set["special.allInRunItBack"] = 1;
@@ -1401,8 +1697,21 @@ async function evaluateBadges(
event.type === "game_start" && event.type === "game_start" &&
event.source === "run_it_back" && event.source === "run_it_back" &&
event.previousAllIn === true && event.previousAllIn === true &&
Number(event.previousBet) > 0 && Number(
Number(event.bet) === Number(event.previousBet) event.previousWager ??
event.previousBet ??
0
) > 0 &&
Number(
event.wager ??
event.bet ??
0
) ===
Number(
event.previousWager ??
event.previousBet ??
0
)
) { ) {
await tryClaimBadge( await tryClaimBadge(
interaction, interaction,
@@ -1434,15 +1743,14 @@ async function evaluateCollectorBadges(
.find({ .find({
active: { active: {
$ne: false $ne: false
},
measure: {
$ne:
"achievementPercentage"
} }
}) })
.toArray() .toArray()
).filter(achievement => ).filter(
Boolean(achievement.stat) achievement =>
isStatBackfillableAchievement(
achievement
)
); );
if ( if (
@@ -2070,8 +2378,9 @@ async function buildAchievementsEmbed(
const collectibleDefinitions = const collectibleDefinitions =
definitions.filter( definitions.filter(
definition => definition =>
definition.measure !== isStatBackfillableAchievement(
"achievementPercentage" definition
)
); );
const unlockedCollectible = const unlockedCollectible =
@@ -3279,6 +3588,371 @@ async function evaluateEventAchievements(
); );
} }
} }
async function updateBankruptcyRecovery(
userId,
guildId
) {
const {
stats
} =
getProfileCollections();
const document =
await stats.findOne(
{
userId,
guildId
},
{
projection: {
"special.bankruptcyCount":
1
}
}
);
if (
Number(
document
?.special
?.bankruptcyCount ?? 0
) <= 0
) {
return;
}
const balance =
await getCookieBalance(
userId
);
await stats.updateOne(
{
userId,
guildId
},
{
$max: {
"special.bankruptcyRecoveryBest":
balance
}
}
);
}
async function trackDailyGameState(
userId,
guildId,
event
) {
if (
event.type !==
"game_result"
) {
return;
}
const game =
String(
event.game ?? ""
).toLowerCase();
const result =
String(
event.result ?? ""
).toLowerCase();
if (
![
"slots",
"blackjack",
"roulette",
"heist"
].includes(
game
)
) {
return;
}
const {
stats
} =
getProfileCollections();
const {
dateKey
} =
getActivityDateContext();
const document =
await stats.findOne(
{
userId,
guildId
},
{
projection: {
daily: 1
}
}
);
const currentDateKey =
document
?.daily
?.dateKey ?? null;
if (
currentDateKey !==
dateKey
) {
await stats.updateOne(
{
userId,
guildId
},
{
$set: {
"daily.dateKey":
dateKey,
"daily.gamesPlayed":
[game],
"daily.gamesWon":
result === "win"
? [game]
: [],
"daily.gamesLost":
result === "loss"
? [game]
: [],
"daily.collectedCookies":
false,
"daily.gambledAfterCollect":
false,
"daily.wonAnyGame":
result === "win",
"daily.lostAnyGame":
result === "loss",
"daily.gaveCookies":
false,
"daily.stoleCookies":
false
}
}
);
return;
}
const addToSet = {
"daily.gamesPlayed":
game
};
const set = {};
if (
result ===
"win"
) {
addToSet[
"daily.gamesWon"
] = game;
set[
"daily.wonAnyGame"
] = true;
}
if (
result ===
"loss"
) {
addToSet[
"daily.gamesLost"
] = game;
set[
"daily.lostAnyGame"
] = true;
}
await stats.updateOne(
{
userId,
guildId
},
{
$addToSet:
addToSet,
...(
Object.keys(
set
).length
? {
$set:
set
}
: {}
)
}
);
}
async function trackDailyNarrativeState(
userId,
guildId,
event
) {
const {
stats
} =
getProfileCollections();
const {
dateKey
} =
getActivityDateContext();
const document =
await stats.findOne(
{
userId,
guildId
},
{
projection: {
daily: 1
}
}
);
const currentDateKey =
document
?.daily
?.dateKey ?? null;
if (
currentDateKey !==
dateKey
) {
await stats.updateOne(
{
userId,
guildId
},
{
$set: {
"daily.dateKey":
dateKey,
"daily.gamesPlayed":
[],
"daily.gamesWon":
[],
"daily.gamesLost":
[],
"daily.collectedCookies":
false,
"daily.gambledAfterCollect":
false,
"daily.wonAnyGame":
false,
"daily.lostAnyGame":
false,
"daily.gaveCookies":
false,
"daily.stoleCookies":
false
}
}
);
}
const set = {};
if (
event.type ===
"cookie_gain"
&&
event.source ===
"collect"
) {
set[
"daily.collectedCookies"
] = true;
}
if (
event.type ===
"cookies_given"
) {
set[
"daily.gaveCookies"
] = true;
}
if (
event.type ===
"steal_success"
) {
set[
"daily.stoleCookies"
] = true;
}
if (
event.type ===
"game_start"
&&
document
?.daily
?.collectedCookies === true
) {
set[
"daily.gambledAfterCollect"
] = true;
}
if (
Object.keys(
set
).length === 0
) {
return;
}
await stats.updateOne(
{
userId,
guildId
},
{
$set:
set
}
);
}
module.exports = { module.exports = {
ensureProfileSystem, ensureProfileSystem,
@@ -3288,6 +3962,7 @@ module.exports = {
getProfileCollections, getProfileCollections,
getDefaultStats, getDefaultStats,
getCompositeProgress, getCompositeProgress,
updateBankruptcyRecovery,
readStatValue, readStatValue,
readStatPath, readStatPath,
@@ -3316,4 +3991,7 @@ module.exports = {
evaluateEventCondition, evaluateEventCondition,
evaluateEventAchievement, evaluateEventAchievement,
evaluateEventAchievements, evaluateEventAchievements,
evaluateSocialRelationshipFlags,
trackDailyGameState,
trackDailyNarrativeState,
}; };