Вход Регистрация
Файл: system/progression.php
Строк: 879
<?php
/* Игровые прогрессии, недельные турниры, коллекции и сундуки.
 * Обновление игры: by Метриум (Стэлп)
 */
declare(strict_types=1);

function 
game_week_start(?int $ts=null): int {
    
$ts ??= time();
    
$d=(int)date('N',$ts); // 1=Mon..7=Sun
    
return strtotime(date('Y-m-d 00:00:00',$ts))-(($d-1)*86400);
}
function 
game_week_key(?int $ts=null): string { return date('o-W'game_week_start($ts)); }
function 
game_tournament_end(?int $ts=null): int { return game_week_start($ts)+5*86400; } // Saturday 00:00

function game_progression_bootstrap(): void {
    static 
$done=false; if($done) return; $done=true;
    
// One cheap probe on normal requests; DDL runs only after upgrades/new installs.
    
$probe=db_query("SHOW TABLES LIKE 'game_meta'");
    if(!
$probe || db_num_rows($probe)===0){
        
db_query("CREATE TABLE IF NOT EXISTS `game_meta` (`meta_key` varchar(64) NOT NULL, `meta_value` text NOT NULL, PRIMARY KEY (`meta_key`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    }
    
$ver=(int)(db_result(db_query("SELECT `meta_value` FROM `game_meta` WHERE `meta_key`='schema_version' LIMIT 1"),0) ?: 0);
    if(
$ver>=26) return;
    
db_query("CREATE TABLE IF NOT EXISTS `trophy_user` (`user` int NOT NULL, `qualified_week` varchar(10) NOT NULL DEFAULT '', `divine_until` int NOT NULL DEFAULT 0, `last_award_week` varchar(10) NOT NULL DEFAULT '', PRIMARY KEY (`user`), KEY `divine_until` (`divine_until`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    
db_query("CREATE TABLE IF NOT EXISTS `collector_heads` (`user` int NOT NULL, `collection` tinyint NOT NULL, `head` tinyint NOT NULL, `qty` int NOT NULL DEFAULT 0, PRIMARY KEY (`user`,`collection`,`head`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    
db_query("CREATE TABLE IF NOT EXISTS `relic_claims` (`user` int NOT NULL, `set_no` tinyint NOT NULL, `week_key` varchar(10) NOT NULL, PRIMARY KEY (`user`,`set_no`,`week_key`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    
db_query("CREATE TABLE IF NOT EXISTS `relic_daily` (`user` int NOT NULL, `day_key` varchar(10) NOT NULL, PRIMARY KEY (`user`,`day_key`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    
db_query("CREATE TABLE IF NOT EXISTS `daily_quest_assignment` (
      `user` int unsigned NOT NULL, `day_key` char(10) NOT NULL, `quest_id` int unsigned NOT NULL, `slot` tinyint unsigned NOT NULL,
      PRIMARY KEY (`user`,`day_key`,`quest_id`), UNIQUE KEY `uq_daily_slot` (`user`,`day_key`,`slot`), KEY `day_quest` (`day_key`,`quest_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
    
db_query("CREATE TABLE IF NOT EXISTS `economy_gold_daily` (
      `user` int unsigned NOT NULL, `day_key` char(10) NOT NULL, `source` varchar(32) NOT NULL, `earned` int unsigned NOT NULL DEFAULT 0,
      PRIMARY KEY (`user`,`day_key`,`source`), KEY `user_day` (`user`,`day_key`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
    
$cbm=db_fetch_assoc(db_query("SHOW COLUMNS FROM `users` LIKE 'collector_bonus_mode'"));
    if(!
$cbmdb_query("ALTER TABLE `users` ADD COLUMN `collector_bonus_mode` tinyint(1) NOT NULL DEFAULT 0 AFTER `bonus_col`");
        
db_query("CREATE TABLE IF NOT EXISTS `moderation_log` (`id` bigint NOT NULL AUTO_INCREMENT, `actor` int NOT NULL DEFAULT 0, `target` int NOT NULL DEFAULT 0, `kind` varchar(24) NOT NULL, `reason` varchar(300) NOT NULL DEFAULT '', `until_at` int NOT NULL DEFAULT 0, `action` varchar(24) NOT NULL DEFAULT 'apply', `created_at` int NOT NULL, `ip` varchar(45) NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `target_time` (`target`,`created_at`), KEY `actor_time` (`actor`,`created_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
db_query("CREATE TABLE IF NOT EXISTS `tournament_archive` (`id` bigint NOT NULL AUTO_INCREMENT, `week_key` varchar(10) NOT NULL, `kind` varchar(24) NOT NULL, `position` tinyint NOT NULL, `entity_id` int NOT NULL, `score` bigint NOT NULL DEFAULT 0, `reward_gold` bigint NOT NULL DEFAULT 0, `reward_silver` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uq_week_kind_pos` (`week_key`,`kind`,`position`), KEY `entity` (`kind`,`entity_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
    
$c=db_query("SHOW COLUMNS FROM `stars_dust_users` LIKE 'dust'");
    if(!
$c || db_num_rows($c)===0db_query("ALTER TABLE `stars_dust_users` ADD `dust` int NOT NULL DEFAULT 0 AFTER `sunduk`");
    
$c=db_query("SHOW COLUMNS FROM `stars_dust_users` LIKE 'cycle_key'");
    if(!
$c || db_num_rows($c)===0db_query("ALTER TABLE `stars_dust_users` ADD `cycle_key` varchar(24) NOT NULL DEFAULT '' AFTER `time`");
    
$c=db_query("SHOW COLUMNS FROM `inv` LIKE 'stars'");
    if(!
$c || db_num_rows($c)===0db_query("ALTER TABLE `inv` ADD `stars` tinyint NOT NULL DEFAULT 0 AFTER `smith`");
    foreach([
7,8] as $n){
        foreach([
'user_'.$n=>'int NOT NULL DEFAULT 0','user_'.$n.'_p'=>'int NOT NULL DEFAULT 0','q_'.$n=>'int NOT NULL DEFAULT 0',$n.'_time'=>'int NOT NULL DEFAULT 0'] as $col=>$def){
            
$c=db_query("SHOW COLUMNS FROM `clans_q` LIKE '".$col."'");
            if(!
$c || db_num_rows($c)===0db_query("ALTER TABLE `clans_q` ADD `".$col."` ".$def);
        }
    }
    
db_query("INSERT INTO `game_meta` (`meta_key`,`meta_value`) VALUES ('schema_version','26') ON DUPLICATE KEY UPDATE `meta_value`='26'");
}

function 
game_meta_get(string $key,string $default=''): string {
    
$v=db_result(db_query("SELECT `meta_value` FROM `game_meta` WHERE `meta_key`='".db_real_escape_string($key)."' LIMIT 1"),0);
    return 
$v===false?$default:(string)$v;
}
function 
game_meta_set(string $key,string $value): void {
    
db_query("INSERT INTO `game_meta` (`meta_key`,`meta_value`) VALUES ('".db_real_escape_string($key)."','".db_real_escape_string($value)."') ON DUPLICATE KEY UPDATE `meta_value`=VALUES(`meta_value`)");
}

function 
game_forum_admin_news(string $title,string $text): void {
    
db_query("INSERT INTO `forum_topic` (`sub`,`close`,`stick`,`user`,`name`,`text`,`time`) VALUES (1,0,0,0,'".db_real_escape_string($title)."','".db_real_escape_string($text)."',".time().")");
}
function 
game_system_mail(int $userId,string $text): void {
    if(
$userId<=0) return;
    
db_query("INSERT INTO `mail` (`from`,`to`,`text`,`time`,`read`) VALUES (0,$userId,'".db_real_escape_string($text)."',".time().",0)");
}

function 
game_weekly_tick(): void {
    
// Все три недельных турнира подводятся вместе в субботу.
    
$now=time(); $last=(int)game_meta_get('weekly_tick_at','0');
    if(
$last>$now-60) return; game_meta_set('weekly_tick_at',(string)$now);
    
$start=game_week_start($now); $end=$start+5*86400;
    if(
$now<$end) return;
    
$week=date('o-W',$start);
    if(
game_meta_get('weekly_finalized')===$week) return;
    
game_finalize_week($week);
}

function 
game_finalize_week(string $week): void {
    
$pdo=db_pdo();
    
$pdo->beginTransaction();
    try {
    
$news="[center][b]Еженедельные турниры окончены[/b][/center]nn";
    
// Crystal tournament: clans.
    
$news.="[center]Еженедельный турнир по 💎 кристаллам окончен[/center]nn";
    
$r=db_query("SELECT `id`,`name`,`plat`,`g`,`s` FROM `clans` WHERE `plat`>0 ORDER BY `plat` DESC,`id` ASC LIMIT 5");
    
$pos=0; while($c=db_fetch_assoc($r)){ $pos++; $score=(int)$c['plat']; $base=[0,100,80,65,50,40][$pos]; $g=min(250,$base+(int)floor($score/50)); $s=min(500000,$base*1200+$score*20);
        
db_query("UPDATE `clans` SET `g`=`g`+$g,`s`=`s`+$s WHERE `id`=".(int)$c['id']);
        
db_query("INSERT IGNORE INTO `tournament_archive` (`week_key`,`kind`,`position`,`entity_id`,`score`,`reward_gold`,`reward_silver`) VALUES ('$week','crystal',$pos,".(int)$c['id'].",$score,$g,$s)");
        
$news.="⭐ $pos. [url=/clan/".(int)$c['id']."/]".$c['name']."[/url] 💎 $scorenНаграда$g золота и $s серебраn";
    }
    
// Relics tournament: users.plat is relic tournament score in legacy project.
    
$news.="n[center]Еженедельный турнир по реликвиям окончен[/center]nn";
    
$pr=[0,120,90,70,50,30];
    
$r=db_query("SELECT `id`,`login`,`plat` FROM `users` WHERE `plat`>0 ORDER BY `plat` DESC,`id` ASC LIMIT 5"); $pos=0;
    while(
$u=db_fetch_assoc($r)){ $pos++; $g=$pr[$pos]; $score=(int)$u['plat']; db_query("UPDATE `users` SET `g`=`g`+$g WHERE `id`=".(int)$u['id']);
        
db_query("INSERT IGNORE INTO `tournament_archive` (`week_key`,`kind`,`position`,`entity_id`,`score`,`reward_gold`) VALUES ('$week','relic',$pos,".(int)$u['id'].",$score,$g)");
        
game_system_mail((int)$u['id'],"Еженедельный турнир по реликвиям окончен. Ваше место: $pos. Награда: $g золота.");
        
$news.="⭐ $pos. [url=/user/".(int)$u['id']."/]".$u['login']."[/url] 🏆 $scorenНаграда$g золотаn";
    }
    
// Trophy tournament: five random holders of the temporary divine trophy.
    
$news.="n[center]Еженедельный турнир по трофеям окончен[/center]nn";
    
$r=db_query("SELECT tu.user,u.login FROM `trophy_user` tu JOIN `users` u ON u.id=tu.user WHERE tu.qualified_week='$week' ORDER BY RAND() LIMIT 5"); $pos=0;
    while(
$u=db_fetch_assoc($r)){ $pos++; db_query("UPDATE `users` SET `g`=`g`+150 WHERE `id`=".(int)$u['user']);
        
db_query("UPDATE `trophy_user` SET `last_award_week`='$week' WHERE `user`=".(int)$u['user']);
        
db_query("INSERT IGNORE INTO `tournament_archive` (`week_key`,`kind`,`position`,`entity_id`,`score`,`reward_gold`) VALUES ('$week','trophy',$pos,".(int)$u['user'].",1,150)");
        
game_system_mail((int)$u['user'],"Турнир по трофеям завершён. Вы получили сундук с сокровищем: 150 золота!");
        
$news.="⭐ $pos. [url=/user/".(int)$u['user']."/]".$u['login']."[/url] — сундук с сокровищем и 150 золотаn";
    }
    
game_forum_admin_news('Еженедельные турниры завершены'$news."n[center][url=/relictournament/]Подробнее о турнирах[/url][/center]");
    
// New season. Crystal/relic scores and relic collections reset; trophy qualification becomes historical.
    
db_query("UPDATE `clans` SET `plat`=0");
    
db_query("UPDATE `clan_memb` SET `plat`=0");
    
db_query("UPDATE `users` SET `plat`=0");
    
// Недельные клановые задания относятся к завершившемуся турнирному циклу.
    
if(db_num_rows(db_query("SHOW TABLES LIKE 'clans_q'"))>0db_query("DELETE FROM `clans_q`");
    if(
db_num_rows(db_query("SHOW TABLES LIKE 'clan_q_user'"))>0db_query("DELETE FROM `clan_q_user`");
    
db_query("UPDATE `relict` SET a1=0,a2=0,a3=0,a4=0,b1=0,b2=0,b3=0,b4=0,c1=0,c2=0,c3=0,c4=0,d1=0,d2=0,d3=0,d4=0,e1=0,e2=0,e3=0,e4=0,f1=0,f2=0,f3=0,f4=0,g1=0,g2=0,g3=0,g4=0,h1=0,h2=0,h3=0,h4=0,i1=0,i2=0,i3=0,i4=0,j1=0,j2=0,j3=0,j4=0");
    
game_meta_set('weekly_finalized',$week);
    
$pdo->commit();
    } catch(
Throwable $e) {
        if(
$pdo->inTransaction()) $pdo->rollBack();
        throw 
$e;
    }
}

function 
game_trophy_task_data(array $user): array {
    
$uid=(int)$user['id'];
    
$clanId=(int)(db_result(db_query("SELECT `clan` FROM `clan_memb` WHERE `user`=$uid LIMIT 1"),0) ?: 0);
    
$topCrystal=$clanId>&& (int)(db_result(db_query("SELECT COUNT(*) FROM `tournament_archive` WHERE `kind`='crystal' AND `entity_id`=$clanId AND `position`<=3"),0) ?: 0)>0;
    
$runes=(int)(db_result(db_query("SELECT COUNT(*) FROM `inv` WHERE `user`=$uid AND `equip`=1 AND `rune`>=5"),0) ?: 0);
    
$smith15=(int)(db_result(db_query("SELECT COUNT(*) FROM `inv` WHERE `user`=$uid AND `equip`=1 AND `smith`>=15"),0) ?: 0);
    
$pet50=(int)(db_result(db_query("SELECT COUNT(*) FROM `user_pets` WHERE `id_user`=$uid AND (`tune_1`+`tune_2`+`tune_3`+`tune_4`)>=50"),0) ?: 0);
    
$abilEpic=((int)($user['ability_1_quality']??0)>=|| (int)($user['ability_2_quality']??0)>=|| (int)($user['ability_3_quality']??0)>=|| (int)($user['ability_4_quality']??0)>=|| (int)($user['ability_5_quality']??0)>=3);
    
$bonusSum=(int)(db_result(db_query("SELECT COALESCE(SUM(`bonus`),0) FROM `inv` WHERE `user`=$uid AND `equip`=1"),0) ?: 0);
    
$kingKills=(int)(db_result(db_query("SELECT COALESCE(MAX(`kills`),0) FROM `king_member` WHERE `user`=$uid"),0) ?: 0);
    return [
      [
'text'=>'Подними свой ранг в колизее до 2','progress'=>((int)($user['rang']??25)<=2?2:0),'goal'=>2,'link'=>'/coliseum/'],
      [
'text'=>'Добей Короля бессмертных последним ударом','progress'=>min(1,$kingKills),'goal'=>1,'link'=>'/undying/'],
      [
'text'=>'Попади в тройку лучших в еженедельном турнире по очкам реликвий','progress'=>(int)((db_result(db_query("SELECT COUNT(*) FROM `tournament_archive` WHERE `kind`='relic' AND `entity_id`=$uid AND `position`<=3"),0) ?: 0)>0),'goal'=>1,'link'=>'/relictournament/'],
      [
'text'=>'Попади в тройку выживших в битве с Королём бессмертных','progress'=>min(1,(int)($user['ach_undying']??0)),'goal'=>1,'link'=>'/undying/'],
      [
'text'=>'Помоги клану занять призовое место в клановом турнире','progress'=>(int)$topCrystal,'goal'=>1,'link'=>'/clantournament/'],
      [
'text'=>'Установи 2 руны редкого качества и выше','progress'=>min(2,$runes),'goal'=>2,'link'=>'/smith/runes/'],
      [
'text'=>'Установи на все 8 слотов заточку на +15 и выше','progress'=>min(8,$smith15),'goal'=>8,'link'=>'/smith/smith/'],
      [
'text'=>'Улучши любое умение до эпического качества','progress'=>(int)$abilEpic,'goal'=>1,'link'=>'/ability/'.$uid.'/'],
      [
'text'=>'Прокачай тренировочное мастерство до 250','progress'=>min(250,(int)($user['achievement_train']??0)),'goal'=>250,'link'=>'/train/'],
      [
'text'=>'Прокачай питомца до 50 уровня','progress'=>min(50,$pet50?50:(int)($user['pet_lvl']??0)),'goal'=>50,'link'=>'/petshop/'],
      [
'text'=>'Сумма всех бонусов 360 и выше','progress'=>min(360,$bonusSum),'goal'=>360,'link'=>'/user/'],
      [
'text'=>'Выполни 500 заданий','progress'=>min(500,(int)($user['quests']??0)),'goal'=>500,'link'=>'/quest/'],
      [
'text'=>'Победи 5000 раз на арене','progress'=>min(5000,(int)($user['arena_pobed']??0)),'goal'=>5000,'link'=>'/arena/'],
    ];
}
function 
game_trophy_refresh(array $user): array {
    
$uid=(int)$user['id']; $week=game_week_key(); $end=game_tournament_end();
    
$done=0; for($i=1;$i<=9;$i++) if((int)($user['troph'.$i]??0)===1)$done++;
    
db_query("INSERT IGNORE INTO `trophy_user` (`user`) VALUES ($uid)");
    if(
$done===&& time()<$end){ db_query("UPDATE `trophy_user` SET `qualified_week`='$week',`divine_until`=$end WHERE `user`=$uid"); }
    
$row=db_fetch_assoc(db_query("SELECT * FROM `trophy_user` WHERE `user`=$uid")) ?: ['qualified_week'=>'','divine_until'=>0];
    return [
'tasks'=>game_trophy_task_data($user),'done'=>$done,'total'=>9,'qualified'=>(($row['qualified_week']??'')===$week && (int)($row['divine_until']??0)>time()),'until'=>(int)($row['divine_until']??0)];
}
function 
game_apply_trophy_bonus(array &$user): void {
    if(empty(
$user['id'])) return; $uid=(int)$user['id'];
    
$until=(int)(db_result(db_query("SELECT `divine_until` FROM `trophy_user` WHERE `user`=$uid LIMIT 1"),0) ?: 0);
    if(
$until>time()){ foreach(['str','vit','agi','def'] as $k$user[$k]=(int)($user[$k]??0)+500; }
}

function 
game_collector_defs(): array {
    return [
      
1=>['name'=>'Обычная','color'=>'#9c9','heads'=>4,'need'=>100,'bonus'=>30],
      
2=>['name'=>'Обычная+','color'=>'#9c9','heads'=>4,'need'=>650,'bonus'=>60],
      
3=>['name'=>'Редкая','color'=>'#69c','heads'=>6,'need'=>2000,'bonus'=>120],
      
4=>['name'=>'Редкая+','color'=>'#69c','heads'=>6,'need'=>4800,'bonus'=>240],
      
5=>['name'=>'Эпическая','color'=>'#c6f','heads'=>8,'need'=>6600,'bonus'=>400],
      
6=>['name'=>'Коллекция Минотавра','color'=>'#f6a','heads'=>8,'need'=>25000,'bonus'=>500],
    ];
}
/** Общий бонус коллекций — это сумма параметров, а не прибавка к каждому стату. */
function game_stat_sum_parts(int $total): array {
    
$total=max(0,$total); $q=intdiv($total,4); $r=$total%4;
    return [
$q+($r>0?1:0),$q+($r>1?1:0),$q+($r>2?1:0),$q+($r>3?1:0)];
}
function 
game_collector_expected_bonus(int $nextCollection): int {
    
$defs=game_collector_defs(); $sum=0;
    
$completed=max(0,min(6,$nextCollection-1));
    for(
$i=1;$i<=$completed;$i++) $sum+=(int)$defs[$i]['bonus'];
    return 
$sum;
}

/**
 * Единый дневной потолок фармового золота. Пополнения, одноразовые покупки и
 * административные выдачи идут отдельно и не используют эту функцию.
 */
/** Единая цена магазина: качество и реальная сила вещи важнее старых раздутых legacy-cost. */
function game_shop_item_price(array $item,float $discountPct=0.0,int $flatReduction=0): int {
    
$q=max(0,min(7,(int)($item['quality']??0)));
    
$base=[0=>5,1=>10,2=>25,3=>45,4=>80,5=>140,6=>220,7=>300][$q];
    
$power=max(0,(int)($item['_str']??0)+(int)($item['_vit']??0)+(int)($item['_agi']??0)+(int)($item['_def']??0));
    
$price=min(500,$base+min(200,(int)floor($power/1000)));
    
$price=(int)ceil($price*max(0.0,1.0-min(90.0,max(0.0,$discountPct))/100.0));
    return 
max(5,$price-max(0,$flatReduction));
}

function 
game_economy_award_gold(int $uid,int $amount,string $source='gameplay',int $dailyCap=150): int {
    if(
$uid<=0||$amount<=0||$dailyCap<=0) return 0;
    
$day=game_daily_quest_day_key(); $source=substr(preg_replace('/[^a-z0-9_-]/i','_',$source)?:'gameplay',0,32);
    
$pdo=db_pdo(); $own=!$pdo->inTransaction(); if($own$pdo->beginTransaction();
    try{
        
db_execute('INSERT IGNORE INTO `economy_gold_daily` (`user`,`day_key`,`source`,`earned`) VALUES (:u,:d,:s,0)',['u'=>$uid,'d'=>$day,'s'=>'__total__']);
        
$row=db_fetch_one('SELECT `earned` FROM `economy_gold_daily` WHERE `user`=:u AND `day_key`=:d AND `source`=:s FOR UPDATE',['u'=>$uid,'d'=>$day,'s'=>'__total__']);
        
$earned=(int)($row['earned']??0); $grant=max(0,min($amount,$dailyCap-$earned));
        if(
$grant>0){
            
db_execute('UPDATE `economy_gold_daily` SET `earned`=`earned`+:g WHERE `user`=:u AND `day_key`=:d AND `source`=:s',['g'=>$grant,'u'=>$uid,'d'=>$day,'s'=>'__total__']);
            
db_execute('INSERT INTO `economy_gold_daily` (`user`,`day_key`,`source`,`earned`) VALUES (:u,:d,:s,:g) ON DUPLICATE KEY UPDATE `earned`=`earned`+VALUES(`earned`)',['u'=>$uid,'d'=>$day,'s'=>$source,'g'=>$grant]);
            
db_execute('UPDATE `users` SET `g`=`g`+:g WHERE `id`=:u',['g'=>$grant,'u'=>$uid]);
        }
        if(
$own$pdo->commit(); return $grant;
    }catch(
Throwable $e){ if($own && $pdo->inTransaction()) $pdo->rollBack(); throw $e; }
}

function 
game_collector_heads(int $uid,int $coll): array {
    
$out=[]; $r=db_query("SELECT `head`,`qty` FROM `collector_heads` WHERE `user`=$uid AND `collection`=$coll"); while($x=db_fetch_assoc($r)) $out[(int)$x['head']]=(int)$x['qty']; return $out;
}
function 
game_collector_drop(int $uid,int $coll): int {
    
$defs=game_collector_defs(); if(!isset($defs[$coll])) return 0$n=$defs[$coll]['heads']; $owned=game_collector_heads($uid,$coll); $missing=[]; for($i=1;$i<=$n;$i++) if(($owned[$i]??0)===0)$missing[]=$i;
    
$newChance=[1=>72,2=>62,3=>50,4=>40,5=>28,6=>18][$coll];
    if(
$missing && random_int(1,100)<=$newChance$head=$missing[array_rand($missing)]; else $head=random_int(1,$n);
    
db_query("INSERT INTO `collector_heads` (`user`,`collection`,`head`,`qty`) VALUES ($uid,$coll,$head,1) ON DUPLICATE KEY UPDATE `qty`=`qty`+1"); return $head;
}
function 
game_collector_unique_count(int $uid,int $coll): int { return (int)(db_result(db_query("SELECT COUNT(*) FROM `collector_heads` WHERE `user`=$uid AND `collection`=$coll AND `qty`>0"),0) ?: 0); }

function 
game_daily_quest_day_key(?int $ts=null): string {
    return 
date('Y-m-d',$ts??time());
}

/**
 * Ровно 10 ежедневных заданий. Состав вращается по дням и игрокам, но после
 * первого открытия дня не меняется. Старые лишние user_q не удаляются — они
 * просто не участвуют в сегодняшнем прогрессе, поэтому обновление безопасно.
 */
function game_daily_quest_ids(int $uid): array {
    if(
$uid<=0) return [];
    
$day=game_daily_quest_day_key();
    
$hasClan=(bool)db_fetch_one('SELECT `id` FROM `clan_memb` WHERE `user`=:u LIMIT 1',['u'=>$uid]);
    
$rows=db_prepare('SELECT a.`quest_id`,q.`place` FROM `daily_quest_assignment` a JOIN `quest` q ON q.`id`=a.`quest_id` WHERE a.`user`=:u AND a.`day_key`=:d ORDER BY a.`slot`',['u'=>$uid,'d'=>$day])->fetchAll();
    
$validTen=count($rows)===10 && ($hasClan || !array_filter($rows,static fn($r)=>(int)$r['place']===11));
    if(
$validTen) return array_map(static fn($r)=>(int)$r['quest_id'],$rows);

    
// Удаляем только недособранную запись текущего дня: это мог быть оборванный первый запрос.
    
db_execute('DELETE FROM `daily_quest_assignment` WHERE `user`=:u AND `day_key`=:d',['u'=>$uid,'d'=>$day]);
    
game_quest_seed_v49();
    
// Рекурсивные задания «выполни N заданий» не входят в ежедневную десятку.
    
$pool=$hasClan db_prepare('SELECT `id` FROM `quest` WHERE `id` NOT IN (2,5) ORDER BY `id`')->fetchAll() : db_prepare('SELECT `id` FROM `quest` WHERE `id` NOT IN (2,5) AND `place`<>11 ORDER BY `id`')->fetchAll();
    
$ids=array_map(static fn($r)=>(int)$r['id'],$pool);
    if(!
$ids) return [];
    
$count=count($ids); $want=min(10,$count);
    
$seed=(int)sprintf('%u',crc32($day.':'.$uid)); $offset=$count>0?$seed%$count:0;
    
$picked=[];
    for(
$i=0;$i<$count && count($picked)<$want;$i++){
        
$qid=$ids[($offset+$i*3)%$count]; // шаг 3 даёт смешение разных локаций
        
if(!in_array($qid,$picked,true)) $picked[]=$qid;
    }
    
// Если gcd(3,count) не позволил набрать 10 — добираем по порядку.
    
foreach($ids as $qid){ if(count($picked)>=$want) break; if(!in_array($qid,$picked,true))$picked[]=$qid; }

    
$pdo=db_pdo(); $ownTx=!$pdo->inTransaction(); if($ownTx)$pdo->beginTransaction();
    try{
        foreach(
array_values($picked) as $slot=>$qid){
            
db_execute('INSERT INTO `daily_quest_assignment` (`user`,`day_key`,`quest_id`,`slot`) VALUES (:u,:d,:q,:s)',['u'=>$uid,'d'=>$day,'q'=>$qid,'s'=>$slot+1]);
            
$exists=db_fetch_one('SELECT 1 ok FROM `user_q` WHERE `user`=:u AND `q`=:q LIMIT 1',['u'=>$uid,'q'=>$qid]);
            if(
$existsdb_execute('UPDATE `user_q` SET `c`=0,`complete`=0,`time`=0 WHERE `user`=:u AND `q`=:q',['u'=>$uid,'q'=>$qid]);
            else 
db_execute('INSERT INTO `user_q` (`user`,`q`,`c`,`complete`,`time`) VALUES (:u,:q,0,0,0)',['u'=>$uid,'q'=>$qid]);
        }
        if(
$ownTx)$pdo->commit();
    }catch(
Throwable $e){ if($ownTx&&$pdo->inTransaction())$pdo->rollBack(); throw $e; }
    return 
$picked;
}

function 
game_daily_quest_rows(int $uid): array {
    
$ids=game_daily_quest_ids($uid); if(!$ids)return [];
    
$day=game_daily_quest_day_key();
    return 
db_prepare('SELECT a.`slot`,uq.`q`,uq.`c` progress,uq.`complete`,uq.`time`,q.`title`,q.`description`,q.`place`,q.`type`,q.`c` goal,q.`_gold`,q.`_plat`,q.`_exp` '
        
.'FROM `daily_quest_assignment` a JOIN `quest` q ON q.`id`=a.`quest_id` LEFT JOIN `user_q` uq ON uq.`user`=a.`user` AND uq.`q`=a.`quest_id` '
        
.'WHERE a.`user`=:u AND a.`day_key`=:d ORDER BY a.`slot`',['u'=>$uid,'d'=>$day])->fetchAll();
}

function 
game_daily_quest_completed(int $uid): int {
    
$day=game_daily_quest_day_key();
    return (int)(
db_fetch_one('SELECT COUNT(*) c FROM `daily_quest_assignment` a JOIN `user_q` uq ON uq.`user`=a.`user` AND uq.`q`=a.`quest_id` WHERE a.`user`=:u AND a.`day_key`=:d AND uq.`complete`=1',['u'=>$uid,'d'=>$day])['c']??0);
}

function 
game_quest_routes(): array {
    return [
        
1=>'/arena/'2=>'/quest/'3=>'/duel/'4=>'/farm/'5=>'/cave/',
        
6=>'/coliseum/'7=>'/undying/'8=>'/dryit/'9=>'/league/',
        
10=>'/campaign/'11=>'/altars/'
    
];
}

/**
 * Создаёт отсутствующие строки прогресса для конкретной активности.
 * Старый проект создавал user_q только после открытия /quest/, поэтому бои,
 * проведённые до первого посещения страницы заданий, теряли прогресс.
 */
function game_quest_ensure_user_place(int $uid,int $place): void {
    
// Daily assignment owns the active set. Calling this before combat makes sure the 10 rows exist,
    // but never creates hidden 11th/12th tasks.
    
if($uid<=0) return;
    
game_daily_quest_ids($uid);
}

/** Единый счётчик: изменяет только те задания, которые входят в сегодняшние 10. */
function game_quest_progress(int $uid,int $place,int $type,int $amount=1): int {
    if(
$uid<=|| $place<=|| $amount<=|| !in_array($type,[0,1],true)) return 0;
    
game_daily_quest_ids($uid); $day=game_daily_quest_day_key();
    
$st=db_prepare(
        
'UPDATE `user_q` uq JOIN `quest` q ON q.`id`=uq.`q` '
        
.'JOIN `daily_quest_assignment` a ON a.`user`=uq.`user` AND a.`quest_id`=uq.`q` AND a.`day_key`=:day '
        
.'SET uq.`c`=LEAST(q.`c`,uq.`c`+:inc) '
        
.'WHERE uq.`user`=:uid AND uq.`complete`=0 AND q.`place`=:place AND q.`type`=:type AND uq.`c`<q.`c`',
        [
'day'=>$day,'inc'=>$amount,'uid'=>$uid,'place'=>$place,'type'=>$type]
    );
    if(
$st->rowCount()>&& function_exists('game_activity_mark')) game_activity_mark('quests',1);
    return 
$st->rowCount();
}

function 
game_quest_progress_event(int $uid,int $place,int $participation=0,int $success=0): void {
    if(
$participation>0game_quest_progress($uid,$place,0,$participation);
    if(
$success>0game_quest_progress($uid,$place,1,$success);
}

/** Сводка активного задания для страницы, куда ведёт «Перейти к выполнению». */
function game_quest_context(int $uid,int $qid): ?array {
    if(
$uid<=0||$qid<=0)return null$day=game_daily_quest_day_key();
    return 
db_fetch_one('SELECT uq.`c` progress,uq.`complete`,q.`c` goal,q.`title`,q.`description` FROM `daily_quest_assignment` a JOIN `quest` q ON q.`id`=a.`quest_id` JOIN `user_q` uq ON uq.`user`=a.`user` AND uq.`q`=a.`quest_id` WHERE a.`user`=:u AND a.`day_key`=:d AND a.`quest_id`=:q LIMIT 1',['u'=>$uid,'d'=>$day,'q'=>$qid]);
}

function 
game_progress_markup(int $current,int $goal,string $label='Прогресс'): string {
    
$goal=max(1,$goal); $current=max(0,min($goal,$current)); $pct=(int)floor(($current/$goal)*100);
    
$done=$current>=$goal;
    return 
'<div class="game-progress-panel'.($done?' is-complete':'').'">'
        
.'<div class="game-progress-numbers"><span class="blue">'.security_html($label).':</span> <b>'.n_f($current).'</b> из <b>'.n_f($goal).'</b>'
        
.($done?' <span class="green">✓ выполнено</span>':' <span class="grey">· осталось '.n_f($goal-$current).'</span>').'</div>'
        
.'<div class="game-progress-bar"><span style="width:'.$pct.'%"></span></div></div>';
}

function 
game_quest_seed_v49(): void {
    
// Награды рассчитаны на максимум 10 ежедневных заданий: основной рост идёт через опыт,
    // золото остаётся редкой премиальной валютой и не фармится сотнями за сутки.
    
$rows=[
      [
1,'Только победы!','Победи противников на арене 10 раз',1,10,1,5,0,1800,0],
      [
2,'Задание мудреца','Выполни любые 10 ежедневных заданий',2,10,0,5,0,2500,0],
      [
3,'Бои на арене','Проведи 5 боёв на арене',1,5,0,2,0,1200,0],
      [
4,'Дуэли','Проведи 10 боёв в дуэлях',3,10,0,4,0,1500,0],
      [
5,'Выполни все задания','Выполни 5 ежедневных заданий',2,5,0,3,0,1600,0],
      [
9,'Жажда крови!','Убей 5 врагов в Долине бессмертных',7,5,1,6,1,2600,0],
      [
11,'Легионер','Проведи 25 боёв в Лиге',9,25,0,5,0,2400,0],
      [
12,'Разминка на арене','Проведи 3 боя на арене',1,3,0,2,0,900,0],
      [
13,'Серия побед','Победи на арене 5 раз',1,5,1,4,0,1500,0],
      [
14,'Дуэлянт','Проведи 5 боёв в дуэлях',3,5,0,2,0,1000,0],
      [
15,'Победитель дуэлей','Одержи 3 победы в дуэлях',3,3,1,4,0,1500,0],
      [
16,'Искатель','Проведи 5 поисков в пещере',5,5,0,2,0,1000,0],
      [
17,'Собиратель ресурсов','Найди ресурсы в пещере 3 раза',5,3,1,4,0,1400,0],
      [
18,'Испытание бессмертных','Прими участие в 3 боях Долины бессмертных',7,3,0,3,0,1300,0],
      [
19,'Охотник на бессмертных','Победи 5 врагов в Долине бессмертных',7,5,1,6,1,2400,0],
      [
20,'Новичок Лиги','Проведи 5 боёв в Лиге',9,5,0,2,0,1000,0],
      [
21,'Боец Лиги','Победи в 5 боях Лиги',9,5,1,5,0,1800,0],
      [
22,'Паломник древних алтарей','Прими участие в 3 битвах Древних алтарей',11,3,0,3,0,1400,0],
      [
23,'Захватчик алтаря','Помоги клану захватить древний алтарь',11,1,1,6,1,2200,0],
      [
24,'Воин кампании','Проведи 5 сражений в кампании',10,5,0,3,0,1300,0],
      [
25,'Покоритель кампании','Одержи 3 победы в кампании',10,3,1,5,0,1900,0],
      [
26,'Гладиатор','Проведи 5 боёв в Колизее',6,5,0,3,0,1300,0],
      [
27,'Триумфатор Колизея','Одержи 3 победы в Колизее',6,3,1,5,0,1900,0]
    ];
    foreach(
$rows as $r){
        
db_execute('INSERT INTO `quest` (`id`,`title`,`description`,`place`,`c`,`type`,`_gold`,`_plat`,`_exp`,`_maze_kluchs`) '
            
.'VALUES (:id,:t,:d,:p,:c,:ty,:g,:pl,:e,:m) '
            
.'ON DUPLICATE KEY UPDATE `title`=VALUES(`title`),`description`=VALUES(`description`),`place`=VALUES(`place`),`c`=VALUES(`c`),`type`=VALUES(`type`),`_gold`=VALUES(`_gold`),`_plat`=VALUES(`_plat`),`_exp`=VALUES(`_exp`),`_maze_kluchs`=VALUES(`_maze_kluchs`)',[
            
'id'=>$r[0],'t'=>$r[1],'d'=>$r[2],'p'=>$r[3],'c'=>$r[4],'ty'=>$r[5],'g'=>$r[6],'pl'=>$r[7],'e'=>$r[8],'m'=>$r[9]
        ]);
    }
}

function 
game_star_chest_state(int $uid): array {
    
game_progression_bootstrap(); $now=time(); $row=db_fetch_assoc(db_query("SELECT * FROM `stars_dust_users` WHERE `user`=$uid LIMIT 1"));
    if(!
$row){ $next=$now+16*3600db_query("INSERT INTO `stars_dust_users` (`user`,`energy`,`sunduk`,`dust`,`time`,`cycle_key`) VALUES ($uid,0,0,0,$next,'".date('YmdHi',$now)."')"); $row=['user'=>$uid,'energy'=>0,'sunduk'=>0,'dust'=>0,'time'=>$next]; }
    if((int)
$row['time']<=$now){ $next=$now+16*3600db_query("UPDATE `stars_dust_users` SET `energy`=0,`sunduk`=0,`time`=$next,`cycle_key`='".date('YmdHi',$now)."' WHERE `user`=$uid"); $row['energy']=0;$row['sunduk']=0;$row['time']=$next; }
    return 
$row;
}
function 
game_energy_spend(int $uid,int $amount): void { if($uid<=0||$amount<=0)return; game_star_chest_state($uid); db_query("UPDATE `stars_dust_users` SET `energy`=`energy`+$amount WHERE `user`=$uid"); }
function 
game_star_chest_open(int $uid): array {
    
$s=game_star_chest_state($uid); $stage=(int)$s['sunduk']; $threshold=[0=>6420,1=>21400]; $reward=[0=>10,1=>25];
    if(
$stage>=2) return ['ok'=>false,'msg'=>'Все сундуки уже открыты до обновления.'];
    if((int)
$s['energy']<$threshold[$stage]) return ['ok'=>false,'msg'=>'Недостаточно потраченной энергии.'];
    
$dust=$reward[$stage]; db_query("UPDATE `stars_dust_users` SET `sunduk`=`sunduk`+1,`dust`=`dust`+$dust WHERE `user`=$uid");
    return [
'ok'=>true,'msg'=>'Сундук открыт! Получено '.$dust.' звездной пыли.','dust'=>$dust];
}


function 
game_relic_ensure(int $uid): array {
    
$r=db_fetch_assoc(db_query("SELECT * FROM `relict` WHERE `usr`='$uid' LIMIT 1"));
    if(
$r) return $r;
    
$cols=[];$vals=[]; foreach(range('a','j') as $g){for($i=1;$i<=4;$i++){ $cols[]='`'.$g.$i.'`'$vals[]='0'; }}
    
db_query("INSERT INTO `relict` (`usr`,".implode(',',$cols).") VALUES ('$uid',".implode(',',$vals).")");
    return 
db_fetch_assoc(db_query("SELECT * FROM `relict` WHERE `usr`='$uid' LIMIT 1")) ?: [];
}

function 
game_award_daily_relic(int $uid): ?array {
    if((int)
date('N')>5) return null$day=date('Y-m-d');
    if((int)(
db_result(db_query("SELECT COUNT(*) FROM `relic_daily` WHERE `user`=$uid AND `day_key`='$day'"),0) ?: 0)>0) return null;
    
db_query("INSERT IGNORE INTO `relic_daily` (`user`,`day_key`) VALUES ($uid,'$day')");
    
$r=game_relic_ensure($uid);
    
$groups=range('a','j'); $chosen=null; foreach($groups as $g){ for($i=1;$i<=4;$i++){ $f=$g.$i; if((int)($r[$f]??0)===0){$chosen=$f;break 2;} } }
    if(
$chosen===null$chosen=$groups[array_rand($groups)].random_int(1,4);
    
db_query("UPDATE `relict` SET `$chosen`=1 WHERE `usr`='$uid'");
    
db_query("UPDATE `users` SET `plat`=`plat`+1 WHERE `id`=$uid");
    return [
'field'=>$chosen,'point'=>1];
}
Онлайн: 4
Реклама