Файл: system/automation.php
Строк: 463
<?php
/**
* Единый игровой календарь.
* Все периодические циклы запускаются из одного фонового обработчика и защищены
* от повторной выдачи наград/повторного завершения одним и тем же period_key.
*/
declare(strict_types=1);
function game_automation_bootstrap(): void {
static $done=false;
if($done) return;
$done=true;
$probe=db_query("SHOW TABLES LIKE 'automation_log'");
if($probe && db_num_rows($probe)>0) return;
db_query("CREATE TABLE IF NOT EXISTS `automation_log` (
`code` varchar(64) NOT NULL,
`period_key` varchar(64) NOT NULL,
`created_at` int NOT NULL,
PRIMARY KEY (`code`,`period_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
}
function game_automation_once(string $code, string $periodKey, callable $callback): bool {
game_automation_bootstrap();
$lockName='game:'.substr(preg_replace('/[^a-z0-9:_-]/i','_', $code.':'.$periodKey) ?: 'job',0,60);
$got=(int)(db_fetch_one('SELECT GET_LOCK(:n,0) AS l',['n'=>$lockName])['l'] ?? 0);
if($got!==1) return false;
$pdo=db_pdo();
try {
$pdo->beginTransaction();
$exists=db_fetch_one('SELECT 1 FROM `automation_log` WHERE `code`=:c AND `period_key`=:p LIMIT 1 FOR UPDATE',['c'=>$code,'p'=>$periodKey]);
if($exists){ $pdo->commit(); return false; }
$callback();
db_execute('INSERT INTO `automation_log` (`code`,`period_key`,`created_at`) VALUES (:c,:p,:t)',['c'=>$code,'p'=>$periodKey,'t'=>time()]);
$pdo->commit();
return true;
} catch(Throwable $e) {
if($pdo->inTransaction()) $pdo->rollBack();
throw $e;
} finally {
try { db_fetch_one('SELECT RELEASE_LOCK(:n) AS r',['n'=>$lockName]); } catch(Throwable) {}
}
}
function game_automation_table_exists(string $table): bool {
$row=db_fetch_one('SELECT COUNT(*) AS c FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=:t',['t'=>$table]);
return (int)($row['c'] ?? 0)>0;
}
function game_automation_dragon_revive(): void {
if(!game_automation_table_exists('aluko')) return;
$row=db_fetch_one('SELECT `id`,`health` FROM `aluko` ORDER BY `id` LIMIT 1');
if(!$row || (int)$row['health']>0) return;
$hp=random_int(1500000,3000000);
db_execute('UPDATE `aluko` SET `health`=:h,`max_health`=:h2 WHERE `id`=:i',['h'=>$hp,'h2'=>$hp,'i'=>(int)$row['id']]);
}
function game_automation_league_daily(): void {
if(!game_automation_table_exists('users')) return;
$rows=db_prepare('SELECT `id`,`league_place` FROM `users` WHERE `league_place`<1000')->fetchAll();
foreach($rows as $row){
$place=(int)$row['league_place'];
if($place>850) $next=1000;
else $next=min(1000,$place+random_int(200,240));
db_execute('UPDATE `users` SET `league_place`=:p WHERE `id`=:u',['p'=>$next,'u'=>(int)$row['id']]);
}
db_execute('UPDATE `users` SET `league_fights`=25');
}
function game_automation_league_reseed(): void {
if(!game_automation_table_exists('users')) return;
db_execute("UPDATE `users` SET `league_place`=1000,`league_fights`=25,`league`=CASE
WHEN `level`<=20 THEN 1
WHEN `level`<=30 THEN 2
WHEN `level`<=50 THEN 3
WHEN `level`<=80 THEN 4
WHEN `level`<=100 THEN 5
ELSE 6 END");
}
function game_automation_chests_daily(): void {
if(game_automation_table_exists('sunduk')) db_query('DELETE FROM `sunduk`');
}
function game_automation_runtime_cleanup(): void {
$root=dirname(__DIR__);
$logs=$root.'/logs';
foreach(['app-error-*.log','php-error-*.log'] as $pattern){
$files=(array)glob($logs.'/'.$pattern);
usort($files,static fn($a,$b)=>(filemtime($b)?:0)<=>(filemtime($a)?:0));
foreach(array_slice($files,2) as $file) @unlink($file);
}
$tmp=sys_get_temp_dir().'/game_rate';
$now=time();
foreach((array)glob($tmp.'/*.json') as $file){
if($now-(filemtime($file)?:$now)>172800) @unlink($file);
}
}
function game_automation_clan_dungeon(bool $open): void {
if(!game_automation_table_exists('clans')) return;
if($open){
db_execute('UPDATE `clans` SET `clandungeon`=1');
return;
}
db_execute('UPDATE `clans` SET `clandungeon`=0');
if(game_automation_table_exists('clan_dungeon')) db_query('DELETE FROM `clan_dungeon`');
if(game_automation_table_exists('clan_dungeon_user')) db_query('DELETE FROM `clan_dungeon_user`');
if(game_automation_table_exists('users')) db_execute('UPDATE `users` SET `fight_duengeon_time`=0');
}
function game_automation_clan_battle(bool $open): void {
if(!game_automation_table_exists('clans')) return;
if(game_automation_table_exists('clan_battle')) db_query('DELETE FROM `clan_battle`');
if(game_automation_table_exists('clan_battle_log')) db_query('DELETE FROM `clan_battle_log`');
if($open) db_execute('UPDATE `clans` SET `battle`=0,`battle_cool`=1,`global_battle`=:t',['t'=>time()+172800]);
else db_execute('UPDATE `clans` SET `battle`=0,`battle_cool`=1,`global_battle`=0');
}
function game_automation_mail(int $uid,string $text): void {
if($uid<=0 || !game_automation_table_exists('mail')) return;
db_execute('INSERT INTO `mail` (`from`,`to`,`text`,`time`,`read`) VALUES (0,:u,:x,:t,0)',['u'=>$uid,'x'=>$text,'t'=>time()]);
}
function game_automation_finalize_coliseum(string $weekKey): void {
if(!game_automation_table_exists('users')) return;
$rows=db_prepare('SELECT `id`,`login`,`r`,`skull` FROM `users` WHERE `skull`>0 ORDER BY `skull` DESC,`id` ASC LIMIT 10')->fetchAll();
$news="[center][b][color=#e8c66a]Турнир Колизея завершён[/color][/b][/center]nn";
if(!$rows){
$news.="[center]На этой неделе призовые места остались свободны.[/center]n";
} else {
$place=0;
foreach($rows as $row){
$place++; $skulls=max(0,(int)$row['skull']); $rewardSkulls=min(1000,$skulls);
$gold=$skulls>=250?1000:($skulls>=125?750:500);
$silver=min(2000000000,72*$rewardSkulls); $exp=min(9000000000000000000,108*$rewardSkulls);
db_execute('UPDATE `users` SET `g`=`g`+:g,`s`=LEAST(2000000000,`s`+:s),`exp`=LEAST(9000000000000000000,`exp`+:e) WHERE `id`=:u',[
'g'=>$gold,'s'=>$silver,'e'=>$exp,'u'=>(int)$row['id']
]);
game_automation_mail((int)$row['id'],'[b]Турнир Колизея завершён.[/b]nВаш результат: '.$skulls.' черепов.nНаграда: '.$gold.' золота, '.$silver.' серебра и '.$exp.' опыта.');
$news.=$place.'. [url=/user/'.(int)$row['id'].'/]'.(string)$row['login'].'[/url] — [b]'.$skulls.'[/b] черепов, награда [color=#ffd66b]'.$gold.' золота[/color]n';
}
}
if(function_exists('game_forum_admin_news')) game_forum_admin_news('Итоги турнира Колизея',$news."n[center][url=/coliseum/]Вернуться в Колизей[/url][/center]");
db_execute('UPDATE `users` SET `rang`=25,`rang_star`=0,`skull`=0');
}
function game_automation_finalize_valor(string $periodKey): void {
if(!game_automation_table_exists('users') || !game_automation_table_exists('clans')) return;
// Сезонные знаки доблести получают все герои, достигшие порогов сезона.
db_execute("UPDATE `users` SET `med`=CASE
WHEN `lvl_dobl`>=75 THEN 4
WHEN `lvl_dobl`>=50 THEN 3
WHEN `lvl_dobl`>=25 THEN 2
WHEN `lvl_dobl`>=10 THEN 1
ELSE 0 END");
db_execute('UPDATE `clans` SET `g`=`g`+1000,`exp`=`exp`+5000000 WHERE `dobl`>=250');
$news="[center][b][color=#e8c66a]Сезон доблести завершён[/color][/b][/center]nn";
$news.="[b]Лучшие кланы[/b]n";
$clans=db_prepare('SELECT `id`,`name`,`dobl` FROM `clans` WHERE `dobl`>0 ORDER BY `dobl` DESC,`id` ASC LIMIT 10')->fetchAll();
$clanGold=[1=>5000,2=>4500,3=>4000,4=>3500,5=>3250,6=>3000,7=>2750,8=>2500,9=>2250,10=>2000];
$clanExp=[1=>50000000,2=>45000000,3=>40000000,4=>35000000,5=>32500000,6=>30000000,7=>27500000,8=>25000000,9=>22500000,10=>20000000];
$pos=0; foreach($clans as $clan){$pos++;$g=$clanGold[$pos];$e=$clanExp[$pos];db_execute('UPDATE `clans` SET `g`=`g`+:g,`exp`=`exp`+:e WHERE `id`=:c',['g'=>$g,'e'=>$e,'c'=>(int)$clan['id']]);$news.=$pos.'. [url=/clan/'.(int)$clan['id'].'/]'.(string)$clan['name'].'[/url] — [b]'.(int)$clan['dobl'].'[/b] доблестиn';}
if(!$clans) $news.="Нет участников.n";
$news.="n[b]Лучшие герои[/b]n";
$users=db_prepare('SELECT `id`,`login`,`lvl_dobl` FROM `users` WHERE `lvl_dobl`>0 ORDER BY `lvl_dobl` DESC,`id` ASC LIMIT 10')->fetchAll();
$medals=[1=>5,2=>5,3=>5,4=>5,5=>5,6=>4,7=>4,8=>4,9=>4,10=>4];
$gold=[1=>3000,2=>2500,3=>2000,4=>1500,5=>1000,6=>0,7=>0,8=>0,9=>0,10=>0];
$pos=0; foreach($users as $hero){$pos++;$m=$medals[$pos];$g=$gold[$pos];db_execute('UPDATE `users` SET `med`=:m,`g`=`g`+:g WHERE `id`=:u',['m'=>$m,'g'=>$g,'u'=>(int)$hero['id']]);$news.=$pos.'. [url=/user/'.(int)$hero['id'].'/]'.(string)$hero['login'].'[/url] — [b]'.(int)$hero['lvl_dobl'].'[/b] доблести'.($g>0?' · [color=#ffd66b]'.$g.' золота[/color]':'').'n';}
if(!$users) $news.="Нет участников.n";
if(function_exists('game_forum_admin_news')) game_forum_admin_news('Итоги сезона доблести',$news."n[center]Новый сезон уже начался. Слава ждёт сильнейших![/center]");
db_execute('UPDATE `users` SET `alex_dobl`=0,`lvl_dobl`=0');
db_execute('UPDATE `clans` SET `dobl`=0');
if(game_automation_table_exists('clan_memb')) db_execute('UPDATE `clan_memb` SET `dobl`=0');
}
function game_automation_news(string $code,string $periodKey,string $title,string $text): void {
game_automation_once($code,$periodKey,static function() use($title,$text): void {
if(function_exists('game_forum_admin_news')) game_forum_admin_news($title,$text);
});
}
function game_automation_tick(): void {
static $done=false; if($done) return; $done=true;
$now=time();
// Веб-запросы служат только резервным запуском. Основной календарь запускается
// фоновым обработчиком раз в минуту; этот барьер не даёт обычным страницам
// выполнять весь набор периодических задач чаще одного раза примерно в минуту.
$lastTick=(int)game_meta_get('automation_tick_at','0');
if($lastTick>0 && ($now-$lastTick)<50) return;
game_meta_set('automation_tick_at',(string)$now);
game_automation_bootstrap();
// Сражения по расписанию продвигаются независимо от открытых страниц.
if(function_exists('game_v51_events_tick')) game_v51_events_tick();
if(function_exists('game_weekly_tick')) game_weekly_tick();
// Каждые два часа проверяем пробуждение дракона.
$twoHour=(int)floor((int)date('G',$now)/2)*2;
$dragonKey=date('Ymd',$now).'-'.str_pad((string)$twoHour,2,'0',STR_PAD_LEFT);
game_automation_once('dragon_awaken',$dragonKey,'game_automation_dragon_revive');
// Ежедневные циклы.
$day=date('Y-m-d',$now);
if((int)date('G',$now)>=0){
game_automation_once('league_daily',$day,'game_automation_league_daily');
game_automation_once('chests_daily',$day,'game_automation_chests_daily');
game_automation_once('runtime_cleanup',$day,'game_automation_runtime_cleanup');
}
$weekday=(int)date('N',$now); $weekKey=date('o-W',$now);
// Понедельник: новая турнирная неделя, перераспределение лиг и закрытие кланового подземелья.
if($weekday===1){
game_automation_once('league_weekly',$weekKey,'game_automation_league_reseed');
game_automation_once('clan_dungeon_close',$weekKey,static fn()=>game_automation_clan_dungeon(false));
game_automation_news('weekly_tournaments_start',$weekKey,'Новая турнирная неделя',
'[center][b][color=#e8c66a]Новая турнирная неделя началась![/color][/b][/center]'."nn".
'С понедельника по пятницу герои собирают реликвии, а кланы — кристаллы.'."n".
'Обладатели Божественного трофея также сражаются за недельную награду.'."nn".
'[center][url=/relictournament/]Турнир реликвий[/url] · [url=/clantournament/]Клановый турнир[/url] · [url=/trophytournament/]Турнир трофеев[/url][/center]');
}
// Клановые битвы активны с пятницы до конца субботы. Пропущенный пятничный запуск догоняется в субботу.
if(in_array($weekday,[5,6],true)) game_automation_once('clan_battle_open',$weekKey,static fn()=>game_automation_clan_battle(true));
if($weekday===7) game_automation_once('clan_battle_close',$weekKey,static fn()=>game_automation_clan_battle(false));
// Клановое подземелье доступно в выходные. Если субботний запуск был пропущен, воскресенье восстановит цикл.
if(in_array($weekday,[6,7],true)) game_automation_once('clan_dungeon_open',$weekKey,static fn()=>game_automation_clan_dungeon(true));
// Колизей идёт с субботы по среду; итоги подводятся в ночь на четверг.
if($weekday===4){
$prevWeekKey=date('o-W',$now-86400);
game_automation_once('coliseum_weekly',$prevWeekKey,static fn()=>game_automation_finalize_coliseum($prevWeekKey));
}
if($weekday===6){
game_automation_news('coliseum_start',$weekKey,'Турнир Колизея начался',
'[center][b][color=#e8c66a]Турнир Колизея начался![/color][/b][/center]'."nn".
'Сражайтесь в Колизее, собирайте черепа и поднимайтесь в десятку сильнейших.'."n".
'Итоги будут подведены после среды.'."nn".
'[center][url=/coliseum/]Войти в Колизей[/url][/center]');
}
// Сезон доблести переключается по фактической смене месяца и не теряется после простоя.
$currentMonth=date('Y-m',$now); $activeMonth=game_meta_get('valor_active_month','');
if($activeMonth==='') game_meta_set('valor_active_month',$currentMonth);
elseif($activeMonth!==$currentMonth){
game_automation_once('valor_monthly',$activeMonth,static fn()=>game_automation_finalize_valor($activeMonth));
game_meta_set('valor_active_month',$currentMonth);
game_automation_news('valor_month_start',$currentMonth,'Новый сезон доблести',
'[center][b][color=#e8c66a]Новый сезон доблести открыт![/color][/b][/center]'."nn".
'Сражайтесь, повышайте личную доблесть и ведите свой клан к вершине рейтинга.'."nn".
'[center][url=/valor_info2/]Узнать о наградах[/url][/center]');
}
}