INDEX / modules /LevelSystem.js
akra35567's picture
Update modules/LevelSystem.js
d37f8ec verified
const fs = require('fs');
const path = require('path');
const ConfigManager = require('./ConfigManager');
class LevelSystem {
constructor(logger = console) {
this.config = ConfigManager.getInstance();
this.logger = logger;
// ═══════════════════════════════════════════════════════════════════
// HF SPACES: Usar /tmp para garantir permissΓ΅es de escrita
// O HF Spaces tem sistema de arquivos somente-leitura em /
// ═══════════════════════════════════════════════════════════════════
// ForΓ§ar uso de /tmp no HF Spaces (sistema read-only)
const basePath = '/tmp/akira_data';
this.dbPath = path.join(basePath, 'data', 'group_levels.json');
this.promoPath = path.join(basePath, 'datauser', 'level_adm_promotion.json');
this._ensureFiles();
this.data = this._load(this.dbPath, []);
this.promos = this._load(this.promoPath, {});
this.windowDays = this.config.LEVEL_WINDOW_DAYS || 3;
this.maxLevel = this.config.LEVEL_MAX || 60;
this.topForAdm = this.config.LEVEL_TOP_FOR_ADM || 3;
}
_ensureFiles() {
try {
if (!fs.existsSync(path.dirname(this.dbPath))) fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
if (!fs.existsSync(path.dirname(this.promoPath))) fs.mkdirSync(path.dirname(this.promoPath), { recursive: true });
if (!fs.existsSync(this.dbPath)) fs.writeFileSync(this.dbPath, JSON.stringify([], null, 2));
if (!fs.existsSync(this.promoPath)) fs.writeFileSync(this.promoPath, JSON.stringify({}, null, 2));
} catch (e) {
this.logger.warn('LevelSystem: erro ao garantir arquivos:', e.message);
// Silenciosamente falha em caso de erro no HF Spaces
}
}
_load(p, fallback) {
try {
const raw = fs.readFileSync(p, 'utf8');
return JSON.parse(raw || '[]');
} catch (e) {
return fallback;
}
}
_save(p, obj) {
try { fs.writeFileSync(p, JSON.stringify(obj, null, 2)); } catch (e) { this.logger.warn('LevelSystem save erro:', e.message); }
}
getGroupRecord(gid, uid, createIfMissing = false) {
const rec = this.data.find(r => r.gid === gid && r.uid === uid);
if (rec) return rec;
if (createIfMissing) {
const n = { gid, uid, level: 0, xp: 0 };
this.data.push(n);
this._save(this.dbPath, this.data);
return n;
}
return { gid, uid, level: 0, xp: 0 };
}
saveRecord(rec) {
const i = this.data.findIndex(r => r.gid === rec.gid && r.uid === rec.uid);
if (i === -1) this.data.push(rec); else this.data[i] = rec;
this._save(this.dbPath, this.data);
}
// FΓ³rmula: cada nΓ­vel requer o dobro do anterior -> base * (2^level)
// Ex.: level 0 -> base, level 1 -> base*2, level 2 -> base*4, ...
requiredXp(level) {
const base = this.config.LEVEL_BASE_XP || 100;
const factor = 2; // crescimento x2 por nΓ­vel
if (level >= this.maxLevel) return Infinity;
return Math.floor(base * Math.pow(factor, level));
}
awardXp(gid, uid, xpAmount = 10) {
const rec = this.getGroupRecord(gid, uid, true);
rec.xp = (rec.xp || 0) + xpAmount;
let leveled = false;
// Permitir mΓΊltiplos level-ups se receber muito XP de uma vez
while (rec.level < this.maxLevel) {
const req = this.requiredXp(rec.level || 0);
if (!isFinite(req)) break;
if (rec.xp >= req) {
rec.xp = rec.xp - req; // mantΓ©m overflow de XP
rec.level = (rec.level || 0) + 1;
leveled = true;
continue; // verifica prΓ³ximo nΓ­vel
}
break;
}
// Se atingiu nΓ­vel mΓ‘ximo, zera XP para evitar overflow
if (rec.level >= this.maxLevel) {
rec.level = this.maxLevel;
rec.xp = 0;
}
this.saveRecord(rec);
return { rec, leveled };
}
// Auto-ADM promotion window logic
registerMaxLevelUser(gid, uid, userName, sock) {
try {
if (!this.promos[gid]) {
this.promos[gid] = {
windowStart: Date.now(),
windowEnd: Date.now() + (this.windowDays * 24 * 60 * 60 * 1000),
maxLevelUsers: [],
promotedToADM: [],
failedUsers: []
};
}
const window = this.promos[gid];
if (Date.now() > window.windowEnd) {
this.promos[gid] = { windowStart: Date.now(), windowEnd: Date.now() + (this.windowDays * 24 * 60 * 60 * 1000), maxLevelUsers: [], promotedToADM: [], failedUsers: [] };
}
if (window.failedUsers.includes(uid)) return { success: false, message: 'VocΓͺ jΓ‘ falhou nesta janela.' };
if (window.promotedToADM.includes(uid)) return { success: false, message: 'JΓ‘ promovido nesta janela.' };
if (!window.maxLevelUsers.find(u => u.uid === uid)) window.maxLevelUsers.push({ uid, userName, timestamp: Date.now(), position: window.maxLevelUsers.length + 1 });
const cfg = this._load(path.join(this.config.DATABASE_FOLDER, 'datauser', 'level_adm_config.json'), {});
const auto = cfg[gid]?.autoADMEnabled === true;
this._save(this.promoPath, this.promos);
if (auto && window.maxLevelUsers.length <= this.topForAdm) {
const position = window.maxLevelUsers.findIndex(u => u.uid === uid) + 1;
if (position <= this.topForAdm) {
try {
window.promotedToADM.push(uid);
this._save(this.promoPath, this.promos);
if (sock && typeof sock.groupUpdateDescription === 'function') {
sock.groupUpdateDescription(gid, `Akira Auto-ADM: ${userName} (NΓ­vel ${this.maxLevel} - Top ${position}/${this.topForAdm})`).catch(()=>{});
}
return { success: true, promoted: true, position, message: `Promovido a ADM (Top ${position})` };
} catch (e) {
return { success: false, message: 'Erro ao promover ADM' };
}
}
}
return { success: true, promoted: false, message: `Max level registrado (${window.maxLevelUsers.length}/${this.topForAdm})` };
} catch (e) {
return { success: false, message: e.message };
}
}
// status info
getStatus(gid) {
const window = this.promos[gid];
if (!window) return { isActive: false };
const daysRemaining = Math.max(0, Math.ceil((window.windowEnd - Date.now()) / (24*60*60*1000)));
return { isActive: true, daysRemaining, maxLevelUsers: window.maxLevelUsers, promotedToADM: window.promotedToADM, failedUsers: window.failedUsers };
}
}
module.exports = LevelSystem;