ÿØÿà JFIF    ÿÛ „  ( %"1!%)+...383,7(-.+  -+++--++++---+-+-----+---------------+---+-++7-----ÿÀ  ß â" ÿÄ     ÿÄ H    !1AQaq"‘¡2B±ÁÑð#R“Ò Tbr‚²á3csƒ’ÂñDS¢³$CÿÄ   ÿÄ %  !1AQa"23‘ÿÚ   ? ôÿ ¨pŸªáÿ —åYõõ\?àÒü©ŠÄï¨pŸªáÿ —åYõõ\?àÓü©ŠÄá 0Ÿªáÿ Ÿå[úƒ ú®ði~TÁbqÐ8OÕpÿ ƒOò¤Oè`–RÂáœá™êi€ßÉ< FtŸI“öÌ8úDf´°å}“¾œ6  öFá°y¥jñÇh†ˆ¢ã/ÃÐ:ªcÈ "Y¡ðÑl>ÿ ”ÏËte:qž\oäŠe÷󲍷˜HT4&ÿ ÓÐü6ö®¿øþßèô Ÿ•7Ñi’•j|“ñì>b…þS?*Óôÿ ÓÐü*h¥£ír¶ü UãS炟[AÐaè[ûª•õ&õj?†Éö+EzP—WeÒírJFt ‘BŒ†Ï‡%#tE Øz ¥OÛ«!1›üä±Í™%ºÍãö]°î(–:@<‹ŒÊö×òÆt¦ãº+‡¦%ÌÁ²h´OƒJŒtMÜ>ÀÜÊw3Y´•牋4ǍýʏTì>œú=Íwhyë,¾Ôò×õ¿ßÊa»«þˆѪQ|%6ž™A õ%:øj<>É—ÿ Å_ˆCbõ¥š±ý¯Ýƒï…¶|RëócÍf溪“t.СøTÿ *Ä¿-{†çàczůŽ_–^XþŒ±miB[X±d 1,é”zEù»& î9gœf™9Ð'.;—™i}!ôšåîqêÛ٤ёý£½ÆA–àôe"A$˝Úsäÿ ÷Û #°xŸëí(l »ý3—¥5m! rt`†0~'j2(]S¦¦kv,ÚÇ l¦øJA£Šƒ J3E8ÙiŽ:cÉžúeZ°€¯\®kÖ(79«Ž:¯X”¾³Š&¡* ….‰Ž(ÜíŸ2¥ª‡×Hi²TF¤ò[¨íÈRëÉ䢍mgÑ.Ÿ<öäS0í„ǹÁU´f#Vß;Õ–…P@3ío<ä-±»Ž.L|kªÀê›fÂ6@»eu‚|ÓaÞÆŸ…¨ááå>åŠ?cKü6ùTÍÆ”†sĤÚ;H2RÚ†õ\Ö·Ÿn'¾ ñ#ºI¤Å´%çÁ­‚â7›‹qT3Iï¨ÖÚ5I7Ë!ÅOóŸ¶øÝñØôת¦$Tcö‘[«Ö³šÒ';Aþ ¸èíg A2Z"i¸vdÄ÷.iõ®§)¿]¤À†–‡É&ä{V¶iŽ”.Ó×Õÿ û?h¬Mt–íª[ÿ Ñÿ ÌV(í}=ibÔ¡›¥¢±b Lô¥‡piη_Z<‡z§èŒ)iÖwiÇ 2hÙ3·=’d÷8éŽ1¦¸c¤µ€7›7Ø ð\á)} ¹fËí›pAÃL%âc2 í§æQz¿;T8sæ°qø)QFMð‰XŒÂ±N¢aF¨…8¯!U  Z©RÊ ÖPVÄÀÍin™Ì-GˆªÅËŠ›•zË}º±ŽÍFò¹}Uw×#ä5B¤{î}Ð<ÙD é©¤&‡ïDbàÁôMÁ." ¤‡ú*õ'VŽ|¼´Úgllº¼klz[Æüï÷Aób‡Eÿ dÑ»Xx9ÃÜ£ÁT/`¼¸vI±Ýµ·Ë‚“G³þ*Ÿû´r|*}<¨îºœ @¦mÄ’M¹”.œ«Y–|6ÏU¤jç¥ÕÞqO ˜kDÆÁ¨5ÿ š;ÐЦ¦€GÙk \ –Þ=â¼=SͧµªS°ÚÍpÜãQűÀõ¬?ÃÁ1Ñ•õZà?hóœ€ L¦l{Y*K˜Ù›zc˜–ˆâ ø+¾ ­-Ök¥%ùEÜA'}ˆ><ÊIè“bpÍ/qÞâvoX€w,\úªò6Z[XdÒæ­@Ö—€$òJí#é>'°Ú ôª˜<)4ryÙ£|óAÅn5žêŸyÒäMÝ2{"}‰–¤l÷ûWX\l¾Á¸góÉOÔ /óñB¤f¸çñ[.P˜ZsÊË*ßT܈§QN¢’¡¨§V¼(Üù*eÕ“”5T¨‹Âê¥FŒã½Dü[8'Ò¥a…Ú¶k7a *•›¼'Ò·\8¨ª\@\õ¢¦íq+DÙrmÎ…_ªæ»ŠÓœ¡¯’Ré9MÅ×D™lælffc+ŒÑ,ý™ÿ ¯þǤ=Å’Á7µ÷ÚÛ/“Ü€ñýã¼àí¾ÕÑ+ƒ,uµMâÀÄbm:ÒÎPæ{˜Gz[ƒ¯«® KHà`ߨŠéí¯P8Aq.C‰ à€kòpj´kN¶qô€…Õ,ÜNŠª-­{Zö’æû44‰sŽè‰îVíRœÕm" 6?³D9¡ÇTíÅꋇ`4«¸ÝÁô ï’ýorqКÇZ«x4Žâéþuïf¹µö[P ,Q£éaX±`PÉÍZ ¸äYúg üAx ’6Lê‚xÝÓ*äQ  Ï’¨hÍ =²,6ï#rÃ<¯–£»ƒ‹,–ê•€ aÛsñ'%Æ"®ÛüìBᝠHÚ3ß°©$“XnœÖ’î2ËTeûìxîß ¦å¿çÉ ðK§þ{‘t‚Ϋ¬jéîZ[ ”š7L¥4VÚCE×]m¤Øy”ä4-dz£œ§¸x.*ãÊÊ b÷•h:©‡¦s`BTÁRû¾g⻩‹jø sF¢àJøFl‘È•Xᓁà~*j¯ +(ÚÕ6-£¯÷GŠØy‚<Ç’.F‹Hœw(+)ÜÜâÈzÄäT§FߘãÏ;DmVœ3Àu@mÚüXÝü•3B¨òÌÁÛ<·ÃÜ z,Ì@õÅ·d2]ü8s÷IôÞ¯^Ç9¢u„~ëAŸï4«M? K]­ÅàPl@s_ p:°¬ZR”´›JC[CS.h‹ƒïËœ«Æ]–÷ó‚wR×k7X‰k›‘´ù¦=¡«‰¨¨Â')—71ó’c‡Ðúµ `é.{§p¹ój\Ž{1h{o±Ý=áUÊïGÖŒõ–-BÄm+AZX¶¡ ïHðæ¥JmÙ;…䡟ˆ¦ ° äšiÉg«$üMk5¤L“’çÊvïâï ,=f“"íἊ5ô¬x6{ɏžID0e¸vçmi'︧ºð9$ò¹÷*£’9ÿ ²TÔ…×>JV¥}Œ}$p[bÔ®*[jzS*8 ”·T›Í–ñUîƒwo$áè=LT™ç—~ô·¤ÈÚ$榍q‰„+´kFm)ž‹©i–ËqÞŠ‰à¶ü( ‚•§ •°ò·‡#5ª•µÊ﯅¡X¨šÁ*F#TXJÊ ušJVÍ&=iÄs1‚3•'fý§5Ñ<=[íÞ­ PÚ;ѱÌ_~Ä££8rÞ ²w;’hDT°>ÈG¬8Á²ÚzŽ®ò®qZcqJêäÞ-ö[ܘbň±çb“ж31²n×iƒðÕ;1¶þÉ ªX‰,ßqÏ$>•î íZ¥Z 1{ç൵+ƒÕµ¥°T$§K]á»Ûï*·¤tMI’ÂZbŽÕiÒ˜}bÓ0£ª5›¨ [5Ž^ÝœWøÂÝh° ¢OWun£¤5 a2Z.G2³YL]jåtì”ä ÁÓ‘%"©<Ôúʰsº UZvä‡ÄiÆÒM .÷V·™ø#kèýiíÌ–ª)µT[)BˆõÑ xB¾B€ÖT¨.¥~ð@VĶr#¸ü*åZNDŽH;âi ],©£öØpù(šºãö¼T.uCê•4@ÿ GÕÛ)Cx›®0ø#:ÏðFÒbR\(€€Ä®fã4Þ‰Fä¯HXƒÅ,†öEÑÔÜ]Öv²?tLÃvBY£ú6Êu5ÅAQ³1‘’¬x–HŒÐ‡ ^ ¸KwJôÖŽ5×CÚ¨vÜ«/B0$×k°=ðbÇ(Ï)w±A†Á† 11Í=èQšµ626ŒÜ/`G«µ<}—-Ö7KEHÈÉðóȤmݱû±·ø«Snmá=“䫚mݱŸ¡¶~ó·“äUóJæúòB|E LêŽy´jDÔ$G¢þÐñ7óR8ýÒ…Ç› WVe#·Ÿ p·Fx~•ݤF÷0Èÿ K¯æS<6’¡WШ; ´ÿ ¥Êø\Òuî†åÝ–VNœkÒ7oòX¨Á­Ø÷FÎÑä±g÷ÿ M~Çî=p,X´ ÝÌÚÅ‹’ÃjÖ.ØöÏñ qïQ¤ÓZE†° =6·]܈ s¸>v•Ž^Ý\wq9r‰Î\¸¡kURÒ$­*‹Nq?Þª*!sŠÆ:TU_u±T+øX¡ ®¹¡,ÄâÃBTsÜ$Ø›4m椴zÜK]’’›Pƒ @€#â˜`é¹=I‡fiV•Ôî“nRm+µFPOhÍ0B£ €+¬5c v•:P'ÒyÎ ‰V~‚Ó†ÖuókDoh$å\*ö%Ю=£«…aȼ½÷Û.-½VŒŠ¼'lyî±1¬3ó#ÞE¿ÔS¤gV£m›=§\û"—WU¤ÚǼÿ ÂnÁGŒÃ ‚õN D³õNÚíŒÕ;HôyÄÈ©P¹Ä{:?R‘Ô¨âF÷ø£bÅó® JS|‚R÷ivýáâ€Æé¡è³´IئÑT!§˜•ت‚¬â@q€wnïCWÄ@JU€ê¯m6]Ï:£âx'+ÒðXvÓ¦Úm=–´7œ $ì“B£~p%ÕŸUþ« N@¼üï~w˜ñø5®—'Ôe»¤5ã//€ž~‰Tþ›Å7•#¤× Íö pÄ$ùeåì*«ÓŠEØWEÈsßg ¦ûvžSsLpºÊW–âµEWöˬH; ™!CYõZ ÃÄf æ#1W. \uWâ\,\Çf j’<qTbên›Î[vxx£ë 'ö¨1›˜ÀM¼Pÿ H)ƒêêŒA7s,|F“ 꺸k³9Ìö*ç®;Ö!Ö$Eiž•¹ÒÚ†ýóéÝû¾ÕS®ó$’NÝäŸz¤5r¦ãÄÃD÷Üø!°ø‡Ô&@m™Ì^Ãä­d q5Lnÿ N;.6½·N|#ä"1Nƒx“ã<3('&ñßt  ~ªu”1Tb㫨9ê–›–bìd$ߣ=#ÕãÒmU¯eí$EFù5ýYô櫨æì™Ç—±ssM]·á¿0ÕåJRÓªîiƒ+O58ÖñªŠÒx" \µâá¨i’¤i —Ö ” M+M¤ë9‚‰A¦°Qõ¾ßøK~¼Ã‘g…Ö´~÷Ï[3GUœÒ½#…kàÔ®Ò”‰³·dWV‰IP‰Ú8u¹”E ÖqLj¾êÕCBš{A^Âß;–¨`¯¬ìö ˼ ×tìø.tƐm*n¨y4o&Àx¥n¦×î‡aupáÛj8¿m›è¶ã!o½;ß0y^ý×^EÑ¿ÒjzŒ­)vÚÑnÄL …^ªô× ‡—‚3k Îý­hï]içå–îÏ*÷ñþ»Ô CÒjøjÍznˆ´ ¹#b'Fô‹ ‰v¥'’à'T´ƒHýÍ%M‰ ƒ&ÆÇŒï1 ‘ –Þ ‰i¬s žR-Ÿ kЬá¬7:þ 0ŒÅÒÕ/aÙ¬ÃÝ#Úøœ ©aiVc‰. ¹¦ãµ” ›Yg¦›ÆÎýº°f³7ƒhá·¸­}&D9¡ÂsÉÙÞèŠõØàC™¨ñbFC|´Ü(ŸƒÚÒ-%»'a Ì¿)ËÇn¿úÿ ÞŽX…4ÊÅH^ôΑí@ù¹Eh¶“L8Çjù ¼ÎåVªóR©Ï5uà V4lZß®=€xÖŸ–ÑÈ ÷”¨°¾__yM1tÉ?uÆþIkÄgæ@þ[¢†°XÃJ£j·:nkÅ¢u ‘}âGzö­/IµèЬ¼48q¦F°ŽR¼=ûì{´¯RýicS ÕÛ íNtÍÙï£,w4rêì®»~x(©Uñ§#Ñ&œÕ¤>ÎåÍÓ9’Ö{9eV­[Öjâ²ãu]˜å2›qÑšÕJç0€sÄ|Êëè0튔bÁ>“{×_F`Ø©ºê:µä,v¤ðfc1±"«ÔÍän1#=· Âøv~H½ÐßA¾¿Ü€Óš]Õ; I¾÷ç‚Qi†î¹9ywÔKG˜áñ zQY—§ÃÕZ07§X‚ Áh;ÁM)iÌCH-¯T‘ë|A0{Ò½LÚ–TâÖkÜ’dÀ“rmm»”جPF³ÖcbE§T€ÒxKºû’Ó®7±²(\4ŽÃ¸Uu@j™yĵ;³µ!Á¢b.W¤=mõ´êµK k ¸K^ÜÛ#p*Ü14qkZç5ïë †°5Ï%ÍÛ<Õ¤×Ô¥ê†C Õ´¼ú$ƒÖ“”]Ù¬qÞÚ[4©ý!ûÏ—Áb쳐XµA¬â~`›Çr¸8ìùÝ䫦<>ä÷«?xs´ÇÑ /á;¹øüÊÈÙà{"@Žïzâ¬[âß‚ U_<ÇŸ½4èN˜ú61®qŠu ¦þF£»äJ_ˆÙÎ~ ÞAã–݄ϗrŠD;xTž‘ô`É«…suãO`?³à™ô Lý#Íc5öoæØ‚y´´÷«ZR§<&JÇ+éâô´€i!Àˆ0æAoàðLèÖ-2ŸõW.’t^–(KÁmHµV@xÜÇy®Ñø­â^:Ú3w· 7½¹°ñ¸â¹®:',«Mœ—n­Á+Ãbš LÈ‘ÄnRÓÅœ%¦²‰¨ùQ:¤f‚ "PÕtô¸…cæl…&˜Ú˜Ôkv‹ž+vŠ,=¢v­6—Xy*¥t£«<™:“aîϲ=¦6rO]XI¿Œ÷¤zÚ­›¶ 6÷”w\d ü~v®ˆÌk«^m<ÿ ¢‰Õ\)ùºŽ;… lîÙÅEŠ®cѾ@vnMÏ,¼“ñ•ŽBxðÃzãÇç%3ˆ"}Ù•Åî> BÉú;Ò]V+P˜F_´ßé> Øše|ï‡ÄOmFæÇ ãqÞ$/xÐx­z`ï9"œÜij‚!7.\Td…9M‡•iŽ‹¾‘50ÞŽn¥ß4ÉôO ¹*í^QêËÜÇÌ8=ާs‰'ÂëÙ«á%Pú[O †ÅP¯Vsް.‰,kc¶ ¬A9n˜XÎ-ÞšN["¹QÕ‰ƒMýÁߺXJæÍaLj¾×Ãmã¾ãÚ uñÒþåQô¦¥ /ÄUx:‚ÍÜ’ Đ©ØÝ3V¨‰ÕnÐ6ó*óúK­«…c ¯U òhsý­jóÔj#,ímŒRµ«lbïUTŒÑ8†Ä0œÏr`ð¡¬É Ї ë"À² ™ 6¥ f¶ ¢ÚoܱԷ-<Àî)†a¶ž'Ú»¨TXqØæ¶÷YÄHy˜9ÈIW­YÀuMFë ºÏ’AqÌ4·/Ú †ô'i$øä­=Ä Ý|öK×40è|È6p‘0§)o¥ctî§H+CA-“ xØ|ÐXАç l8íºð3Ø:³¤¬KX¯UÿÙ true,'data'=>['preflight'=>true]], JSON_UNESCAPED_UNICODE); exit; } // sane defaults if not provided in config.php if (!defined('FRIDGE_PRICE')) define('FRIDGE_PRICE', 500); if (!defined('MARKET_FEE_PCT')) define('MARKET_FEE_PCT', 5); if (!defined('WATER_INTERVAL_H')) define('WATER_INTERVAL_H', 6); if (!defined('HARVEST_PARTS')) define('HARVEST_PARTS', 4); if (!defined('HARVEST_PART_INTERVAL_H')) define('HARVEST_PART_INTERVAL_H', 12); if (!defined('HARVEST_DEADLINE_H')) define('HARVEST_DEADLINE_H', 6); if (!defined('SOIL_GROWTH_BOOST_PCT')) define('SOIL_GROWTH_BOOST_PCT', 30); if (!defined('NFC_EVENT_TO_MULT')) { define('NFC_EVENT_TO_MULT', [ 'rain' => 1.20, 'drought' => 0.80, 'hail' => 0.80, 'neutral' => 1.00, ]); } // ===== ERROR → JSON ===== ini_set('display_errors', '0'); ini_set('log_errors', '1'); header_remove('X-Powered-By'); set_error_handler(function($no,$str,$file,$line){ throw new ErrorException($str, 0, $no, $file, $line); }); set_exception_handler(function($e){ @file_put_contents(__DIR__.'/app.log', json_encode([ 'ts'=>date('c'),'type'=>'php.exception', 'data'=>['msg'=>$e->getMessage(),'file'=>$e->getFile(),'line'=>$e->getLine()] ], JSON_UNESCAPED_UNICODE).PHP_EOL, FILE_APPEND); http_response_code(500); echo json_encode(['ok'=>false,'error'=>'SERVER_ERROR','message'=>$e->getMessage()], JSON_UNESCAPED_UNICODE); exit; }); register_shutdown_function(function(){ $e = error_get_last(); if ($e && in_array($e['type'], [E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR], true)) { @file_put_contents(__DIR__.'/app.log', json_encode(['ts'=>date('c'),'type'=>'php.fatal','data'=>$e], JSON_UNESCAPED_UNICODE).PHP_EOL, FILE_APPEND); http_response_code(500); echo json_encode(['ok'=>false,'error'=>'FATAL','message'=>$e['message'] ?? 'fatal'], JSON_UNESCAPED_UNICODE); } }); // ===== Test ping ===== if (($_GET['action'] ?? '') === 'ping') { echo json_encode(['ok'=>true,'data'=>['pong'=>true,'time'=>gmdate('c')]], JSON_UNESCAPED_UNICODE); exit; } // ===== Requires ===== require_once __DIR__.'/config.php'; require_once __DIR__.'/db.php'; require_once __DIR__.'/utils.php'; require_once __DIR__.'/logger.php'; // ===== Auth ===== $init = $_SERVER['HTTP_X_INIT_DATA'] ?? ($_GET['init_data'] ?? ''); if (!validate_webapp_initdata($init)) { log_event('api.denied', ['reason'=>'bad_initdata']); json_err('ERR_NO_INITDATA', 401); } // DEV fallback user id $uid = (int)($_GET['user_id'] ?? 0); if ($uid <= 0) $uid = 1001; $action = $_GET['action'] ?? 'state'; $settings = load_settings(); $pdo = db(); ensure_user($pdo, $uid, $settings); // ===== Router ===== switch ($action) { case 'state': json_ok(build_state($pdo, $uid, $settings)); break; // Plots case 'plots.list': json_ok(['plots'=>get_plots($pdo, $uid)]); break; case 'plots.buy': json_ok(plots_buy($pdo, $uid, $settings)); break; case 'soil.buy': json_ok(soil_buy($pdo, $uid, $settings)); break; case 'plots.plant': json_ok(plots_plant($pdo, $uid, $_POST)); break; case 'plots.water': json_ok(plots_water($pdo, $uid, $_POST)); break; case 'harvest.take': json_ok(harvest_take($pdo, $uid, $_POST, $settings)); break; // Storage case 'storage.autosort': json_ok(storage_autosort($pdo, $uid)); break; case 'storage.move_fridge': json_ok(storage_move_to_fridge($pdo, $uid)); break; case 'storage.buy_fridge': json_ok(storage_buy_fridge($pdo, $uid, $settings)); break; // Market case 'market.list': json_ok(['items'=>market_list($pdo)]); break; case 'market.sell': json_ok(market_sell($pdo, $uid, read_json_body())); break; case 'market.buy': json_ok(market_buy($pdo, $uid, read_json_body())); break; case 'market.my': json_ok(['items'=>market_my($pdo, $uid)]); break; case 'market.cancel': json_ok(market_cancel($pdo, $uid, read_json_body())); break; case 'market.trades': json_ok(market_trades($pdo, $uid)); break; // Profile case 'profile.daily_claim': json_ok(profile_daily_claim($pdo, $uid, $settings)); break; case 'nfc.prices': json_ok(['prices'=>nfc_prices_for($pdo, (int)user_snt($pdo, $uid))]); break; case 'nfc.sell': json_ok(nfc_sell($pdo, $uid, read_json_body())); break; // продать урожай в NFC case 'nfc.buy_seed': json_ok(nfc_buy_seed($pdo, $uid, read_json_body())); break; // если хотите закупать у NFC case 'nfc.roll_daily': json_ok(nfc_roll_daily_for_user_snt($pdo, $uid)); break; default: json_err('Unknown action', 404); } // ====================== Domain ====================== function user_snt(PDO $pdo, int $uid): int { if (!has_column($pdo,'users','snt_id')) return 0; $st = $pdo->prepare("SELECT snt_id FROM users WHERE id=?"); $st->execute([$uid]); return (int)$st->fetchColumn(); } function nfc_roll_daily_for_user_snt(PDO $pdo, int $uid): array { $snt_id = user_snt($pdo, $uid); if ($snt_id <= 0) return ['error'=>'NO_SNT']; return nfc_roll_daily_for_snt($pdo, $snt_id); } /** * Выставляет на ближайшие 24 часа новый множитель цен NFC по СНТ * Правило: одно случайное событие из {'rain','drought','hail','neutral'} * и для всех позиций в nfc_catalog обновляем mult (0.8/1.0/1.2). */ function nfc_roll_daily_for_snt(PDO $pdo, int $snt_id): array { // Выбираем событие $kinds = ['rain','drought','hail','neutral']; $kind = $kinds[array_rand($kinds)]; $mult = (float)(NFC_EVENT_TO_MULT[$kind] ?? 1.0); try{ $pdo->beginTransaction(); // Закрыть старые цены (если есть valid_to IS NULL) $pdo->prepare("UPDATE nfc_prices SET valid_to=NOW() WHERE snt_id=? AND valid_to IS NULL") ->execute([$snt_id]); // Создать новые записи по всем товарам каталога $st = $pdo->query("SELECT name FROM nfc_catalog"); $names = $st->fetchAll(PDO::FETCH_COLUMN); $ins = $pdo->prepare("INSERT INTO nfc_prices (snt_id, name, mult, valid_from) VALUES (?,?,?,NOW())"); foreach ($names as $nm) { $ins->execute([$snt_id, $nm, $mult]); } // Лог события $pdo->prepare("INSERT INTO snt_events (snt_id,kind,mult) VALUES (?,?,?)") ->execute([$snt_id, $kind, $mult]); $pdo->commit(); log_event('nfc.roll_daily',['snt'=>$snt_id,'kind'=>$kind,'mult'=>$mult]); return ['success'=>true,'kind'=>$kind,'mult'=>$mult]; }catch(Throwable $e){ if ($pdo->inTransaction()) $pdo->rollBack(); log_event('nfc.roll_daily.error',['e'=>$e->getMessage()]); return ['error'=>'SERVER_ERROR']; } } function nfc_sell(PDO $pdo, int $uid, array $body): array { $snt_id = user_snt($pdo, $uid); if ($snt_id <= 0) return ['error'=>'NO_SNT']; $itemId = (int)($body['item_id'] ?? 0); $name = trim($body['name'] ?? ''); $qty = (float)($body['qty'] ?? 0); if ($qty <= 0) return ['error'=>'BAD_QTY']; // Определяем товар if ($itemId > 0) { $st = $pdo->prepare("SELECT id,name,qty FROM storage_items WHERE id=? AND user_id=? AND qty>0 FOR UPDATE"); $findBy = 'id'; $findVal= $itemId; } else { if ($name==='') return ['error'=>'NO_ITEM']; $st = $pdo->prepare("SELECT id,name,qty FROM storage_items WHERE user_id=? AND name=? AND qty>0 ORDER BY id FOR UPDATE"); $findBy = 'name'; $findVal= $name; } try{ $pdo->beginTransaction(); if ($findBy==='id') { $st->execute([$itemId,$uid]); $it = $st->fetch(); if (!$it) { $pdo->rollBack(); return ['error'=>'ITEM_NOT_FOUND']; } } else { $st->execute([$uid,$name]); $it = $st->fetch(); if (!$it) { $pdo->rollBack(); return ['error'=>'ITEM_NOT_FOUND']; } } $name = $it['name']; if ($qty > (float)$it['qty']) { $pdo->rollBack(); return ['error'=>'NO_INVENTORY']; } // Получаем текущую цену NFC: base_buy * mult $price = nfc_price_for_item($pdo, $snt_id, $name, 'buy'); if ($price <= 0) { $pdo->rollBack(); return ['error'=>'NO_PRICE']; } $sum = (int)round($price * $qty); // списываем со склада, начисляем баланс $pdo->prepare("UPDATE storage_items SET qty=qty-? WHERE id=?")->execute([$qty,(int)$it['id']]); $pdo->prepare("UPDATE users SET balance=balance+? WHERE id=?")->execute([$sum,$uid]); $pdo->commit(); log_event('nfc.sell', ['uid'=>$uid,'snt'=>$snt_id,'name'=>$name,'qty'=>$qty,'price'=>$price,'sum'=>$sum]); return ['success'=>true,'sum'=>$sum,'price'=>$price]; }catch(Throwable $e){ if ($pdo->inTransaction()) $pdo->rollBack(); log_event('nfc.sell.error',['e'=>$e->getMessage()]); return ['error'=>'SERVER_ERROR']; } } function nfc_price_for_item(PDO $pdo, int $snt_id, string $name, string $kind): int { $st = $pdo->prepare("SELECT c.base_buy, c.base_sell, p.mult FROM nfc_catalog c LEFT JOIN nfc_prices p ON p.snt_id=? AND p.name=c.name WHERE c.name=?"); $st->execute([$snt_id, $name]); $r = $st->fetch(); if (!$r) return 0; $mult = (float)($r['mult'] ?? 1.0); if ($kind === 'buy') return (int)round((int)$r['base_buy'] * $mult); if ($kind === 'sell') return (int)round((int)$r['base_sell'] * $mult); return 0; } function nfc_buy_seed(PDO $pdo, int $uid, array $body): array { $snt_id = user_snt($pdo, $uid); if ($snt_id <= 0) return ['error'=>'NO_SNT']; $name = trim($body['name'] ?? ''); $qty = (float)($body['qty'] ?? 0); if ($name==='' || $qty<=0) return ['error'=>'BAD_PAYLOAD']; $price = nfc_price_for_item($pdo, $snt_id, $name, 'sell'); if ($price <= 0) return ['error'=>'NO_PRICE']; $sum = (int)round($price * $qty); try{ $pdo->beginTransaction(); // баланс $st = $pdo->prepare("SELECT balance FROM users WHERE id=? FOR UPDATE"); $st->execute([$uid]); $bal=(int)$st->fetchColumn(); if ($bal < $sum) { $pdo->rollBack(); return ['error'=>'NO_BALANCE']; } // списываем деньги, кладём на склад $pdo->prepare("UPDATE users SET balance=balance-? WHERE id=?")->execute([$sum,$uid]); $pdo->prepare("INSERT INTO storage_items (user_id,name,qty,place,fresh_pct) VALUES (?,?,?,?,?)") ->execute([$uid,$name,$qty,'cellar',100]); $pdo->commit(); log_event('nfc.buy_seed',['uid'=>$uid,'snt'=>$snt_id,'name'=>$name,'qty'=>$qty,'price'=>$price,'sum'=>$sum]); return ['success'=>true,'sum'=>$sum,'price'=>$price]; }catch(Throwable $e){ if ($pdo->inTransaction()) $pdo->rollBack(); log_event('nfc.buy_seed.error',['e'=>$e->getMessage()]); return ['error'=>'SERVER_ERROR']; } } function ensure_user(PDO $pdo, int $uid, array $settings): void { $st = $pdo->prepare("SELECT id FROM users WHERE id=?"); $st->execute([$uid]); if ($st->fetch()) return; $cellar = (int)$settings['cellar_capacity']; if (has_column($pdo,'users','snt_id')) { $snt_id = (int)$pdo->query("SELECT id FROM snts ORDER BY RAND() LIMIT 1")->fetchColumn(); $ins = $pdo->prepare("INSERT INTO users (id, balance, cellar_cap, snt_id) VALUES (?, 0, ?, ?)"); $ins->execute([$uid, $cellar, $snt_id ?: null]); } else { $ins = $pdo->prepare("INSERT INTO users (id, balance, cellar_cap) VALUES (?, 0, ?)"); $ins->execute([$uid, $cellar]); } $pdo->prepare("INSERT INTO plots (user_id,status) VALUES (?, 'empty'), (?, 'empty')")->execute([$uid,$uid]); } function build_state(PDO $pdo, int $uid, array $settings): array { if (has_column($pdo,'users','snt_id')) { $user = $pdo->query("SELECT id, balance, cellar_cap, last_daily_at, snt_id FROM users WHERE id=".$uid)->fetch(); } else { $user = $pdo->query("SELECT id, balance, cellar_cap, last_daily_at FROM users WHERE id=".$uid)->fetch(); $user['snt_id'] = null; } $snt = ($user && $user['snt_id']) ? snt_meta($pdo, (int)$user['snt_id']) : null; $nfcPrices = has_column($pdo,'nfc_prices','mult') ? nfc_prices_for($pdo, (int)($user['snt_id'] ?? 0)) : []; return [ 'server_ts' => time(), 'user' => $user, 'snt' => $snt, 'nfc' => ['prices'=>$nfcPrices], 'settings' => $settings, 'plots' => get_plots($pdo, $uid), 'cellar' => cellar_meta($pdo, $uid), 'fridges' => fridges_list($pdo, $uid), 'storage' => storage_list($pdo, $uid), 'market' => market_list($pdo), 'refs' => refs_list($pdo, $uid), 'quests' => quests_flags($pdo, $uid), ]; } function snt_meta(PDO $pdo, int $snt_id): ?array { $st = $pdo->prepare("SELECT id,code,name FROM snts WHERE id=?"); $st->execute([$snt_id]); $row = $st->fetch(); return $row ?: null; } function nfc_prices_for(PDO $pdo, int $snt_id): array { if ($snt_id <= 0) return []; // Собираем текущие цены: base * mult $st = $pdo->prepare("SELECT c.name, c.base_buy, c.base_sell, p.mult FROM nfc_catalog c LEFT JOIN nfc_prices p ON p.snt_id=? AND p.name=c.name"); $st->execute([$snt_id]); $rows = $st->fetchAll(); $out = []; foreach ($rows as $r) { $mult = (float)($r['mult'] ?? 1.0); $out[] = [ 'name' => $r['name'], 'buy_price' => (int)round((int)$r['base_buy'] * $mult), // сколько NFC платит игроку 'sell_price'=> (int)round((int)$r['base_sell'] * $mult), // сколько NFC продаёт игроку (если используете) 'mult' => $mult, ]; } return $out; } function get_plots(PDO $pdo, int $uid): array { $st = $pdo->prepare("SELECT * FROM plots WHERE user_id=? ORDER BY id"); $st->execute([$uid]); $rows = $st->fetchAll(); $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); foreach ($rows as &$r) { $r['canWater'] = false; $r['canHarvest'] = false; $r['nextWaterSec'] = null; $r['deadlineSec'] = null; // Полив if ($r['status']==='growing' || $r['status']==='dry') { if (empty($r['next_water_at'])) { $r['canWater'] = true; } else { $nwa = new DateTimeImmutable($r['next_water_at']); if ($nwa <= $now) $r['canWater'] = true; else $r['nextWaterSec'] = max(0, $nwa->getTimestamp() - $now->getTimestamp()); } } // Сбор по окнам if ($r['status']==='ready') { $start = !empty($r['harvest_start_at']) ? new DateTimeImmutable($r['harvest_start_at']) : null; $ddl = !empty($r['harvest_deadline']) ? new DateTimeImmutable($r['harvest_deadline']) : null; if ($start && $ddl) { if ($now < $start) { $r['canHarvest'] = false; $r['deadlineSec'] = max(0, $start->getTimestamp() - $now->getTimestamp()); } elseif ($now >= $start && $now <= $ddl) { $r['canHarvest'] = true; $r['deadlineSec'] = max(0, $ddl->getTimestamp() - $now->getTimestamp()); } else { $r['canHarvest'] = false; $r['deadlineSec'] = 0; } } else { $r['canHarvest'] = true; // запасной путь $r['deadlineSec'] = 0; } } if ($r['status']==='empty') { $r['crop'] = $r['crop'] ?: 'Свободно'; } } return $rows; } function plots_buy(PDO $pdo, int $uid, array $settings): array { $price = (int)$settings['plot_price']; $pdo->beginTransaction(); $bal = (int)$pdo->query("SELECT balance FROM users WHERE id={$uid} FOR UPDATE")->fetchColumn(); if ($bal < $price) { $pdo->rollBack(); return ['error'=>'NO_BALANCE']; } $pdo->exec("INSERT INTO plots (user_id,status) VALUES ($uid,'empty')"); $pdo->exec("UPDATE users SET balance=balance-{$price} WHERE id={$uid}"); $pdo->commit(); log_event('plots.buy', ['uid'=>$uid,'price'=>$price]); return ['success'=>true]; } function soil_buy(PDO $pdo, int $uid, array $settings): array { $price = (int)$settings['soil_price']; $st = $pdo->prepare("SELECT id FROM plots WHERE user_id=? AND status IN ('growing','dry') AND soil_boost=0 ORDER BY id LIMIT 1"); $st->execute([$uid]); $row = $st->fetch(); if (!$row) return ['error'=>'NO_GROWING_PLOT']; $plotId = (int)$row['id']; $pdo->beginTransaction(); $bal = (int)$pdo->query("SELECT balance FROM users WHERE id={$uid} FOR UPDATE")->fetchColumn(); if ($bal < $price) { $pdo->rollBack(); return ['error'=>'NO_BALANCE']; } $pdo->exec("UPDATE users SET balance=balance-{$price} WHERE id={$uid}"); $pdo->prepare("UPDATE plots SET soil_boost=1 WHERE id=? AND user_id=?")->execute([$plotId,$uid]); $pdo->commit(); log_event('soil.buy', ['uid'=>$uid,'plot_id'=>$plotId,'price'=>$price]); return ['success'=>true,'plot_id'=>$plotId]; } function plots_plant(PDO $pdo, int $uid, array $post): array { $plotId = (int)($post['plot_id'] ?? 0); $crop = trim($post['crop'] ?? 'Картофель'); if ($plotId <= 0) return ['error'=>'BAD_PLOT']; $st = $pdo->prepare("UPDATE plots SET status='growing', crop=?, progress=0, harvest_part=NULL, next_water_at=UTC_TIMESTAMP(), harvest_deadline=NULL, harvest_start_at=NULL, soil_boost=soil_boost WHERE id=? AND user_id=? AND status='empty'"); $st->execute([$crop, $plotId, $uid]); if ($st->rowCount() === 0) return ['error'=>'NOT_EMPTY']; log_event('plots.plant', ['uid'=>$uid,'plot_id'=>$plotId,'crop'=>$crop]); return ['success'=>true]; } function plots_water(PDO $pdo, int $uid, array $post): array { $plotId = (int)($post['plot_id'] ?? 0); if ($plotId <= 0) return ['error'=>'BAD_PLOT']; $pdo->beginTransaction(); $st = $pdo->prepare("SELECT * FROM plots WHERE id=? AND user_id=? FOR UPDATE"); $st->execute([$plotId, $uid]); $p = $st->fetch(); if (!$p) { $pdo->rollBack(); return ['error'=>'NOT_FOUND']; } if (!in_array($p['status'], ['growing','dry'], true)) { $pdo->rollBack(); return ['error'=>'BAD_STATUS']; } $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); $nwa = empty($p['next_water_at']) ? null : new DateTimeImmutable($p['next_water_at']); if ($nwa && $nwa > $now) { $pdo->rollBack(); return ['error'=>'TOO_EARLY']; } $progress = min(100, (int)$p['progress'] + 25); if ($progress >= 100) { // созрело: первое окно открыто сейчас, дедлайн 6ч $hp = 0; // частей собрано $start = $now; $deadline = (clone $start)->modify('+'.HARVEST_DEADLINE_H.' hours')->format('Y-m-d H:i:s'); $upd = $pdo->prepare("UPDATE plots SET status='ready', progress=100, harvest_part=?, harvest_start_at=?, harvest_deadline=?, next_water_at=NULL WHERE id=?"); $upd->execute([$hp, $start->format('Y-m-d H:i:s'), $deadline, $plotId]); } else { $hours = WATER_INTERVAL_H; if ((int)$p['soil_boost'] === 1) { $hours = max(1, (int)round($hours * (100 - SOIL_GROWTH_BOOST_PCT) / 100)); } $next = $now->modify('+'.$hours.' hours')->format('Y-m-d H:i:s'); $upd = $pdo->prepare("UPDATE plots SET status='growing', progress=?, next_water_at=? WHERE id=?"); $upd->execute([$progress, $next, $plotId]); } $pdo->commit(); log_event('plots.water', ['uid'=>$uid,'plot_id'=>$plotId,'progress'=>$progress]); return ['success'=>true,'progress'=>$progress]; } function harvest_take(PDO $pdo, int $uid, array $post, array $settings): array { $plotId = (int)($post['plot_id'] ?? 0); if ($plotId <= 0) return ['error'=>'BAD_PLOT']; $pdo->beginTransaction(); $st = $pdo->prepare("SELECT * FROM plots WHERE id=? AND user_id=? FOR UPDATE"); $st->execute([$plotId, $uid]); $p = $st->fetch(); if (!$p || $p['status']!=='ready') { $pdo->rollBack(); return ['error'=>'NOT_READY']; } $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); $start = !empty($p['harvest_start_at']) ? new DateTimeImmutable($p['harvest_start_at']) : null; $ddl = !empty($p['harvest_deadline']) ? new DateTimeImmutable($p['harvest_deadline']) : null; if ($start && $ddl) { if ($now < $start) { $pdo->rollBack(); return ['error'=>'WINDOW_NOT_OPEN']; } if ($now > $ddl) { // окно пропущено → потеря части и сдвиг окна $missed = (int)$p['harvest_part'] + 1; if ($missed >= HARVEST_PARTS) { $pdo->prepare("UPDATE plots SET status='empty', crop=NULL, progress=0, harvest_part=NULL, harvest_deadline=NULL, harvest_start_at=NULL, soil_boost=0 WHERE id=?")->execute([$plotId]); $pdo->commit(); return ['success'=>true,'lost'=>true,'finished'=>true]; } else { $nextStart = $start->modify('+'.HARVEST_PART_INTERVAL_H.' hours'); $nextDdl = $nextStart->modify('+'.HARVEST_DEADLINE_H.' hours'); $pdo->prepare("UPDATE plots SET harvest_part=?, harvest_start_at=?, harvest_deadline=? WHERE id=?") ->execute([$missed, $nextStart->format('Y-m-d H:i:s'), $nextDdl->format('Y-m-d H:i:s'), $plotId]); $pdo->commit(); return ['success'=>true,'lost'=>true,'part'=>$missed]; } } } // Выдаём 1/4 урожая в склад $yield_full = 8.0; // общий урожай за грядку (можно вынести в настройки) $qty = $yield_full / HARVEST_PARTS; $pdo->prepare("INSERT INTO storage_items (user_id,name,qty,place,fresh_pct) VALUES (?,?,?,?,?)") ->execute([$uid, $p['crop'] ?: 'Урожай', $qty, 'cellar', 100]); $part = (int)$p['harvest_part'] + 1; if ($part >= HARVEST_PARTS) { $pdo->prepare("UPDATE plots SET status='empty', crop=NULL, progress=0, harvest_part=NULL, harvest_deadline=NULL, harvest_start_at=NULL, soil_boost=0 WHERE id=?")->execute([$plotId]); $pdo->commit(); log_event('harvest.finish', ['uid'=>$uid,'plot_id'=>$plotId,'qty_total'=>$yield_full]); return ['success'=>true,'finished'=>true,'qty'=>round($qty,3)]; } else { $curStart = $start ?: $now; $nextStart = $curStart->modify('+'.HARVEST_PART_INTERVAL_H.' hours'); $nextDdl = $nextStart->modify('+'.HARVEST_DEADLINE_H.' hours'); $pdo->prepare("UPDATE plots SET harvest_part=?, harvest_start_at=?, harvest_deadline=? WHERE id=?") ->execute([$part, $nextStart->format('Y-m-d H:i:s'), $nextDdl->format('Y-m-d H:i:s'), $plotId]); $pdo->commit(); log_event('harvest.part', ['uid'=>$uid,'plot_id'=>$plotId,'part'=>$part,'qty'=>round($qty,3)]); return ['success'=>true,'part'=>$part,'qty'=>round($qty,3)]; } } // ===== Storage function cellar_meta(PDO $pdo, int $uid): array { $used = (float)$pdo->query("SELECT COALESCE(SUM(qty),0) FROM storage_items WHERE user_id={$uid} AND place='cellar'")->fetchColumn(); $cap = (int)$pdo->query("SELECT cellar_cap FROM users WHERE id={$uid}")->fetchColumn(); return ['used'=>round($used,3),'cap'=>$cap]; } function fridges_list(PDO $pdo, int $uid): array { $st = $pdo->prepare("SELECT * FROM fridges WHERE user_id=? ORDER BY id"); $st->execute([$uid]); return $st->fetchAll(); } function storage_list(PDO $pdo, int $uid): array { $st = $pdo->prepare("SELECT * FROM storage_items WHERE user_id=? ORDER BY id DESC"); $st->execute([$uid]); return $st->fetchAll(); } function storage_buy_fridge(PDO $pdo, int $uid, array $settings): array { $price = (int)FRIDGE_PRICE; $cap = (int)$settings['fridge_capacity']; $pdo->beginTransaction(); $st = $pdo->prepare("SELECT balance FROM users WHERE id=? FOR UPDATE"); $st->execute([$uid]); $bal = (int)$st->fetchColumn(); if ($bal < $price) { $pdo->rollBack(); return ['error'=>'NO_BALANCE']; } $pdo->prepare("UPDATE users SET balance=balance-? WHERE id=?")->execute([$price,$uid]); $pdo->prepare("INSERT INTO fridges (user_id, used, cap) VALUES (?,?,?)")->execute([$uid, 0, $cap]); $pdo->commit(); log_event('storage.buy_fridge', ['uid'=>$uid,'price'=>$price,'cap'=>$cap]); return ['success'=>true]; } function storage_autosort(PDO $pdo, int $uid): array { log_event('storage.autosort', ['uid'=>$uid]); return ['success'=>true]; } function storage_move_to_fridge(PDO $pdo, int $uid): array { $st = $pdo->prepare("SELECT id, qty FROM storage_items WHERE user_id=? AND place='cellar' AND qty>0 ORDER BY id DESC LIMIT 1"); $st->execute([$uid]); if (!$row = $st->fetch()) return ['error'=>'EMPTY']; $fst = $pdo->prepare("SELECT id, used, cap FROM fridges WHERE user_id=? ORDER BY id LIMIT 1"); $fst->execute([$uid]); $fr = $fst->fetch(); if (!$fr) return ['error'=>'NO_FRIDGE']; $free = (float)$fr['cap'] - (float)$fr['used']; $qty = (float)$row['qty']; if ($free <= 0) return ['error'=>'FRIDGE_FULL']; $move = min($qty, $free); if ($move <= 0) return ['error'=>'NOTHING_TO_MOVE']; $pdo->beginTransaction(); $pdo->prepare("UPDATE storage_items SET qty=qty-? WHERE id=?")->execute([$move,(int)$row['id']]); $pdo->prepare("INSERT INTO storage_items (user_id,name,qty,place,fresh_pct) SELECT user_id,name,?, CONCAT('fridge#',?), fresh_pct FROM storage_items WHERE id=?") ->execute([$move,(int)$fr['id'],(int)$row['id']]); $pdo->prepare("UPDATE fridges SET used=used+? WHERE id=?")->execute([$move,(int)$fr['id']]); $pdo->commit(); log_event('storage.move_to_fridge', ['uid'=>$uid,'item_id'=>$row['id'],'fridge'=>$fr['id'],'moved'=>$move]); return ['success'=>true,'fridge'=>$fr['id'],'moved'=>$move]; } // ===== Market function market_list(PDO $pdo): array { return $pdo->query("SELECT id,name,qty,price,user_id FROM market_offers ORDER BY id DESC LIMIT 100")->fetchAll(); } function market_sell(PDO $pdo, int $uid, array $body): array { $name = trim($body['name'] ?? ''); $qty = (float)($body['qty'] ?? 0); $price= (float)($body['price'] ?? 0); if ($name==='' || $qty<=0 || $price<=0) return ['error'=>'BAD_PAYLOAD']; try { $pdo->beginTransaction(); // Резерв со склада (FIFO) $st = $pdo->prepare("SELECT id, qty FROM storage_items WHERE user_id=? AND name=? AND qty>0 ORDER BY id FOR UPDATE"); $st->execute([$uid, $name]); $remain = $qty; foreach ($st->fetchAll() as $row) { if ($remain <= 0) break; $take = min($remain, (float)$row['qty']); $pdo->prepare("UPDATE storage_items SET qty = qty - ? WHERE id=?")->execute([$take, (int)$row['id']]); $remain -= $take; } if ($remain > 0) { $pdo->rollBack(); return ['error'=>'NO_INVENTORY']; } $pdo->prepare("INSERT INTO market_offers (user_id,name,qty,price) VALUES (?,?,?,?)") ->execute([$uid,$name,$qty,$price]); $pdo->commit(); log_event('market.sell', ['uid'=>$uid,'name'=>$name,'qty'=>$qty,'price'=>$price]); return ['success'=>true]; } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); log_event('market.sell.error', ['e'=>$e->getMessage()]); return ['error'=>'SERVER_ERROR']; } } function market_buy(PDO $pdo, int $uid, array $body): array { $id = (int)($body['offer_id'] ?? 0); if ($id <= 0) return ['error' => 'BAD_ID']; $fee_pct = MARKET_FEE_PCT; try { $pdo->beginTransaction(); // Лот $st = $pdo->prepare("SELECT * FROM market_offers WHERE id=? FOR UPDATE"); $st->execute([$id]); $off = $st->fetch(); if (!$off) { $pdo->rollBack(); return ['error' => 'NOT_FOUND']; } $seller_id = (int)$off['user_id']; if ($seller_id === $uid) { $pdo->rollBack(); return ['error' => 'ERR_OWN_OFFER']; } $qty = (float)$off['qty']; $price = (float)$off['price']; $sum = (int)round($qty * $price); if ($sum <= 0) { $pdo->rollBack(); return ['error'=>'BAD_SUM']; } $fee = (int)round($sum * $fee_pct / 100.0); $proceeds = $sum - $fee; // Баланс покупателя $stb = $pdo->prepare("SELECT balance FROM users WHERE id=? FOR UPDATE"); $stb->execute([$uid]); $buyer_bal = (int)$stb->fetchColumn(); if ($buyer_bal < $sum) { $pdo->rollBack(); return ['error' => 'NO_BALANCE']; } // Денежные движения $pdo->prepare("UPDATE users SET balance = balance - ? WHERE id=?")->execute([$sum, $uid]); $pdo->prepare("UPDATE users SET balance = balance + ? WHERE id=?")->execute([$proceeds, $seller_id]); // Товар покупателю $pdo->prepare("INSERT INTO storage_items (user_id,name,qty,place,fresh_pct) VALUES (?,?,?,?,?)") ->execute([$uid, $off['name'], $qty, 'cellar', 100]); // Запись сделки $pdo->prepare("INSERT INTO market_trades (offer_id,buyer_id,seller_id,name,qty,price,`sum`,fee,proceeds) VALUES (?,?,?,?,?,?,?,?,?)") ->execute([$id, $uid, $seller_id, $off['name'], $qty, $price, $sum, $fee, $proceeds]); // Удаляем лот $pdo->prepare("DELETE FROM market_offers WHERE id=?")->execute([$id]); $pdo->commit(); log_event('market.buy', [ 'buyer'=>$uid,'seller'=>$seller_id,'offer_id'=>$id, 'qty'=>$qty,'price'=>$price,'sum'=>$sum,'fee'=>$fee,'proceeds'=>$proceeds ]); return ['success'=>true,'sum'=>$sum,'fee'=>$fee,'proceeds'=>$proceeds]; } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); log_event('market.buy.error', ['e'=>$e->getMessage(), 'offer_id'=>$id]); return ['error' => 'SERVER_ERROR']; } } function market_my(PDO $pdo, int $uid): array { $st=$pdo->prepare("SELECT id,name,qty,price,created_at FROM market_offers WHERE user_id=? ORDER BY id DESC"); $st->execute([$uid]); return $st->fetchAll(); } function market_cancel(PDO $pdo, int $uid, array $body): array { $id=(int)($body['offer_id']??0); if($id<=0) return ['error'=>'BAD_ID']; try{ $pdo->beginTransaction(); $st=$pdo->prepare("SELECT * FROM market_offers WHERE id=? FOR UPDATE"); $st->execute([$id]); $off=$st->fetch(); if(!$off){ $pdo->rollBack(); return ['error'=>'NOT_FOUND']; } if((int)$off['user_id']!==$uid){ $pdo->rollBack(); return ['error'=>'FORBIDDEN']; } $pdo->prepare("INSERT INTO storage_items (user_id,name,qty,place,fresh_pct) VALUES (?,?,?,?,?)") ->execute([$uid,$off['name'],(float)$off['qty'],'cellar',100]); $pdo->prepare("DELETE FROM market_offers WHERE id=?")->execute([$id]); $pdo->commit(); log_event('market.cancel',['uid'=>$uid,'offer_id'=>$id]); return ['success'=>true]; }catch(Throwable $e){ if($pdo->inTransaction()) $pdo->rollBack(); log_event('market.cancel.error',['e'=>$e->getMessage(),'offer_id'=>$id]); return ['error'=>'SERVER_ERROR']; } } function market_trades(PDO $pdo, int $uid): array { $role = $_GET['role'] ?? 'all'; // 'buyer'|'seller'|'all' $limit = max(1, min(200, (int)($_GET['limit'] ?? 50))); if ($role === 'buyer') { $st = $pdo->prepare("SELECT * FROM market_trades WHERE buyer_id=? ORDER BY id DESC LIMIT {$limit}"); $st->execute([$uid]); } elseif ($role === 'seller') { $st = $pdo->prepare("SELECT * FROM market_trades WHERE seller_id=? ORDER BY id DESC LIMIT {$limit}"); $st->execute([$uid]); } else { $st = $pdo->prepare("SELECT * FROM market_trades WHERE buyer_id=? OR seller_id=? ORDER BY id DESC LIMIT {$limit}"); $st->execute([$uid, $uid]); } return ['items'=>$st->fetchAll()]; } // ===== Social / quests / bonus function refs_list(PDO $pdo, int $uid): array { // Заглушка return []; } function quests_flags(PDO $pdo, int $uid): array { return [ ['id'=>'q1','code'=>'connect_ton','title'=>'Подключите TON-кошелёк','reward'=>'+500 BABKI','done'=>0], ['id'=>'q2','code'=>'water_4','title'=>'Полейте 4 раза за день','reward'=>'+50 BABKI','done'=>0], ['id'=>'q3','code'=>'harvest_4','title'=>'Соберите 4 части урожая','reward'=>'+100 BABKI','done'=>0], ]; } function has_column(PDO $pdo, string $table, string $column): bool { static $cache = []; $key = $table.'.'.$column; if (isset($cache[$key])) return $cache[$key]; $st = $pdo->prepare("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?"); $st->execute([$table, $column]); return $cache[$key] = ((int)$st->fetchColumn() > 0); } function profile_daily_claim(PDO $pdo, int $uid, array $settings): array { $bonus = (int)$settings['daily_bonus']; $st = $pdo->prepare("SELECT last_daily_at FROM users WHERE id=?"); $st->execute([$uid]); $last = $st->fetchColumn(); $now = new DateTimeImmutable('now', new DateTimeZone('UTC')); $can = true; if ($last) { $ld = new DateTimeImmutable($last); $can = ($now->getTimestamp() - $ld->getTimestamp()) >= 86400; } if (!$can) return ['error'=>'ALREADY_TAKEN']; $pdo->prepare("UPDATE users SET balance=balance+?, last_daily_at=UTC_TIMESTAMP() WHERE id=?")->execute([$bonus,$uid]); log_event('daily.claim', ['uid'=>$uid,'bonus'=>$bonus]); return ['success'=>true,'bonus'=>$bonus]; }