mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-01-18 16:37:40 +01:00
Merge pull request #59 from cassoule/cases-balance
cases prices and content balance
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
129
src/utils/caseOpening.js
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user