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
.env
backup/
*.bak
+50 -39
View File
@@ -447,56 +447,59 @@ async function steal(interaction) {
* Thief stats.
*/
await trackProfileEvent(
await trackProfileEvent(
interaction,
user.id,
{
type:
"steal_success",
type: "steal_success",
source: "steal",
amount,
targetUserId:
target.id
}
);
);
await trackProfileEvent(
await trackProfileEvent(
interaction,
user.id,
{
type:
"cookie_gain",
amount
type: "cookie_gain",
source: "steal",
amount,
targetUserId:
target.id
}
);
);
/*
* Victim stats.
*/
await trackProfileEvent(
await trackProfileEvent(
interaction,
target.id,
{
type:
"stolen_from_me",
type: "stolen_from_me",
source: "steal",
amount,
thiefUserId:
user.id
}
);
);
await trackProfileEvent(
await trackProfileEvent(
interaction,
target.id,
{
type:
"cookie_loss",
amount
type: "cookie_loss",
source: "steal",
amount,
thiefUserId:
user.id
}
);
);
const [
@@ -653,28 +656,32 @@ async function steal(interaction) {
* Profile tracking.
*/
await trackProfileEvent(
await trackProfileEvent(
interaction,
user.id,
{
type:
"cookie_loss",
type: "cookie_loss",
source: "steal_penalty",
amount:
penalty
penalty,
targetUserId:
target.id
}
);
);
await trackProfileEvent(
await trackProfileEvent(
interaction,
target.id,
{
type:
"cookie_gain",
type: "cookie_gain",
source: "steal_penalty",
amount:
penalty
penalty,
thiefUserId:
user.id
}
);
);
const [
@@ -902,30 +909,34 @@ async function give(interaction) {
* Sender lifetime giving.
*/
await trackProfileEvent(
await trackProfileEvent(
interaction,
user.id,
{
type:
"cookies_given",
amount
type: "cookies_given",
source: "give",
amount,
targetUserId:
target.id
}
);
);
/*
* Recipient lifetime gain.
*/
await trackProfileEvent(
await trackProfileEvent(
interaction,
target.id,
{
type:
"cookie_gain",
amount
type: "cookie_gain",
source: "give",
amount,
senderUserId:
user.id
}
);
);
const [
+12
View File
@@ -166,6 +166,18 @@ async function declareBankruptcy(interaction, userId = interaction.user.id) {
userId,
{
type: "cookie_gain",
source: "bankruptcy",
amount:
BANKRUPTCY_GRANT
}
);
await trackProfileEvent(
interaction,
userId,
{
type: "bankruptcy",
source: "bankruptcy",
amount: BANKRUPTCY_GRANT
}
);
+10 -3
View File
@@ -269,17 +269,24 @@ async function resolveCookieHeist(
}
);
/*
* ============================================================
* HEIST
* ============================================================
*/
await trackProfileEvent(
interaction,
playerId,
{
type:
"vault_steal",
type: "vault_steal",
source: "heist",
game: "heist",
result: "win",
amount
}
);
const [
playerAfter,
vaultAfter
+1 -1
View File
@@ -303,7 +303,7 @@ async function startBlackjack(
type: "game_start",
game: "blackjack",
source,
bet,
wager: bet,
allIn,
previousAllIn,
previousBet
+3 -2
View File
@@ -87,14 +87,15 @@ async function collectCookies(
);
await trackProfileEvent(
await trackProfileEvent(
interaction,
userId,
{
type: "cookie_gain",
source: "collect",
amount
}
);
);
const balance = await getCookieBalance(
+3 -3
View File
@@ -495,19 +495,19 @@ async function playRoulette(
const allIn =
bet === playerBalance;
await trackProfileEvent(
await trackProfileEvent(
interaction,
playerId,
{
type: "game_start",
game: "roulette",
source,
bet,
wager: bet,
allIn,
previousAllIn,
previousBet
}
);
);
await interaction.update({
embeds: [
+10 -4
View File
@@ -106,7 +106,9 @@ async function showSlots(interaction) {
async function playSlots(
interaction,
bet,
source = "normal"
source = "normal",
previousAllIn = false,
previousBet = 0
) {
const ownerId =
getSlotsSessionOwner(
@@ -199,16 +201,20 @@ async function playSlots(
throw error;
}
await trackProfileEvent(
await trackProfileEvent(
interaction,
playerId,
{
type: "game_start",
game: "slots",
source,
bet
wager: bet,
allIn:
bet === playerBalance,
previousAllIn,
previousBet
}
);
);
await interaction.update({
embeds: [
+360 -2
View File
@@ -6,10 +6,18 @@ const {
} = require("./profileSystem");
const {
getUnixTime
getUnixTime,
changeCookies
} = require("./cookieEconomy");
const DEFAULT_MAINTENANCE_REWARD_ID =
"maintenance_2026_08_27";
const DEFAULT_MAINTENANCE_REWARD_AMOUNT =
5000;
function getActiveDefinitions(
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(
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
@@ -1407,5 +1760,10 @@ function isValidWeekKey(
module.exports = {
reconcileUserAchievements,
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) {
return {
userId,
@@ -486,8 +625,21 @@ function getDefaultStats(userId, guildId) {
bestLoss: 0
},
social: {
giftedTo: [],
receivedFrom: [],
successfullyStolenFrom: [],
successfullyStolenBy: []
},
special: {
allInRunItBack: 0
allInRunItBack: 0,
bankruptcyCount: 0,
bankruptcyRecoveryBest: 0,
etTuCrumb: 0,
noHardFeelings: 0,
cookieLaundering: 0
},
activity: {
@@ -517,6 +669,22 @@ function getDefaultStats(userId, guildId) {
evening: 0
}
},
daily: {
dateKey: null,
gamesPlayed: [],
gamesWon: [],
gamesLost: [],
collectedCookies: false,
gambledAfterCollect: false,
wonAnyGame: false,
lostAnyGame: false,
gaveCookies: false,
stoleCookies: false
},
profileSchemaVersion: 2
};
@@ -590,16 +758,54 @@ async function trackProfileEvent(
switch (event.type) {
case "cookie_gain":
await stats.updateOne(
{ userId, guildId },
{
userId,
guildId
},
{
$inc: {
"economy.cookiesObtainedTotal":
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;
case "cookie_loss":
await stats.updateOne(
{ userId, guildId },
@@ -614,48 +820,87 @@ async function trackProfileEvent(
case "cookies_given":
await stats.updateOne(
{ userId, guildId },
{
userId,
guildId
},
{
$inc: {
"economy.cookiesGivenTotal":
event.amount
},
...(event.targetUserId
? {
$addToSet: {
"social.giftedTo":
event.targetUserId
},
$set: {
"special.lastGiftTargetId":
event.targetUserId
}
}
: {})
}
);
break;
case "steal_success":
await stats.updateOne(
{ userId, guildId },
{
userId,
guildId
},
{
$inc: {
"economy.stolenTotal":
event.amount
},
$addToSet: {
"unique.playersStolenFrom":
event.targetUserId,
"social.successfullyStolenFrom":
event.targetUserId
},
$set: {
"special.lastSuccessfulStealTargetId":
event.targetUserId
}
}
);
break;
case "stolen_from_me":
await stats.updateOne(
{ userId, guildId },
{
userId,
guildId
},
{
$inc: {
"economy.stolenFromMeTotal":
event.amount
},
$addToSet: {
"unique.playersStolenBy":
event.thiefUserId,
"social.successfullyStolenBy":
event.thiefUserId
},
$set: {
"special.lastSuccessfulThiefId":
event.thiefUserId
}
}
);
break;
case "vault_steal":
await stats.updateOne(
{ userId, guildId },
@@ -694,6 +939,53 @@ async function trackProfileEvent(
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 =
await getProfileStats(
userId,
@@ -804,18 +1096,22 @@ async function trackGameStart(
const set = {};
const previousBet = Number(
event.previousBet ?? 0
const previousWager = Number(
event.previousWager ??
event.previousBet ??
0
);
const currentBet = Number(
event.bet ?? 0
const currentWager = Number(
event.wager ??
event.bet ??
0
);
const chainedAllIn =
event.previousAllIn === true &&
previousBet > 0 &&
currentBet === previousBet;
previousWager > 0 &&
currentWager === previousWager;
if (chainedAllIn) {
set["special.allInRunItBack"] = 1;
@@ -1401,8 +1697,21 @@ async function evaluateBadges(
event.type === "game_start" &&
event.source === "run_it_back" &&
event.previousAllIn === true &&
Number(event.previousBet) > 0 &&
Number(event.bet) === Number(event.previousBet)
Number(
event.previousWager ??
event.previousBet ??
0
) > 0 &&
Number(
event.wager ??
event.bet ??
0
) ===
Number(
event.previousWager ??
event.previousBet ??
0
)
) {
await tryClaimBadge(
interaction,
@@ -1434,15 +1743,14 @@ async function evaluateCollectorBadges(
.find({
active: {
$ne: false
},
measure: {
$ne:
"achievementPercentage"
}
})
.toArray()
).filter(achievement =>
Boolean(achievement.stat)
).filter(
achievement =>
isStatBackfillableAchievement(
achievement
)
);
if (
@@ -2070,8 +2378,9 @@ async function buildAchievementsEmbed(
const collectibleDefinitions =
definitions.filter(
definition =>
definition.measure !==
"achievementPercentage"
isStatBackfillableAchievement(
definition
)
);
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 = {
ensureProfileSystem,
@@ -3288,6 +3962,7 @@ module.exports = {
getProfileCollections,
getDefaultStats,
getCompositeProgress,
updateBankruptcyRecovery,
readStatValue,
readStatPath,
@@ -3316,4 +3991,7 @@ module.exports = {
evaluateEventCondition,
evaluateEventAchievement,
evaluateEventAchievements,
evaluateSocialRelationshipFlags,
trackDailyGameState,
trackDailyNarrativeState,
};