Merge pull request #59 from cassoule/cases-balance

cases prices and content balance
This commit is contained in:
Milo Gourvest
2025-12-21 18:15:42 +01:00
committed by GitHub
3 changed files with 238 additions and 101 deletions

View File

@@ -18,10 +18,14 @@ import {
getAllUsers,
getUser,
hardUpdateSkin,
insertLog,
updateManyUsers,
updateSkin,
updateUserAvatar,
updateUserCoins,
} from "../../database/index.js";
import { client } from "../client.js";
import { drawCaseContent, drawCaseSkin } from "../../utils/caseOpening.js";
// Constants for the AI rate limiter
const MAX_REQUESTS_PER_INTERVAL = parseInt(process.env.MAX_REQUESTS || "5");
@@ -276,6 +280,90 @@ async function handleAdminCommands(message) {
});
});
console.log("Reworked", dbSkins.length, "skins.");
break;
case `${prefix}:cases-test`:
try {
const caseType = args[0] ?? "standard";
const caseCount = args[1] ?? 1;
let totalResValue = 0;
let highestSkinPrice = 0;
let priceTiers = {
0: 0,
100: 0,
200: 0,
300: 0,
400: 0,
500: 0,
600: 0,
700: 0,
800: 0,
900: 0,
1000: 0,
};
for (let i = 0; i < caseCount; i++) {
const skins = await drawCaseContent(caseType);
const result = drawCaseSkin(skins);
totalResValue += result.finalPrice;
if (result.finalPrice > highestSkinPrice) highestSkinPrice = result.finalPrice;
if (result.finalPrice > 0 && result.finalPrice < 100) priceTiers["0"] += 1;
if (result.finalPrice >= 100 && result.finalPrice < 200) priceTiers["100"] += 1;
if (result.finalPrice >= 200 && result.finalPrice < 300) priceTiers["200"] += 1;
if (result.finalPrice >= 300 && result.finalPrice < 400) priceTiers["300"] += 1;
if (result.finalPrice >= 400 && result.finalPrice < 500) priceTiers["400"] += 1;
if (result.finalPrice >= 500 && result.finalPrice < 600) priceTiers["500"] += 1;
if (result.finalPrice >= 600 && result.finalPrice < 700) priceTiers["600"] += 1;
if (result.finalPrice >= 700 && result.finalPrice < 800) priceTiers["700"] += 1;
if (result.finalPrice >= 800 && result.finalPrice < 900) priceTiers["800"] += 1;
if (result.finalPrice >= 900 && result.finalPrice < 1000) priceTiers["900"] += 1;
if (result.finalPrice >= 1000) priceTiers["1000"] += 1;
console.log(
`Case ${i + 1}: Won a skin worth ${result.finalPrice} Flopos, ${caseType}, ${result.updatedSkin.tierRank}`,
);
}
console.log(totalResValue / caseCount);
message.reply(
`${totalResValue / caseCount} average skin price over ${caseCount} ${caseType} cases.\nHighest skin price: ${highestSkinPrice}\nPrice tier distribution: ${JSON.stringify(priceTiers)}`,
);
} catch (e) {
console.log(e);
message.reply(`Error during case test: ${e.message}`);
}
case `${prefix}:refund-skins`:
try {
const DBskins = getAllSkins.all();
for (const skin of DBskins) {
const owner = getUser.get(skin.user_id);
if (owner) {
updateUserCoins.run({
id: owner.id,
coins: owner.coins + skin.currentPrice,
});
insertLog.run({
id: `${skin.uuid}-skin-refund-${Date.now()}`,
user_id: owner.id,
target_user_id: null,
action: "SKIN_REFUND",
coins_amount: skin.currentPrice,
user_new_amount: owner.coins + skin.currentPrice,
});
}
updateSkin.run({
uuid: skin.uuid,
user_id: null,
currentPrice: null,
currentLvl: null,
currentChroma: null,
});
}
message.reply("All skins refunded.");
} catch (e) {
console.log(e);
message.reply(`Error during refund skins ${e.message}`);
}
break;
}
}

View File

