Lumi/src/services/command-random.js
2026-07-17 22:10:00 +02:00

117 lines
4.5 KiB
JavaScript

"use strict";
const CONDITION_PATTERN = /^(<=|>=|<|>|==|=)\s*(-?\d+)$/;
function normalizeRandomReplies(value) {
let rows = value;
if (typeof rows === "string") {
try { rows = JSON.parse(rows || "[]"); } catch { rows = []; }
}
if (!Array.isArray(rows)) return [];
return rows.map((row) => ({
text: String(row?.text || "").trim(),
weight: integerValue(row?.weight, 1),
condition: String(row?.condition || "").trim().replace(/\s+/g, "")
})).filter((row) => row.text);
}
function parseRandomRepliesFromBody(body = {}) {
const texts = arrayValue(body.random_text);
const weights = arrayValue(body.random_weight);
const conditions = arrayValue(body.random_condition);
return normalizeRandomReplies(texts.map((text, index) => ({
text,
weight: weights[index] ?? 1,
condition: conditions[index] ?? ""
})));
}
function normalizeRandomConfig({ replies, rngEnabled, rngMin, rngMax } = {}) {
return {
replies: normalizeRandomReplies(replies),
rngEnabled: rngEnabled === true || rngEnabled === 1 || rngEnabled === "on" || rngEnabled === "true",
rngMin: integerValue(rngMin, 1),
rngMax: integerValue(rngMax, 100)
};
}
function validateRandomConfig(input = {}) {
const config = normalizeRandomConfig(input);
const errors = [];
if (!config.replies.length) errors.push("Add at least one random reply.");
if (config.replies.length > 100) errors.push("Random Reply supports at most 100 messages.");
if (!Number.isInteger(config.rngMin) || !Number.isInteger(config.rngMax) || config.rngMin < -1000000 || config.rngMin > 1000000 || config.rngMax < -1000000 || config.rngMax > 1000000) {
errors.push("RNG limits must be whole numbers between -1,000,000 and 1,000,000.");
}
if (config.rngMin > config.rngMax) errors.push("RNG minimum must be less than or equal to the maximum.");
config.replies.forEach((reply, index) => {
if (reply.text.length > 2000) errors.push(`Reply ${index + 1} is longer than 2,000 characters.`);
if (!Number.isInteger(reply.weight) || reply.weight < 1 || reply.weight > 999) {
errors.push(`Reply ${index + 1} weight must be a whole number from 1 to 999.`);
}
if (reply.condition && !CONDITION_PATTERN.test(reply.condition)) {
errors.push(`Reply ${index + 1} condition must look like <20, >=10, or =5.`);
}
if (reply.condition && !config.rngEnabled) {
errors.push(`Turn on RNG before adding a condition to reply ${index + 1}.`);
}
});
return { ok: errors.length === 0, errors, config };
}
function selectRandomReply(input = {}, random = Math.random) {
const validation = validateRandomConfig(input);
if (!validation.ok) throw new Error(validation.errors[0]);
const config = validation.config;
const rng = config.rngEnabled
? config.rngMin + Math.floor(safeRandom(random) * (config.rngMax - config.rngMin + 1))
: null;
const eligible = config.replies.filter((reply) => !reply.condition || conditionMatches(reply.condition, rng));
if (!eligible.length) throw new Error(`No random reply matches RNG value ${rng}.`);
const totalWeight = eligible.reduce((total, reply) => total + reply.weight, 0);
let target = safeRandom(random) * totalWeight;
for (const reply of eligible) {
target -= reply.weight;
if (target < 0) return { reply, rng, eligibleCount: eligible.length };
}
return { reply: eligible[eligible.length - 1], rng, eligibleCount: eligible.length };
}
function conditionMatches(condition, value) {
const match = String(condition || "").match(CONDITION_PATTERN);
if (!match || !Number.isFinite(value)) return false;
const expected = Number(match[2]);
if (match[1] === "<") return value < expected;
if (match[1] === "<=") return value <= expected;
if (match[1] === ">") return value > expected;
if (match[1] === ">=") return value >= expected;
return value === expected;
}
function safeRandom(random) {
const value = Number(typeof random === "function" ? random() : Math.random());
if (!Number.isFinite(value)) return 0;
return Math.max(0, Math.min(value, 0.999999999999));
}
function arrayValue(value) {
if (Array.isArray(value)) return value;
if (value === undefined || value === null) return [];
return [value];
}
function integerValue(value, fallback) {
if (value === undefined || value === null || value === "") return fallback;
const number = Number(value);
return Number.isInteger(number) ? number : Number.NaN;
}
module.exports = {
conditionMatches,
normalizeRandomConfig,
normalizeRandomReplies,
parseRandomRepliesFromBody,
selectRandomReply,
validateRandomConfig
};