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,
|
getAllUsers,
|
||||||
getUser,
|
getUser,
|
||||||
hardUpdateSkin,
|
hardUpdateSkin,
|
||||||
|
insertLog,
|
||||||
updateManyUsers,
|
updateManyUsers,
|
||||||
|
updateSkin,
|
||||||
updateUserAvatar,
|
updateUserAvatar,
|
||||||
|
updateUserCoins,
|
||||||
} from "../../database/index.js";
|
} from "../../database/index.js";
|
||||||
import { client } from "../client.js";
|
import { client } from "../client.js";
|
||||||
|
import { drawCaseContent, drawCaseSkin } from "../../utils/caseOpening.js";
|
||||||
|
|
||||||
// Constants for the AI rate limiter
|
// Constants for the AI rate limiter
|
||||||
const MAX_REQUESTS_PER_INTERVAL = parseInt(process.env.MAX_REQUESTS || "5");
|
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.");
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { sleep } from "openai/core";
|
|||||||
// --- Database Imports ---
|
// --- Database Imports ---
|
||||||
import {
|
import {
|
||||||
getAllAkhys,
|
getAllAkhys,
|
||||||
getAllAvailableSkins,
|
|
||||||
getAllUsers,
|
getAllUsers,
|
||||||
getLogs,
|
getLogs,
|
||||||
getMarketOffersBySkin,
|
getMarketOffersBySkin,
|
||||||
@@ -35,6 +34,7 @@ import { DiscordRequest } from "../../api/discord.js";
|
|||||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
|
||||||
import { emitDataUpdated, socketEmit } from "../socket.js";
|
import { emitDataUpdated, socketEmit } from "../socket.js";
|
||||||
import { handleCaseOpening } from "../../utils/marketNotifs.js";
|
import { handleCaseOpening } from "../../utils/marketNotifs.js";
|
||||||
|
import { drawCaseContent, drawCaseSkin } from "../../utils/caseOpening.js";
|
||||||
|
|
||||||
// Create a new router instance
|
// Create a new router instance
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -120,114 +120,29 @@ export function apiRoutes(client, io) {
|
|||||||
router.post("/open-case", async (req, res) => {
|
router.post("/open-case", async (req, res) => {
|
||||||
const { userId, caseType } = req.body;
|
const { userId, caseType } = req.body;
|
||||||
|
|
||||||
let caseTypeVal, tierWeights;
|
let caseTypeVal;
|
||||||
switch (caseType) {
|
switch (caseType) {
|
||||||
case "standard":
|
case "standard":
|
||||||
caseTypeVal = 1;
|
caseTypeVal = 500;
|
||||||
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
|
|
||||||
};
|
|
||||||
break;
|
break;
|
||||||
case "premium":
|
case "premium":
|
||||||
caseTypeVal = 2;
|
caseTypeVal = 750;
|
||||||
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
|
|
||||||
};
|
|
||||||
break;
|
break;
|
||||||
case "ultra":
|
case "ultra":
|
||||||
caseTypeVal = 4;
|
caseTypeVal = 1000;
|
||||||
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
|
|
||||||
};
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return res.status(400).json({ error: "Invalid case type." });
|
return res.status(400).json({ error: "Invalid case type." });
|
||||||
}
|
}
|
||||||
const commandUser = getUser.get(userId);
|
const commandUser = getUser.get(userId);
|
||||||
if (!commandUser) return res.status(404).json({ error: "User not found." });
|
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." });
|
if (commandUser.coins < valoPrice) return res.status(403).json({ error: "Not enough FlopoCoins." });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dbSkins = getAllAvailableSkins.all();
|
const selectedSkins = await drawCaseContent(caseType);
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
function weightedSample(arr, count) {
|
const result = drawCaseSkin(selectedSkins);
|
||||||
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();
|
|
||||||
|
|
||||||
// --- Update Database ---
|
// --- Update Database ---
|
||||||
insertLog.run({
|
insertLog.run({
|
||||||
@@ -243,19 +158,24 @@ export function apiRoutes(client, io) {
|
|||||||
coins: commandUser.coins - valoPrice,
|
coins: commandUser.coins - valoPrice,
|
||||||
});
|
});
|
||||||
updateSkin.run({
|
updateSkin.run({
|
||||||
uuid: randomSkinData.uuid,
|
uuid: result.randomSkinData.uuid,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
currentLvl: randomLevel,
|
currentLvl: result.randomLevel,
|
||||||
currentChroma: randomChroma,
|
currentChroma: result.randomChroma,
|
||||||
currentPrice: finalPrice,
|
currentPrice: result.finalPrice,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
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);
|
const updatedSkin = getSkin.get(result.randomSkinData.uuid);
|
||||||
await handleCaseOpening(caseType, userId, randomSelectedSkinUuid, client);
|
await handleCaseOpening(caseType, userId, result.randomSelectedSkinUuid, client);
|
||||||
res.json({ selectedSkins, randomSelectedSkinUuid, randomSelectedSkinIndex, updatedSkin });
|
res.json({
|
||||||
|
selectedSkins,
|
||||||
|
randomSelectedSkinUuid: result.randomSelectedSkinUuid,
|
||||||
|
randomSelectedSkinIndex: result.randomSelectedSkinIndex,
|
||||||
|
updatedSkin,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching skins:", error);
|
console.error("Error fetching skins:", error);
|
||||||
res.status(500).json({ error: "Failed to fetch skins." });
|
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