@@ -4,7 +4,6 @@ import { sleep } from "openai/core";
// --- Database Imports ---
import {
getAllAkhys,
getAllAvailableSkins,
getAllUsers,
getLogs,
getMarketOffersBySkin,
@@ -35,6 +34,7 @@ import { DiscordRequest } from "../../api/discord.js";
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { emitDataUpdated, socketEmit } from "../socket.js";
import { handleCaseOpening } from "../../utils/marketNotifs.js";
import { drawCaseContent, drawCaseSkin } from "../../utils/caseOpening.js";
// Create a new router instance
const router = express.Router();
@@ -120,114 +120,29 @@ export function apiRoutes(client, io) {
router.post("/open-case", async (req, res) => {
const { userId, caseType } = req.body;
let caseTypeVal, tierWeights;
let caseTypeVal;
switch (caseType) {
case "standard":
caseTypeVal = 1;
tierWeights = {
"12683d76-48d7-84a3-4e09-6985794f0445": 50, // Select
"0cebb8be-46d7-c12a-d306-e9907bfc5a25": 30, // Deluxe
"60bca009-4182-7998-dee7-b8a2558dc369": 15, // Premium
"e046854e-406c-37f4-6607-19a9ba8426fc": 4, // Exclusive
"411e4a55-4e59-7757-41f0-86a53f101bb5": 1, // Ultra
};
caseTypeVal = 500;
break;
case "premium":
caseTypeVal = 2;
tierWeights = {
"12683d76-48d7-84a3-4e09-6985794f0445": 35, // Select
"0cebb8be-46d7-c12a-d306-e9907bfc5a25": 30, // Deluxe
"60bca009-4182-7998-dee7-b8a2558dc369": 30, // Premium
"e046854e-406c-37f4-6607-19a9ba8426fc": 4, // Exclusive
"411e4a55-4e59-7757-41f0-86a53f101bb5": 1, // Ultra
};
caseTypeVal = 750;
break;
case "ultra":
caseTypeVal = 4;
tierWeights = {
"12683d76-48d7-84a3-4e09-6985794f0445": 33, // Select
"0cebb8be-46d7-c12a-d306-e9907bfc5a25": 28, // Deluxe
"60bca009-4182-7998-dee7-b8a2558dc369": 28, // Premium
"e046854e-406c-37f4-6607-19a9ba8426fc": 8, // Exclusive
"411e4a55-4e59-7757-41f0-86a53f101bb5": 3, // Ultra
};
caseTypeVal = 1000;
break;
default:
return res.status(400).json({ error: "Invalid case type." });
}
const commandUser = getUser.get(userId);
if (!commandUser) return res.status(404).json({ error: "User not found." });
const valoPrice = (parseInt(process.env.VALO_PRICE, 10) || 500) * caseTypeVal;
const valoPrice = caseTypeVal;
if (commandUser.coins < valoPrice) return res.status(403).json({ error: "Not enough FlopoCoins." });
try {
const dbSkins = getAllAvailableSkins.all();
const filteredSkins = skins.filter((s) => dbSkins.find((dbSkin) => dbSkin.uuid === s.uuid));
filteredSkins.forEach((s) => {
let dbSkin = getSkin.get(s.uuid);
s.tierColor = dbSkin?.tierColor;
});
filteredSkins.forEach((s) => {
s.weight = tierWeights[s.tierUuid] ?? 1; // fallback if missing
});
const selectedSkins = await drawCaseContent(caseType);
function weightedSample(arr, count) {
let totalWeight = arr.reduce((sum, x) => sum + x.weight, 0);
const list = [...arr];
const result = [];
for (let i = 0; i < count && list.length > 0; i++) {
let r = Math.random() * totalWeight;
let running = 0;
let pickIndex = -1;
for (let j = 0; j < list.length; j++) {
running += list[j].weight;
if (r <= running) {
pickIndex = j;
break;
}
}
if (pickIndex < 0) break;
const picked = list.splice(pickIndex, 1)[0];
result.push(picked);
// Subtract removed weight
totalWeight -= picked.weight;
}
return result;
}
const selectedSkins = weightedSample(filteredSkins, 100);
const randomSelectedSkinIndex = Math.floor(Math.random() * (selectedSkins.length - 1));
const randomSelectedSkinUuid = selectedSkins[randomSelectedSkinIndex].uuid;
const dbSkin = getSkin.get(randomSelectedSkinUuid);
const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid);
if (!randomSkinData) {
throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`);
}
// --- Randomize Level and Chroma ---
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
let randomChroma = 1;
if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) {
// Ensure chroma is at least 1 and not greater than the number of chromas
randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1;
}
// --- Calculate Price ---
const calculatePrice = () => {
let result = parseFloat(dbSkin.basePrice);
result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2);
result *= 1 + randomChroma / 4;
return parseFloat(result.toFixed(0));
};
const finalPrice = calculatePrice();
const result = drawCaseSkin(selectedSkins);
// --- Update Database ---
insertLog.run({
@@ -243,19 +158,24 @@ export function apiRoutes(client, io) {
coins: commandUser.coins - valoPrice,
});
updateSkin.run({
uuid: randomSkinData.uuid,
uuid: result.randomSkinData.uuid,
user_id: userId,
currentLvl: randomLevel,
currentChroma: randomChroma,
currentPrice: finalPrice,
currentLvl: result.randomLevel,
currentChroma: result.randomChroma,
currentPrice: result.finalPrice,
});
console.log(
`[${Date.now()}] ${userId} opened a ${caseType} Valorant case and received skin ${randomSelectedSkinUuid}`,
`[${Date.now()}] ${userId} opened a ${caseType} Valorant case and received skin ${result.randomSelectedSkinUuid}`,
);
const updatedSkin = getSkin.get(randomSkinData.uuid);
await handleCaseOpening(caseType, userId, randomSelectedSkinUuid, client);
res.json({ selectedSkins, randomSelectedSkinUuid, randomSelectedSkinIndex, updatedSkin });
const updatedSkin = getSkin.get(result.randomSkinData.uuid);
await handleCaseOpening(caseType, userId, result.randomSelectedSkinUuid, client);
res.json({
selectedSkins,
randomSelectedSkinUuid: result.randomSelectedSkinUuid,
randomSelectedSkinIndex: result.randomSelectedSkinIndex,
updatedSkin,
});
} catch (error) {
console.error("Error fetching skins:", error);
res.status(500).json({ error: "Failed to fetch skins." });

129
src/utils/caseOpening.js Normal file
View File

@@ -0,0 +1,129 @@
import { getAllAvailableSkins, getSkin } from "../database/index.js";
import { skins } from "../game/state.js";
export async function drawCaseContent(caseType = "standard") {
let tierWeights;
switch (caseType) {
case "standard":
tierWeights = {
"12683d76-48d7-84a3-4e09-6985794f0445": 50, // Select
"0cebb8be-46d7-c12a-d306-e9907bfc5a25": 30, // Deluxe
"60bca009-4182-7998-dee7-b8a2558dc369": 19, // Premium
"e046854e-406c-37f4-6607-19a9ba8426fc": 1, // Exclusive
"411e4a55-4e59-7757-41f0-86a53f101bb5": 0, // Ultra
};
break;
case "premium":
tierWeights = {
"12683d76-48d7-84a3-4e09-6985794f0445": 25, // Select
"0cebb8be-46d7-c12a-d306-e9907bfc5a25": 25, // Deluxe
"60bca009-4182-7998-dee7-b8a2558dc369": 40, // Premium
"e046854e-406c-37f4-6607-19a9ba8426fc": 8, // Exclusive
"411e4a55-4e59-7757-41f0-86a53f101bb5": 2, // Ultra
};
break;
case "ultra":
tierWeights = {
"12683d76-48d7-84a3-4e09-6985794f0445": 0, // Select
"0cebb8be-46d7-c12a-d306-e9907bfc5a25": 0, // Deluxe
"60bca009-4182-7998-dee7-b8a2558dc369": 33, // Premium
"e046854e-406c-37f4-6607-19a9ba8426fc": 33, // Exclusive
"411e4a55-4e59-7757-41f0-86a53f101bb5": 33, // Ultra
};
break;
default:
break;
}
try {
const dbSkins = getAllAvailableSkins.all();
const weightedPool = skins
.filter((s) => dbSkins.find((dbSkin) => dbSkin.uuid === s.uuid))
.map((s) => {
const dbSkin = getSkin.get(s.uuid);
return {
...s, // Shallow copy to avoid mutating the imported 'skins' object
tierColor: dbSkin?.tierColor,
weight: tierWeights[s.contentTierUuid] ?? 0,
};
})
.filter((s) => s.weight > 0); // <--- CRITICAL: Remove 0 weight skins
function weightedSample(arr, count) {
let totalWeight = arr.reduce((sum, x) => sum + x.weight, 0);
const list = [...arr];
const result = [];
// 2. Adjust count if the pool is smaller than requested
const actualCount = Math.min(count, list.length);
for (let i = 0; i < actualCount; i++) {
let r = Math.random() * totalWeight;
let running = 0;
let pickIndex = -1;
for (let j = 0; j < list.length; j++) {
running += list[j].weight;
// Changed to strictly less than for safer bounds,
// though filtering weight > 0 above is the primary fix.
if (r <= running) {
pickIndex = j;
break;
}
}
if (pickIndex < 0) pickIndex = list.length - 1;
const picked = list.splice(pickIndex, 1)[0];
result.push(picked);
totalWeight -= picked.weight;
if (totalWeight <= 0) break; // Stop if no more weight exists
}
return result;
}
return weightedSample(weightedPool, 100);
} catch (e) {
console.log(e);
}
}
export function drawCaseSkin(caseContent) {
const randomSelectedSkinIndex = Math.floor(Math.random() * (caseContent.length - 1));
const randomSelectedSkinUuid = caseContent[randomSelectedSkinIndex].uuid;
const dbSkin = getSkin.get(randomSelectedSkinUuid);
const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid);
if (!randomSkinData) {
throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`);
}
// --- Randomize Level and Chroma ---
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
let randomChroma = 1;
if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) {
// Ensure chroma is at least 1 and not greater than the number of chromas
randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1;
}
// --- Calculate Price ---
const calculatePrice = () => {
let result = parseFloat(dbSkin.basePrice);
result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2);
result *= 1 + randomChroma / 4;
return parseFloat(result.toFixed(0));
};
const finalPrice = calculatePrice();
return {
caseContent,
finalPrice,
randomLevel,
randomChroma,
randomSkinData,
randomSelectedSkinUuid,
randomSelectedSkinIndex,
};
}