Como configurar ranking e VIP no site do servidor de MU
Configure ranking de personagens, guilds e o sistema de VIP no site do servidor de MU Online — queries SQL completas, integração com PHP, páginas de ranking com paginação e painel de conta com VIP.
O ranking é o coração do engajamento em servidores de MU Online — jogadores verificam sua posição diariamente. O sistema de VIP é a principal forma de monetização. Ambos são implementados no site PHP integrado com o banco SQL Server.
O ranking é o coração do engajamento em servidores de MU Online — jogadores verificam sua posição diariamente. O sistema de VIP é a principal forma de monetização. Ambos são implementados no site PHP integrado com o banco SQL Server.
Ranking de personagens — query completa
-- Ranking por Resets (mais comum em servidores Season 6+)
SELECT TOP 100
ROW_NUMBER() OVER (ORDER BY c.Resets DESC, c.cLevel DESC) AS Posicao,
c.Name AS Personagem,
c.Class AS ClasseNum,
c.cLevel AS Level,
c.Resets,
c.ConnectStat AS Online,
c.AccountID
FROM Character c
INNER JOIN MEMB_INFO m ON m.memb_id = c.AccountID
WHERE c.CtlCode = 0 -- excluir GMs
AND m.bloc_code = 0 -- excluir contas banidas
ORDER BY c.Resets DESC, c.cLevel DESC;
-- Ranking por Level (para servidores sem reset)
SELECT TOP 100
ROW_NUMBER() OVER (ORDER BY c.cLevel DESC) AS Posicao,
c.Name, c.Class, c.cLevel, c.ConnectStat
FROM Character c
WHERE c.CtlCode = 0
ORDER BY c.cLevel DESC;
-- Ranking de Guilds por Score
SELECT TOP 20
ROW_NUMBER() OVER (ORDER BY g.Score DESC) AS Posicao,
g.Name AS GuildName,
g.Score,
g.Mark,
COUNT(u.Id) AS Membros
FROM Guild g
LEFT JOIN G_UserList u ON u.G_Name = g.Name
WHERE g.Name != ''
AND g.Score > 0
GROUP BY g.Name, g.Score, g.Mark
ORDER BY g.Score DESC;
PHP — página de ranking completa
<?php
// ranking.php
require_once 'config/db-muserver.php';
// Mapeamento de classes
function getClassName(int $classCode): string {
// No MU Online, a classe evolui somando ao base:
// Base 0 = DW, +1 = SM (Soul Master), +2 = GM (Grand Master)
// Base 16 = DK, +1 = BK, +2 = BM
// etc.
$nomes = [
0 => 'Dark Wizard', 1 => 'Soul Master', 2 => 'Grand Master',
16 => 'Dark Knight', 17 => 'Blade Knight', 18 => 'Blade Master',
32 => 'Elf', 33 => 'Muse Elf', 34 => 'High Elf',
48 => 'Magic Gladiator', 49 => 'Duel Master',
64 => 'Dark Lord', 65 => 'Lord Emperor',
80 => 'Summoner', 81 => 'Bloody Summoner', 82 => 'Dimension Master',
96 => 'Rage Fighter', 97 => 'Fist Master',
112 => 'Grow Lancer', 113 => 'Mirage Lancer',
128 => 'Rune Wizard', 129 => 'Rune Spell Master',
144 => 'Slayer', 145 => 'Royal Slayer',
160 => 'Gun Crusher', 161 => 'Gun Breaker',
];
// Tentar código exato primeiro
if (isset($nomes[$classCode])) return $nomes[$classCode];
// Fallback: procurar base
$base = intdiv($classCode, 16) * 16 + ($classCode % 16 <= 2 ? $classCode % 16 : 0);
return $nomes[$base] ?? "Classe $classCode";
}
// Ícones SVG por classe (opcional — pode usar imagens)
function getClassIcon(int $classCode): string {
$baseClass = intdiv($classCode, 16) * 16;
$icons = [
0 => '🔮', // DW/SM/GM
16 => '⚔️', // DK/BK/BM
32 => '🏹', // Elf
48 => '🗡️', // MG
64 => '👑', // DL
80 => '🌀', // Summoner
96 => '👊', // RF
];
return $icons[$baseClass] ?? '⚔️';
}
$conn = getMuDB();
// Paginação
$page = max(1, (int)($_GET['p'] ?? 1));
$perPage = 50;
$offset = ($page - 1) * $perPage;
// Tipo de ranking
$tipo = $_GET['tipo'] ?? 'resets';
$sql = $tipo === 'level' ? "
SELECT TOP $perPage
ROW_NUMBER() OVER (ORDER BY c.cLevel DESC) + $offset AS Pos,
c.Name, c.Class, c.cLevel, c.Resets, c.ConnectStat
FROM Character c
JOIN MEMB_INFO m ON m.memb_id = c.AccountID
WHERE c.CtlCode = 0 AND m.bloc_code = 0
ORDER BY c.cLevel DESC
OFFSET $offset ROWS
" : "
SELECT
ROW_NUMBER() OVER (ORDER BY c.Resets DESC, c.cLevel DESC) AS Pos,
c.Name, c.Class, c.cLevel, c.Resets, c.ConnectStat
FROM Character c
JOIN MEMB_INFO m ON m.memb_id = c.AccountID
WHERE c.CtlCode = 0 AND m.bloc_code = 0
ORDER BY c.Resets DESC, c.cLevel DESC
OFFSET $offset ROWS FETCH NEXT $perPage ROWS ONLY
";
$stmt = sqlsrv_query($conn, $sql);
$jogadores = [];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$row['ClasseNome'] = getClassName((int)$row['Class']);
$row['ClasseIcon'] = getClassIcon((int)$row['Class']);
$jogadores[] = $row;
}
?>
<div class="ranking-page">
<div class="ranking-tabs">
<a href="?tipo=resets" class="<?= $tipo==='resets' ? 'active' : '' ?>">Por Resets</a>
<a href="?tipo=level" class="<?= $tipo==='level' ? 'active' : '' ?>">Por Level</a>
<a href="?tipo=guild" class="<?= $tipo==='guild' ? 'active' : '' ?>">Guilds</a>
</div>
<table class="ranking-table">
<thead>
<tr>
<th>#</th>
<th>Personagem</th>
<th>Classe</th>
<th>Level</th>
<th>Resets</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($jogadores as $j): ?>
<tr class="<?= (int)$j['ConnectStat'] ? 'row-online' : '' ?>">
<td class="rank-pos"><?= $j['Pos'] ?></td>
<td class="rank-name">
<?= $j['ClasseIcon'] ?> <?= htmlspecialchars($j['Name']) ?>
</td>
<td><?= htmlspecialchars($j['ClasseNome']) ?></td>
<td class="rank-level"><?= $j['cLevel'] ?></td>
<td class="rank-resets"><?= $j['Resets'] ?></td>
<td><?= (int)$j['ConnectStat'] ? '<span class="online">Online</span>' : '' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- Paginação -->
<div class="pagination">
<?php if ($page > 1): ?>
<a href="?tipo=<?= $tipo ?>&p=<?= $page-1 ?>">« Anterior</a>
<?php endif; ?>
<?php for ($i = max(1, $page-2); $i <= $page+2; $i++): ?>
<a href="?tipo=<?= $tipo ?>&p=<?= $i ?>"
class="<?= $i === $page ? 'current' : '' ?>"><?= $i ?></a>
<?php endfor; ?>
<?php if (count($jogadores) === $perPage): ?>
<a href="?tipo=<?= $tipo ?>&p=<?= $page+1 ?>">Próxima »</a>
<?php endif; ?>
</div>
</div>
Sistema de VIP no site
Tabela de VIP no banco
-- Verificar se MEMB_VIP existe (Season 8+)
SELECT * FROM MEMB_VIP LIMIT 1;
-- Se não existir, criar:
CREATE TABLE MEMB_VIP (
memb_id VARCHAR(10) NOT NULL PRIMARY KEY,
vip_type TINYINT NOT NULL DEFAULT 1, -- 1=Bronze, 2=Prata, 3=Ouro
start_date DATETIME NOT NULL DEFAULT GETDATE(),
end_date DATETIME NOT NULL,
CONSTRAINT FK_VIP_MEMB FOREIGN KEY (memb_id) REFERENCES MEMB_INFO(memb_id)
);
GO
Benefícios por tipo de VIP no GameServer.ini
; GameServer.ini — configuração de benefícios VIP
[VIP1] ; VIP Bronze
VIP1ExpBonus=50 ; +50% EXP
VIP1DropBonus=20 ; +20% chance de drop
VIP1CommandPoints=5 ; +5 pontos de CMD por level
VIP1MaxSlots=160 ; mais slots no warehouse
[VIP2] ; VIP Prata
VIP2ExpBonus=100 ; +100% EXP (2x)
VIP2DropBonus=30
VIP2CommandPoints=10
VIP2MaxSlots=180
[VIP3] ; VIP Ouro
VIP3ExpBonus=150 ; +150% EXP (2.5x)
VIP3DropBonus=50
VIP3CommandPoints=15
VIP3MaxSlots=200
VIP3RuudBonus=100 ; +100% Ruud (Season 14+)
VipCheckInterval=60 ; verificar VIP a cada 60 segundos
VipExpiryNotify=1 ; avisar jogador quando VIP vai expirar
VipExpiryNotifyHours=24 ; avisar 24h antes
PHP — ativar VIP no site
<?php
// vip.php — ativar VIP para a conta do jogador logado
require_once 'config/db-muserver.php';
session_start();
if (!isset($_SESSION['conta_mu'])) {
header('Location: /login');
exit;
}
$conta = $_SESSION['conta_mu'];
// Tipos de VIP disponíveis
$planos = [
1 => ['nome' => 'VIP Bronze', 'dias' => 30, 'preco' => 'R$ 15,00', 'cor' => '#cd7f32'],
2 => ['nome' => 'VIP Prata', 'dias' => 30, 'preco' => 'R$ 25,00', 'cor' => '#c0c0c0'],
3 => ['nome' => 'VIP Ouro', 'dias' => 30, 'preco' => 'R$ 45,00', 'cor' => '#ffd700'],
];
// Buscar VIP atual
$conn = getMuDB();
$vipStmt = sqlsrv_query($conn,
"SELECT vip_type, start_date, end_date FROM MEMB_VIP WHERE memb_id = ?",
[[$conta, SQLSRV_PARAM_IN]]
);
$vipAtual = sqlsrv_fetch_array($vipStmt, SQLSRV_FETCH_ASSOC);
// Função de ativação
function ativarVIP(string $conta, int $tipo, int $dias): void {
global $conn;
$sql = "
MERGE MEMB_VIP AS target
USING (SELECT ? AS memb_id) AS source ON target.memb_id = source.memb_id
WHEN MATCHED THEN
UPDATE SET
vip_type = ?,
end_date = CASE
WHEN end_date > GETDATE() THEN DATEADD(DAY, ?, end_date)
ELSE DATEADD(DAY, ?, GETDATE())
END
WHEN NOT MATCHED THEN
INSERT (memb_id, vip_type, start_date, end_date)
VALUES (?, ?, GETDATE(), DATEADD(DAY, ?, GETDATE()));
";
sqlsrv_query($conn, $sql, [
[$conta, SQLSRV_PARAM_IN],
[$tipo, SQLSRV_PARAM_IN],
[$dias, SQLSRV_PARAM_IN],
[$dias, SQLSRV_PARAM_IN],
[$conta, SQLSRV_PARAM_IN],
[$tipo, SQLSRV_PARAM_IN],
[$dias, SQLSRV_PARAM_IN],
]);
}
// Processar ativação (após confirmação de pagamento)
// Na prática, isso é chamado pelo webhook do gateway de pagamento
if ($_GET['action'] === 'ativar' && isset($_GET['tipo'])) {
$tipo = (int)$_GET['tipo'];
if (isset($planos[$tipo])) {
ativarVIP($conta, $tipo, $planos[$tipo]['dias']);
$mensagem = "VIP {$planos[$tipo]['nome']} ativado com sucesso!";
}
}
?>
<!DOCTYPE html>
<html>
<body>
<h1>VIP - <?= SERVER_NAME ?></h1>
<?php if ($vipAtual): ?>
<div class="vip-atual">
<h2>Seu VIP Atual</h2>
<p>Tipo: <strong><?= $planos[$vipAtual['vip_type']]['nome'] ?? 'VIP' ?></strong></p>
<p>Expira em: <strong><?= $vipAtual['end_date'] ?></strong></p>
</div>
<?php endif; ?>
<div class="planos">
<?php foreach ($planos as $tipo => $plano): ?>
<div class="plano" style="border-color: <?= $plano['cor'] ?>">
<h3 style="color: <?= $plano['cor'] ?>"><?= $plano['nome'] ?></h3>
<p><strong><?= $plano['preco'] ?></strong> / <?= $plano['dias'] ?> dias</p>
<ul>
<?php if ($tipo >= 1): ?><li>+50% EXP<?= $tipo >= 2 ? ($tipo >= 3 ? '/+150%' : '/+100%') : '' ?></li><?php endif; ?>
<?php if ($tipo >= 1): ?><li>+20% Drop<?= $tipo >= 2 ? ($tipo >= 3 ? '/+50%' : '/+30%') : '' ?></li><?php endif; ?>
<?php if ($tipo >= 3): ?><li>+100% Ruud</li><?php endif; ?>
</ul>
<a href="/pagamento?plano=<?= $tipo ?>" class="btn-comprar">Ativar</a>
</div>
<?php endforeach; ?>
</div>
</body>
</html>
Cache do ranking para performance
<?php
// Para servidores com muitos jogadores, use cache para o ranking
// O ranking não precisa ser atualizado a cada segundo
function getRankingCached(string $tipo, int $page, int $ttl = 300): array {
$cacheFile = sys_get_temp_dir() . "/mu_ranking_{$tipo}_{$page}.cache";
// Verificar cache
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $ttl) {
return unserialize(file_get_contents($cacheFile));
}
// Cache expirado ou inexistente — buscar do banco
$conn = getMuDB();
$offset = ($page - 1) * 50;
$stmt = sqlsrv_query($conn, "
SELECT Name, Class, cLevel, Resets, ConnectStat
FROM Character
WHERE CtlCode = 0
ORDER BY Resets DESC, cLevel DESC
OFFSET ? ROWS FETCH NEXT 50 ROWS ONLY
", [[$offset, SQLSRV_PARAM_IN]]);
$resultado = [];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
$resultado[] = $row;
}
// Salvar cache
file_put_contents($cacheFile, serialize($resultado));
return $resultado;
}
// Usar:
$ranking = getRankingCached('resets', 1, 300); // cache de 5 minutos
Perguntas frequentes
O ranking do site atualiza em tempo real?
O ranking é lido do banco a cada requisição — é sempre atualizado. Para performance, adicione cache de 5-10 minutos nos rankings (file cache ou Redis).
Como saber a classe do personagem pelo número na tabela?
O campo Class na tabela Character é um número inteiro. Cada classe tem um código: 0=Dark Wizard, 16=Dark Knight, 32=Elf, 48=Magic Gladiator, 64=Dark Lord, 80=Summoner, 96=Rage Fighter. Evoluções somam +1 ou +2 ao código base.
VIP ativado no site entra imediamente no jogo?
Sim — a tabela MEMB_VIP é lida pelo GameServer a cada verificação de VIP (configurada por VipCheckInterval). O jogador pode precisar relogar para ver os bônus.
Como criar diferentes tipos de VIP (Bronze, Prata, Ouro)?
Configure os tipos no GameServer.ini (VIP1, VIP2, VIP3) e na tabela MEMB_VIP use o campo vip_type (1=Bronze, 2=Prata, 3=Ouro). Cada tipo tem seus parâmetros de bônus.