db = $database->dblink; } /** * Functiile sunt pornite? Verificam si flagul, si existenta tabelei, ca sa * nu cada jocul pe un server care nu a rulat inca migrarea SQL. */ public static function enabled() { static $ok = null; if ($ok !== null) { return $ok; } if (!defined('NEW_FUNCTIONS_ALLIANCE_BONUSES') || !NEW_FUNCTIONS_ALLIANCE_BONUSES) { return $ok = false; } global $database; $res = @mysqli_query($database->dblink, "SHOW TABLES LIKE '" . TB_PREFIX . "alliance_bonus'"); return $ok = ($res && mysqli_num_rows($res) > 0); } /** Lista tipurilor, cu cheia de limba si iconita. */ public static function types() { return array( self::RECRUITMENT => array('key' => 'recruitment', 'lang' => 'ALLYBONUS_RECRUITMENT'), self::PHILOSOPHY => array('key' => 'philosophy', 'lang' => 'ALLYBONUS_PHILOSOPHY'), self::METALLURGY => array('key' => 'metallurgy', 'lang' => 'ALLYBONUS_METALLURGY'), self::COMMERCE => array('key' => 'commerce', 'lang' => 'ALLYBONUS_COMMERCE'), ); } /* ------------------------------------------------------------------ */ /* Tabele de valori (din config, cu rezerva pe valorile din T4) */ /* ------------------------------------------------------------------ */ private static function listFromConst($name, $fallback) { $raw = defined($name) ? (string) constant($name) : $fallback; $out = array(); foreach (explode(',', $raw) as $v) { $v = trim($v); if ($v !== '' && is_numeric($v)) { $out[] = (float) $v; } } return $out ? $out : array_map('floatval', explode(',', $fallback)); } /** Resurse necesare pentru a trece de la $level la $level+1. */ public static function costFor($level) { $costs = self::listFromConst('ALLIANCE_BONUS_COSTS', '1200000,5600000,17100000,51200000,153600000'); return isset($costs[$level]) ? (float) $costs[$level] : 0.0; } /** Durata upgrade-ului, in secunde, scalata cu viteza serverului. */ public static function upgradeSeconds($level) { $hours = self::listFromConst('ALLIANCE_BONUS_HOURS', '24,48,72,96,120'); $h = isset($hours[$level]) ? (float) $hours[$level] : 24.0; $speed = (defined('SPEED') && SPEED > 0) ? (float) SPEED : 1.0; return (int) max(60, round($h * 3600 / $speed)); } /** * Plafonul zilnic de donatie al unui jucator, dupa cel mai mare nivel * deblocat in alianta. Se scaleaza cu viteza serverului, ca in T4. */ public static function dailyLimit($highestLevel) { $limits = self::listFromConst('ALLIANCE_BONUS_DAILY', '300000,300000,400000,550000,750000,1000000'); $idx = max(0, min(count($limits) - 1, (int) $highestLevel)); $val = (float) $limits[$idx]; $speed = (defined('SPEED') && SPEED > 0) ? (float) SPEED : 1.0; return (float) round($val * $speed); } /** Procentul acordat de un nivel, pentru un tip de bonus. */ public static function percentFor($btype, $level) { $level = max(0, min(self::MAX_LEVEL, (int) $level)); if ($level === 0) { return 0.0; } $small = defined('ALLIANCE_BONUS_PCT_SMALL') ? (float) ALLIANCE_BONUS_PCT_SMALL : 2.0; $large = defined('ALLIANCE_BONUS_PCT_LARGE') ? (float) ALLIANCE_BONUS_PCT_LARGE : 4.0; // Recruitment si Philosophy cresc cu 2% pe nivel, Metallurgy si // Commerce cu 4% pe nivel (vezi tabelul din T4). $step = in_array((int) $btype, array(self::METALLURGY, self::COMMERCE), true) ? $large : $small; return $level * $step; } /* ------------------------------------------------------------------ */ /* Citire */ /* ------------------------------------------------------------------ */ /** * Starea completa a bonusurilor unei aliante: nivel, progres, upgrade activ. * Randurile lipsa se completeaza cu nivel 0, deci apelantul primeste mereu * toate cele patru tipuri. */ public function getState($aid) { $aid = (int) $aid; $out = array(); foreach (array_keys(self::types()) as $btype) { $out[$btype] = array( 'btype' => $btype, 'level' => 0, 'pool' => 0.0, 'upgrade_end' => 0, ); } if ($aid <= 0 || !self::enabled()) { return $out; } $res = mysqli_query($this->db, "SELECT btype, level, pool, upgrade_end FROM " . TB_PREFIX . "alliance_bonus WHERE aid = " . $aid); while ($res && ($row = mysqli_fetch_assoc($res))) { $b = (int) $row['btype']; if (isset($out[$b])) { $out[$b] = array( 'btype' => $b, 'level' => (int) $row['level'], 'pool' => (float) $row['pool'], 'upgrade_end' => (int) $row['upgrade_end'], ); } } return $out; } /** * Nivelurile efective ale unei aliante, ca array btype => nivel. * Cache-uit pe cerere: se citeste des (lupte, instruire, productie). */ public function getLevels($aid) { $aid = (int) $aid; if (isset(self::$levelCache[$aid])) { return self::$levelCache[$aid]; } $levels = array(); foreach (array_keys(self::types()) as $btype) { $levels[$btype] = 0; } if ($aid > 0 && self::enabled()) { $res = mysqli_query($this->db, "SELECT btype, level FROM " . TB_PREFIX . "alliance_bonus WHERE aid = " . $aid); while ($res && ($row = mysqli_fetch_assoc($res))) { $b = (int) $row['btype']; if (isset($levels[$b])) { $levels[$b] = (int) $row['level']; } } } return self::$levelCache[$aid] = $levels; } /** Cel mai mare nivel deblocat in alianta (pentru plafonul de donatie). */ public function highestLevel($aid) { $levels = $this->getLevels($aid); return $levels ? max($levels) : 0; } /** * Nivelul de care beneficiaza EFECTIV un jucator. * * Membrii noi capata acces treptat: nivelul 1 imediat, nivelul 2 dupa 24h, * nivelul 3 dupa 48h si asa mai departe (scalat cu viteza serverului). * Asa nu se poate sari dintr-o alianta in alta doar ca sa culegi bonusuri. */ public function effectiveLevel($uid, $btype) { if (!self::enabled()) { return 0; } $uid = (int) $uid; $res = mysqli_query($this->db, "SELECT alliance, alliance_joined FROM " . TB_PREFIX . "users WHERE id = " . $uid . " LIMIT 1"); $row = $res ? mysqli_fetch_assoc($res) : null; if (!$row || (int) $row['alliance'] <= 0) { return 0; } $levels = $this->getLevels((int) $row['alliance']); $level = isset($levels[(int) $btype]) ? (int) $levels[(int) $btype] : 0; if ($level <= 1) { return $level; // nivelul 1 e disponibil imediat } $joined = (int) $row['alliance_joined']; // 0 sau 1 inseamna membru dinainte de aceasta functie: primeste tot if ($joined <= 1) { return $level; } $speed = (defined('SPEED') && SPEED > 0) ? (float) SPEED : 1.0; $elapsed = time() - $joined; // nivelul N devine disponibil dupa (N-1) * 24h de la intrare $allowed = 1 + (int) floor($elapsed / max(60, (86400 / $speed))); return max(0, min($level, $allowed)); } /* ------------------------------------------------------------------ */ /* Donatii */ /* ------------------------------------------------------------------ */ /** Cat a donat un jucator azi. */ public function donatedToday($uid) { if (!self::enabled()) { return 0.0; } $uid = (int) $uid; $day = (int) floor(time() / 86400); $res = mysqli_query($this->db, "SELECT amount FROM " . TB_PREFIX . "alliance_donation WHERE uid = " . $uid . " AND day = " . $day . " LIMIT 1"); $row = $res ? mysqli_fetch_assoc($res) : null; return $row ? (float) $row['amount'] : 0.0; } /** * Doneaza resurse catre un bonus. * * $amounts - array cu patru valori: lemn, lut, fier, cereale * $triple - daca jucatorul plateste aur ca donatia sa conteze intreit * * Tipul resursei nu conteaza, doar totalul; resursele se scad din satul * activ. Intoarce una dintre constantele DONATE_*. */ public function donate($uid, $wref, $btype, array $amounts, $triple = false) { global $database, $session; if (!self::enabled()) { return self::DONATE_DISABLED; } $uid = (int) $uid; $wref = (int) $wref; $btype = (int) $btype; if (!isset(self::types()[$btype])) { return self::DONATE_INVALID; } // sumele trebuie sa fie intregi pozitivi $clean = array(); $total = 0.0; foreach (array_slice($amounts, 0, 4) as $a) { $a = (float) $a; if ($a < 0 || !is_finite($a)) { return self::DONATE_INVALID; } $a = floor($a); $clean[] = $a; $total += $a; } while (count($clean) < 4) { $clean[] = 0.0; } if ($total <= 0) { return self::DONATE_INVALID; } // alianta jucatorului $res = mysqli_query($this->db, "SELECT alliance FROM " . TB_PREFIX . "users WHERE id = " . $uid . " LIMIT 1"); $row = $res ? mysqli_fetch_assoc($res) : null; $aid = $row ? (int) $row['alliance'] : 0; if ($aid <= 0) { return self::DONATE_NO_ALLIANCE; } // bonusul nu trebuie sa fie in curs de upgrade $state = $this->getState($aid); if ($state[$btype]['upgrade_end'] > time()) { return self::DONATE_UPGRADING; } if ($state[$btype]['level'] >= self::MAX_LEVEL) { return self::DONATE_INVALID; } // valoarea contorizata (triplata sau nu) si plafonul zilnic $counted = $triple ? $total * 3 : $total; $limit = self::dailyLimit($this->highestLevel($aid)); $already = $this->donatedToday($uid); if ($already + $counted > $limit) { return self::DONATE_LIMIT; } // aurul pentru triplare if ($triple) { $cost = defined('ALLIANCE_BONUS_TRIPLE_GOLD') ? (int) ALLIANCE_BONUS_TRIPLE_GOLD : 3; if ((int) $session->gold < $cost) { return self::DONATE_NO_GOLD; } } // Resursele trebuie sa existe in sat. // FIX: getResource() nu exista in clasa de baza de date; randul satului // (cu wood/clay/iron/crop) se citeste cu getVillage(), fara cache, ca sa // vedem valorile de dupa ultima productie, nu unele vechi de cateva zeci // de secunde. $vres = $database->getVillage($wref, 0, false); if (!$vres) { return self::DONATE_RESOURCES; } $fields = array('wood', 'clay', 'iron', 'crop'); foreach ($fields as $i => $f) { if ((float) $vres[$f] < $clean[$i]) { return self::DONATE_RESOURCES; } } /* ---- de aici incolo modificam starea; totul intr-o tranzactie ---- */ mysqli_query($this->db, "START TRANSACTION"); // ATENTIE LA SEMN: modifyResource cu modul 0 SCADE deja // ("wood = wood - $wood"). Trimiterea de valori negative ar fi // ADAUGAT resurse - aceeasi capcana ca la aurul de la demolare. $ok = $database->modifyResource($wref, $clean[0], $clean[1], $clean[2], $clean[3], 0); if (!$ok) { mysqli_query($this->db, "ROLLBACK"); return self::DONATE_RESOURCES; } // randul de bonus poate lipsi: il cream la prima donatie mysqli_query($this->db, "INSERT INTO " . TB_PREFIX . "alliance_bonus (aid, btype, level, pool, upgrade_end) VALUES (" . $aid . ", " . $btype . ", 0, 0, 0) ON DUPLICATE KEY UPDATE aid = aid"); // adaugam in fond DOAR daca bonusul tot nu e in upgrade (protejeaza // impotriva a doua donatii simultane care ar sari peste verificare) $res = mysqli_query($this->db, "UPDATE " . TB_PREFIX . "alliance_bonus SET pool = pool + " . $counted . " WHERE aid = " . $aid . " AND btype = " . $btype . " AND upgrade_end <= " . time() . " AND level < " . self::MAX_LEVEL . " LIMIT 1"); if (!$res || mysqli_affected_rows($this->db) === 0) { mysqli_query($this->db, "ROLLBACK"); return self::DONATE_UPGRADING; } $day = (int) floor(time() / 86400); mysqli_query($this->db, "INSERT INTO " . TB_PREFIX . "alliance_donation (uid, day, amount) VALUES (" . $uid . ", " . $day . ", " . $counted . ") ON DUPLICATE KEY UPDATE amount = amount + " . $counted); mysqli_query($this->db, "INSERT INTO " . TB_PREFIX . "alliance_donation_log (aid, uid, btype, amount, time) VALUES (" . $aid . ", " . $uid . ", " . $btype . ", " . $counted . ", " . time() . ")"); if ($triple) { $cost = defined('ALLIANCE_BONUS_TRIPLE_GOLD') ? (int) ALLIANCE_BONUS_TRIPLE_GOLD : 3; $database->modifyGold($uid, $cost, 0); $session->gold -= $cost; } mysqli_query($this->db, "COMMIT"); // daca fondul a atins pragul, pornim upgrade-ul $this->startUpgradeIfReady($aid, $btype); unset(self::$levelCache[$aid]); return self::DONATE_OK; } /** * Porneste numaratoarea daca fondul acopera costul nivelului urmator. * Costul se scade din fond, ca surplusul sa se reporteze la nivelul urmator. */ public function startUpgradeIfReady($aid, $btype) { $aid = (int) $aid; $btype = (int) $btype; $res = mysqli_query($this->db, "SELECT level, pool, upgrade_end FROM " . TB_PREFIX . "alliance_bonus WHERE aid = " . $aid . " AND btype = " . $btype . " LIMIT 1"); $row = $res ? mysqli_fetch_assoc($res) : null; if (!$row || (int) $row['upgrade_end'] > time() || (int) $row['level'] >= self::MAX_LEVEL) { return false; } $level = (int) $row['level']; $cost = self::costFor($level); if ($cost <= 0 || (float) $row['pool'] < $cost) { return false; } $end = time() + self::upgradeSeconds($level); mysqli_query($this->db, "UPDATE " . TB_PREFIX . "alliance_bonus SET pool = pool - " . $cost . ", upgrade_end = " . $end . " WHERE aid = " . $aid . " AND btype = " . $btype . " AND upgrade_end <= " . time() . " LIMIT 1"); return true; } /** * Finalizeaza upgrade-urile expirate. Se apeleaza din Automation. * Dupa crestere, verificam din nou fondul: daca surplusul acopera si * nivelul urmator, upgrade-ul continua fara alta donatie. */ public function processUpgrades() { if (!self::enabled()) { return 0; } $now = time(); $done = 0; $res = mysqli_query($this->db, "SELECT aid, btype FROM " . TB_PREFIX . "alliance_bonus WHERE upgrade_end > 0 AND upgrade_end <= " . $now); $rows = array(); while ($res && ($row = mysqli_fetch_assoc($res))) { $rows[] = array((int) $row['aid'], (int) $row['btype']); } foreach ($rows as $r) { list($aid, $btype) = $r; $ok = mysqli_query($this->db, "UPDATE " . TB_PREFIX . "alliance_bonus SET level = level + 1, upgrade_end = 0 WHERE aid = " . $aid . " AND btype = " . $btype . " AND upgrade_end > 0 AND upgrade_end <= " . $now . " AND level < " . self::MAX_LEVEL . " LIMIT 1"); if ($ok && mysqli_affected_rows($this->db) > 0) { $done++; unset(self::$levelCache[$aid]); $this->startUpgradeIfReady($aid, $btype); } } return $done; } /** Contributia fiecarui membru, pentru afisare. */ public function contributions($aid, $limit = 30) { if (!self::enabled()) { return array(); } $aid = (int) $aid; $limit = max(1, min(100, (int) $limit)); $out = array(); $res = mysqli_query($this->db, "SELECT d.uid, u.username, SUM(d.amount) AS total FROM " . TB_PREFIX . "alliance_donation_log d LEFT JOIN " . TB_PREFIX . "users u ON u.id = d.uid WHERE d.aid = " . $aid . " GROUP BY d.uid, u.username ORDER BY total DESC LIMIT " . $limit); while ($res && ($row = mysqli_fetch_assoc($res))) { $out[] = array( 'uid' => (int) $row['uid'], 'username' => (string) $row['username'], 'total' => (float) $row['total'], ); } return $out; } /* ------------------------------------------------------------------ */ /* Ajutor pentru hook-urile din joc */ /* ------------------------------------------------------------------ */ /** * Multiplicatorul unui bonus pentru un jucator, ca numar (1.0 = fara bonus). * Folosit de Technology, Battle, Market si Building. * * Rezultatul e cache-uit pe cerere: se cere de multe ori intr-o singura * pagina (fiecare unitate din lista de instruire, fiecare val din lupta). */ public static function multiplier($uid, $btype) { static $cache = array(); $uid = (int) $uid; $btype = (int) $btype; $key = $uid . ':' . $btype; if (isset($cache[$key])) { return $cache[$key]; } if (!self::enabled() || $uid <= 0) { return $cache[$key] = 1.0; } $engine = new self(); $level = $engine->effectiveLevel($uid, $btype); $pct = self::percentFor($btype, $level); return $cache[$key] = 1.0 + ($pct / 100.0); } }