Last fix for Hero T4 & V11

/*
|--------------------------------------------------------------------------
| FIX List
|--------------------------------------------------------------------------
|
| 1. Hero Attribute Points
|    - When adding attribute points to the hero, the page currently refreshes
|      after every click on the "+" button.
|    - Implement a faster allocation method (AJAX or similar) so multiple
|      points can be assigned without a full page refresh.
|    - The implementation must remain secure and not be exploitable
|      (validate requests server-side, prevent double submissions,
|      verify available points, etc.).
|
| 2. Hero Equipment Restrictions (Original Travian Behavior)
|    - Prevent equipping or unequipping hero items while the hero is:
|        • On an adventure.
|        • Attacking (alone or with troops).
|        • Away as reinforcements.
|
| 3. Oasis Navigation
|    - When opening an oasis (37_land.tpl), the 37_t4nav.tpl navigation
|      disappears.
|    - Include the navigation menu on the oasis page as well.
|
| 4. Auction House Redesign
|    - Redesign 37_auction.tpl to match the Merchant.png mockup.
|    - Keep the existing auction functionality exactly as it is.
|    - Only improve the UI/UX.
|    - You may crop and reuse graphics from Merchant.png.
|    - Add Gold ⇄ Silver exchange functionality.
|
| 5. Hero Item Bonuses (Highest Priority)
|    - Verify that hero equipment bonuses are applied correctly.
|    - Example:
|        • Helmet of the Ruler should reduce Barracks training time by 20%.
|        • Currently, training time remains unchanged whether the helmet
|          is equipped or not.
|    - Also verify:
|        • Stable training speed bonuses.
|        • Boots bonuses.
|        • Any other related hero equipment effects.
|
| 6. Adventures Page Redesign
|    - Redesign 37_adventures.tpl to match the Adventure.png mockup.
|    - Keep the "Expires in" column intact.
|
*/
This commit is contained in:
novgorodschi catalin
2026-07-23 15:16:51 +03:00
parent 34e3d88e39
commit 1e19f117ca
19 changed files with 620 additions and 29 deletions
+1
View File
@@ -92,6 +92,7 @@ $editIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke=
<tr><td><?php echo CONF_SERV_PEACESYST ?> <em class="tooltip">?<span class="classic"><?php echo CONF_SERV_PEACESYST_TOOLTIP ?></span></em></td><td><?php echo (["None", "Normal", "Christmas", "New Year", "Easter"])[PEACE]; ?></td></tr>
<tr><td><?php echo CONF_SERV_GRAPHICPACK ?> <em class="tooltip">?<span class="classic"><?php echo CONF_SERV_GRAPHICPACK_TOOLTIP ?></span></em></td><td><?php echo GP_ENABLE ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
<tr><td><?php echo CONF_SERV_ERRORREPORT ?> <em class="tooltip">?<span class="classic"><?php echo CONF_SERV_ERRORREPORT_TOOLTIP ?></span></em></td><td><b><?php echo (ERROR_REPORT=="error_reporting (0);")? "No": "Yes";?></b></td></tr>
<tr><td>Hero base regeneration <em class="tooltip">?<span class="classic">Hit points the hero recovers per day, independent of the regeneration attribute and of equipped items. 0 disables it.</span></em></td><td><b><?php echo defined('HERO_BASE_REGEN') ? (int) HERO_BASE_REGEN : 10; ?></b> HP / day</td></tr>
</table>
</div>
+7
View File
@@ -342,6 +342,13 @@ function refresh(tz) {
</select>
</td>
</tr>
<tr>
<td class="b">Hero base regeneration <em class="tooltip">?<span class="classic">Hit points the hero recovers per day on top of the points invested in the regeneration attribute and any equipped items. Without it, a hero with no regeneration points would never heal and would eventually die on adventures. Scales with server speed. 0 disables it.</span></em></td>
<td>
<input type="number" name="hero_base_regen" min="0" max="100" style="width:90px"
value="<?php echo defined('HERO_BASE_REGEN') ? (int) HERO_BASE_REGEN : 10; ?>"> HP / day
</td>
</tr>
</tbody>
</table>
</div>
+1 -1
View File
@@ -17,7 +17,7 @@
## Copyright : TravianZ (c) 2010-2025. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
$array_tribe=array('-',TRIBE1,TRIBE2,TRIBE3,TRIBE4,TRIBE5);
$array_tribe=array('-',TRIBE1,TRIBE2,TRIBE3,TRIBE4,TRIBE5,TRIBE6,TRIBE7,TRIBE8,TRIBE9);
$tribename = $array_tribe[$user['tribe']];
$searchresults = $admin->search_player($user['username']);
+14 -1
View File
@@ -50,7 +50,11 @@ if (!function_exists('admin_config_template_path')) {
// - existing server -> keep the current CRON_KEY (survives config saves)
// - key not defined -> provision a fresh random one (servers installed
// before CRON_KEY existed in the template)
function admin_config_template_contents() {
// $overrides permite unui mod de editare sa IMPUNA o valoare pentru un
// placeholder pe care altfel l-am pastra din constanta curenta (ex.
// editServerSet care schimba HERO_BASE_REGEN). Celelalte module apeleaza
// fara argumente si valorile curente raman neatinse.
function admin_config_template_contents(array $overrides = array()) {
$path = admin_config_template_path();
if ($path === false) {
@@ -93,6 +97,15 @@ if (!function_exists('admin_config_template_path')) {
'%CLEANUPMESSAGES%' => array('CLEANUP_MESSAGES_DAYS', 0),
);
// regenerarea de baza a eroului: acelasi tratament, sa nu fie resetata
$cleanupDefaults['%HEROBASEREGEN%'] = array('HERO_BASE_REGEN', 10);
foreach ($overrides as $ovPlaceholder => $ovValue) {
if (strpos($text, $ovPlaceholder) !== false) {
$text = str_replace($ovPlaceholder, (string) $ovValue, $text);
}
}
foreach ($cleanupDefaults as $placeholder => $info) {
if (strpos($text, $placeholder) === false) {
continue;
+7 -1
View File
@@ -74,7 +74,13 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$NEW_FUNCTION_TRIBE_SPARTANS = (defined('NEW_FUNCTION_TRIBE_SPARTANS') && NEW_FUNCTION_TRIBE_SPARTANS ? 'true' : 'false');
$NEW_FUNCTION_TRIBE_VIKINGS = (defined('NEW_FUNCTION_TRIBE_VIKINGS') && NEW_FUNCTION_TRIBE_VIKINGS ? 'true' : 'false');
$text = admin_config_template_contents();
// Regenerarea de baza a vietii eroului (HP pe zi). 0 = dezactivata.
$heroBaseRegen = isset($_POST['hero_base_regen']) ? (int) $_POST['hero_base_regen'] : 10;
if ($heroBaseRegen < 0) { $heroBaseRegen = 0; }
if ($heroBaseRegen > 100) { $heroBaseRegen = 100; }
$text = admin_config_template_contents(array('%HEROBASEREGEN%' => $heroBaseRegen));
$text = preg_replace("'%ERRORREPORT%'", $_POST['error'], $text);
$text = preg_replace("'%ERROR%'", $_POST['error'], $text);
$text = preg_replace("'%SERVERNAME%'", $_POST['servername'], $text);
+60 -2
View File
@@ -121,13 +121,48 @@ trait AutomationHero {
// Passive HP regeneration: returns the new health value, or -1 if unchanged.
private function calculateHealthRegen($hdata) {
// Eroii morti nu se regenereaza (se ridica prin inviere, nu singuri).
if ((int) $hdata['dead'] === 1) {
return -1;
}
if((time()-$hdata['lastupdate']) >= 1){
if($hdata['health'] < 100 and $hdata['health'] > 0){
// FIX: conditia veche era "health > 0", deci un erou VIU ajuns exact
// la 0 ramanea blocat acolo pentru totdeauna. Acum conteaza doar sa
// fie viu si sub 100.
if($hdata['health'] < 100){
if(SPEED <= 10) $speed = SPEED;
else if(SPEED <= 100) $speed = ceil(SPEED / 10);
else $speed = ceil(SPEED / 100);
$reg = $hdata['health'] + $hdata['regeneration'] * 5 * $speed / 86400 * (time() - $hdata['lastupdate']);
// FIX: bonusul HB_REGEN_HP al coifurilor (Regeneration / Health /
// Healing = +10/15/20 HP pe zi) nu era aplicat nicaieri - exista
// doar in definitia itemelor. Se adauga la regenerarea proprie a
// eroului, scalat cu aceeasi viteza de server ca aceasta, altfel
// pe un server rapid ar fi complet nesemnificativ.
// ANOMALIE DE SEMNALAT: in T4 valoarea e "HP pe zi" fixa; aici o
// scalam cu $speed pentru consecventa cu regenerarea din atribute.
// Daca preferi valoarea fixa, scoate "* $speed" de mai jos.
$itemRegen = $this->heroItemRegen($hdata);
// FIX PRINCIPAL: pana acum regenerarea depindea EXCLUSIV de
// atributul 'regeneration'. Un erou nou porneste cu acest atribut
// pe 0 (vezi INSERT-ul din 37_train.tpl), deci daca jucatorul nu
// punea puncte acolo, viata NU se refacea niciodata: scadea la
// fiecare aventura sau lupta si nu urca inapoi. Dupa destule
// aventuri ajungea la 0 si eroul murea chiar si la o aventura
// "Normal" (care ia doar 0-8 HP) - exact simptomul raportat.
//
// Acum exista o regenerare de BAZA, independenta de atribute, ca
// in Travian T4 (implicit 10 HP pe zi). Se poate regla din
// GameEngine/config.php: define('HERO_BASE_REGEN', 10);
$baseRegen = defined('HERO_BASE_REGEN') ? (float) HERO_BASE_REGEN : 10;
// HP pe zi din toate sursele: baza + atribut (5 per punct) + iteme
$perDay = $baseRegen + ($hdata['regeneration'] * 5) + $itemRegen;
$reg = $hdata['health']
+ $perDay * $speed / 86400 * (time() - $hdata['lastupdate']);
return ($reg <= 100) ? $reg : 100;
}
@@ -135,6 +170,29 @@ trait AutomationHero {
return -1;
}
/**
* HP pe zi adaugat de itemele echipate (HB_REGEN_HP).
*
* HeroBattleBonus::bonuses() are gard de feature flag, cache per request si
* intoarce null cand sistemul T4 e oprit - deci pe serverele fara erou T4
* comportamentul ramane exact ca inainte.
*/
private function heroItemRegen($hdata) {
if (!class_exists('HeroBattleBonus') || !HeroBattleBonus::enabled()) {
return 0;
}
$uid = isset($hdata['uid']) ? (int) $hdata['uid'] : 0;
if ($uid <= 0) {
return 0;
}
$bonuses = HeroBattleBonus::bonuses($uid);
return $bonuses ? (float) $bonuses[HB_REGEN_HP] : 0;
}
// Level-up + score points. Returns a column fragment.
private function calculateLevelUp($hdata) {
global $hero_levels;
@@ -212,10 +212,84 @@ trait AutomationVillageUpkeep {
private function culturePoints() {
global $database;
// FIX: punctele de cultura date de coifuri (Gladiator/Tribune/Consul =
// 100/400/800 pe zi) nu erau adaugate nicaieri - HB_CP exista doar in
// definitia itemelor si in agregarea din HeroItems::getBonuses().
// Se aplica INAINTE de updateVSumField, fiindca acela reseteaza
// users.lastupdate; asa ambele folosesc exact aceeasi fereastra de timp.
$this->applyHeroCulturePoints();
$database->updateVSumField('cp');
}
/**
* Adauga in users.cp punctele de cultura ale coifurilor echipate.
*
* Acumularea imita formula din updateVSumField(): valoarea e "pe zi", iar
* ziua de joc e 86400/SPEED (minim o ora), la fel ca pentru cladiri.
* Valorile sunt luate din catalog prin scaledBonusValue(), care aplica deja
* regula de server rapid (coifurile de cultura se injumatatesc la SPEED>=3).
*
* Se numara doar eroii vii (hero.dead = 0). Cum exista un singur slot de
* coif, un jucator nu poate avea doua iteme cu CP echipate simultan, deci
* join-ul nu poate dubla castigul.
*/
private function applyHeroCulturePoints() {
global $database, $heroItemCatalog;
if (!class_exists('HeroBattleBonus') || !HeroBattleBonus::enabled()) {
return;
}
if (empty($heroItemCatalog) || !is_array($heroItemCatalog)) {
return;
}
$heroItems = new HeroItems();
$cpValues = array();
foreach ($heroItemCatalog as $itemid => $def) {
if (!isset($def['bonus']) || !is_array($def['bonus']) || !isset($def['bonus'][HB_CP])) {
continue;
}
$value = (int) $heroItems->scaledBonusValue($def, (int) $def['bonus'][HB_CP]);
if ($value > 0) {
$cpValues[(int) $itemid] = $value;
}
}
if (empty($cpValues)) {
return;
}
$durDay = 86400 / SPEED;
if ($durDay < 3600) {
$durDay = 3600;
}
$case = '';
foreach ($cpValues as $itemid => $value) {
$case .= ' WHEN ' . (int) $itemid . ' THEN ' . (int) $value;
}
$ids = implode(',', array_map('intval', array_keys($cpValues)));
$q = "UPDATE " . TB_PREFIX . "users AS u
INNER JOIN " . TB_PREFIX . "hero_items AS hi
ON hi.uid = u.id AND hi.equipped = 1 AND hi.itemid IN (" . $ids . ")
INNER JOIN " . TB_PREFIX . "hero AS h
ON h.uid = u.id AND h.dead = 0
SET u.cp = u.cp + ((CASE hi.itemid" . $case . " END) * (UNIX_TIMESTAMP() - u.lastupdate) / " . $durDay . ")
WHERE u.lastupdate < (UNIX_TIMESTAMP() - 600)";
mysqli_query($database->dblink, $q);
}
/**
* Recompute a warehouse/granary capacity after a build completion.
* Returns [$fieldDbName, $max]: the vdata column to update and its new value.
+94
View File
@@ -264,11 +264,100 @@ class HeroItems
* - anything already in that slot is unequipped first (atomic enough for
* a per-user action; both statements touch only this user's rows)
*/
/* =========================================================================
* DISPONIBILITATEA EROULUI PENTRU SCHIMBAREA ECHIPAMENTULUI
* ===================================================================== */
/** @var array cache per request: uid => motiv ('' = eroul e acasa) */
private static $awayCache = array();
/**
* Motivul pentru care eroul NU poate schimba echipamentul acum.
*
* Intoarce '' cand eroul e acasa si poate fi echipat, sau una dintre:
* 'adventure' - plecat intr-o aventura
* 'attack' - plecat intr-un atac (singur sau cu trupe), dus sau intors
* 'reinforcement' - trimis ca intarire in alt sat
*
* Comportament ca in Travian original: echipamentul se schimba doar cat timp
* eroul e in sat. Eroul MORT nu e blocat - e "acasa", doar fara viata, si in
* T4 iti poti pregati echipamentul cat timp astepti invierea.
*/
public function heroAwayReason($uid)
{
$uid = (int) $uid;
if (isset(self::$awayCache[$uid])) {
return self::$awayCache[$uid];
}
$p = TB_PREFIX;
// ATENTIE: aliasul pentru miscarea de intoarcere NU poate fi "returning" -
// e cuvant rezervat in MariaDB (clauza RETURNING) si strica query-ul.
// Un singur query, cu motive separate, ca interfata sa poata afisa
// exact de ce e blocat. Coloanele `from`/`to` sunt cuvinte rezervate,
// de aceea sunt in backticks.
$q = "SELECT
(SELECT COUNT(*) FROM {$p}hero
WHERE uid = ? AND COALESCE(intraining, 0) = 0) AS hero_exists,
(SELECT COUNT(*) FROM {$p}hero_adventure
WHERE uid = ? AND status = 1) AS adventure,
(SELECT IFNULL(SUM(e.hero), 0) FROM {$p}enforcement e
INNER JOIN {$p}vdata v ON v.wref = e.`from`
WHERE v.owner = ?) AS reinforcement,
(SELECT IFNULL(SUM(a.t11), 0) FROM {$p}movement m
INNER JOIN {$p}attacks a ON a.id = m.ref
INNER JOIN {$p}vdata v ON v.wref = m.`from`
WHERE v.owner = ? AND m.proc = 0 AND m.sort_type = 3) AS outgoing,
(SELECT IFNULL(SUM(a.t11), 0) FROM {$p}movement m
INNER JOIN {$p}attacks a ON a.id = m.ref
INNER JOIN {$p}vdata v ON v.wref = m.`to`
WHERE v.owner = ? AND m.proc = 0 AND m.sort_type = 4) AS atk_return";
$stmt = $this->db->prepare($q);
if (!$stmt) {
// daca query-ul nu se poate pregati, nu blocam jucatorul
return self::$awayCache[$uid] = '';
}
$stmt->bind_param('iiiii', $uid, $uid, $uid, $uid, $uid);
$stmt->execute();
$stmt->bind_result($heroExists, $adventure, $reinforcement, $outgoing, $atkReturn);
$stmt->fetch();
$stmt->close();
$reason = '';
// Fara erou antrenat nu exista pe cine echipa. Se numara si eroii morti
// (randul exista, doar dead = 1): in T4 iti poti pregati echipamentul cat
// astepti invierea. Eroii aflati INCA in antrenament (intraining = 1) nu
// conteaza - inca nu exista cu adevarat.
if ((int) $heroExists === 0) {
$reason = 'nohero';
} elseif ((int) $adventure > 0) {
$reason = 'adventure';
} elseif (((int) $outgoing + (int) $atkReturn) > 0) {
$reason = 'attack';
} elseif ((int) $reinforcement > 0) {
$reason = 'reinforcement';
}
return self::$awayCache[$uid] = $reason;
}
public function equipItem($uid, $rowId)
{
global $database;
$uid = (int) $uid; $rowId = (int) $rowId;
// Eroul trebuie sa fie in sat. Verificarea e AICI, nu doar in interfata,
// ca un POST trimis manual sa nu poata ocoli regula.
if ($this->heroAwayReason($uid) !== '') {
return false;
}
$row = $this->findRowById($uid, $rowId);
if (!$row || $row['orphan'] || (int) $row['def']['slot'] === HSLOT_BAG) {
return false;
@@ -314,6 +403,11 @@ class HeroItems
/** Unequip a specific owned row. Returns true on success. */
public function unequipItem($uid, $rowId)
{
// Acelasi gard ca la echipare (vezi heroAwayReason).
if ($this->heroAwayReason($uid) !== '') {
return false;
}
$uid = (int) $uid; $rowId = (int) $rowId;
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero_items SET equipped = 0 WHERE uid = ? AND id = ? AND equipped = 1 LIMIT 1"
+5
View File
@@ -2813,6 +2813,7 @@ if (!function_exists('tz_loc_topic')) {
* T4 HERO PORT (Phase 6) - items / adventures / auction house
* ========================================================================== */
tz_def('HERO_T4_TAB_HERO', 'Hero');
tz_def('HERO_T4_TAB_OASIS', 'Oasis');
tz_def('HERO_T4_TAB_ITEMS', 'Inventory');
tz_def('HERO_T4_TAB_ADVENTURES', 'Adventures');
tz_def('HERO_T4_TAB_AUCTION', 'Auctions');
@@ -2842,6 +2843,7 @@ tz_def('HERO_ITEM_USED_OK', 'The item has been used.');
tz_def('HERO_ITEM_USE_FAIL', 'This item cannot be used right now.');
tz_def('HERO_ITEM_USE_BATTLE', 'This item is used automatically (bandages heal returning troops after battles).');
tz_def('HERO_EQUIP_OK', 'Item equipped.');
tz_def('HERO_LOCKED_NOHERO', 'You have no hero yet. Train one in the Hero\'s Mansion before equipping items.');
tz_def('HERO_EQUIP_FAIL', 'This item cannot be equipped (wrong hero unit or item type).');
tz_def('HERO_UNEQUIP_OK', 'Item taken off.');
@@ -2853,6 +2855,9 @@ tz_def('HERO_ADV_NONE', 'No adventures available right now. New ones ap
tz_def('HERO_ADV_DIFFICULTY', 'Difficulty');
tz_def('HERO_ADV_DIFF_NORMAL', 'Normal');
tz_def('HERO_ADV_DIFF_HARD', 'Hard');
tz_def('HERO_ADV_PLACE', 'Place');
tz_def('HERO_ADV_DANGER', 'Danger');
tz_def('HERO_ADV_LINK', 'Link');
tz_def('HERO_ADV_DURATION', 'Travel time (one way)');
tz_def('HERO_ADV_EXPIRES', 'Expires in');
tz_def('HERO_ADV_GO', 'Start adventure');
+5
View File
@@ -2736,6 +2736,9 @@ tz_def('HERO_ADV_DIED_INFO', 'Toata prada a fost pierduta. Eroul poate fi in
tz_def('HERO_ADV_DIFFICULTY', 'Dificultate');
tz_def('HERO_ADV_DIFF_HARD', 'Grea');
tz_def('HERO_ADV_DIFF_NORMAL', 'Normala');
tz_def('HERO_ADV_PLACE', 'Loc');
tz_def('HERO_ADV_DANGER', 'Pericol');
tz_def('HERO_ADV_LINK', 'Actiune');
tz_def('HERO_ADV_DURATION', 'Timp de calatorie (dus)');
tz_def('HERO_ADV_EXPIRES', 'Expira in');
tz_def('HERO_ADV_GO', 'Incepe aventura');
@@ -2787,6 +2790,7 @@ tz_def('HERO_AUC_YOUR_MAX', 'Maximul tau');
// Hero - echipare / inventar
tz_def('HERO_EQUIP', 'Echipeaza');
tz_def('HERO_LOCKED_NOHERO', 'Nu ai inca un erou. Antreneaza unul in Resedinta Eroului inainte de a echipa iteme.');
tz_def('HERO_EQUIP_FAIL', 'Acest obiect nu poate fi echipat (unitate de erou gresita sau tip de obiect gresit).');
tz_def('HERO_EQUIP_OK', 'Obiect echipat.');
tz_def('HERO_EXPERIENCE', 'Experienta');
@@ -2807,6 +2811,7 @@ tz_def('HERO_SLOT_4', 'Mana stanga');
tz_def('HERO_SLOT_5', 'Incaltaminte');
tz_def('HERO_SLOT_6', 'Cal');
tz_def('HERO_SLOT_7', 'Desaga');
tz_def('HERO_T4_TAB_OASIS', 'Oaze');
tz_def('HERO_T4_TAB_ADVENTURES', 'Aventuri');
tz_def('HERO_T4_TAB_AUCTION', 'Licitatii');
tz_def('HERO_T4_TAB_HERO', 'Erou');
+68
View File
@@ -634,9 +634,77 @@ class Technology {
$each = round(($bid19[$building->getTypeLevel(36)]['attri'] / 100) * ${'u'.$unit}['time'] / SPEED);
}
// FIX: bonusurile de instruire ale eroului (coifuri) nu erau aplicate
// NICAIERI - HB_TRAIN_INF si HB_TRAIN_CAV existau doar in definitia
// itemelor si in agregarea din HeroItems::getBonuses(), deci "Helmet of
// the Ruler" (-20% infanterie) sau "Helmet of the Heavy Cavalry" (-20%
// cavalerie) nu schimbau timpul cu nimic.
//
// Se aplica dupa formula cladirii, pe acelasi $each, deci merge identic
// in cazarma/grajd normale si in cele mari (Great Barracks/Great Stable),
// fiindca ramura e aleasa dupa TIPUL unitatii, nu dupa cladire.
// Nu se aplica la atelier, unitati speciale sau capcane - la fel ca in T4.
$each = $this->applyHeroTrainingBonus($each, $unit, $footies, $calvary);
return $each;
}
/**
* Reduce timpul de instruire cu procentul dat de coiful echipat al eroului.
*
* Proprietarul e luat din satul in care se face instruirea (nu din sesiune),
* ca sa fie corect si cand codul ruleaza in alt context decat o pagina a
* jucatorului. HeroBattleBonus::bonuses() are deja gard de feature flag si
* cache per request, si intoarce null cand sistemul T4 e oprit.
*/
private function applyHeroTrainingBonus($each, $unit, array $footies, array $calvary) {
global $village, $database;
if (!class_exists('HeroBattleBonus') || !HeroBattleBonus::enabled()) {
return $each;
}
// Proprietarul satului in care se instruieste, NU utilizatorul din
// sesiune: asa bonusul e corect si cand un admin lucreaza pe satul
// altui jucator (conteaza eroul proprietarului, nu al adminului).
// getVillageField e cache-uit per request, deci nu adauga query-uri.
$uid = (isset($village->wid) && $database)
? (int) $database->getVillageField($village->wid, 'owner')
: 0;
if ($uid <= 0) {
return $each;
}
$bonuses = HeroBattleBonus::bonuses($uid);
if (!$bonuses) {
return $each;
}
$percent = 0;
if (in_array($unit, $footies)) {
$percent = (int) $bonuses[HB_TRAIN_INF];
} elseif (in_array($unit, $calvary)) {
$percent = (int) $bonuses[HB_TRAIN_CAV];
}
if ($percent <= 0) {
return $each;
}
// plasa de siguranta: chiar daca s-ar aduna mai multe surse, timpul nu
// poate cobori sub 10% din valoarea de baza
if ($percent > 90) {
$percent = 90;
}
$each = (int) round($each * (100 - $percent) / 100);
return $each > 0 ? $each : 1;
}
private function getMaxTrainable($unit, $amt, $great) {
global $database, $village, $bid36;
+6
View File
@@ -90,6 +90,12 @@
}
if(isset($_GET['land']) && $village->resarray['f' . $id] >= 1) {
// FIX: pagina de oaze pierdea meniul T4 - ramura asta e inaintea
// celei cu tab-uri si nu includea navigatia deloc.
if (defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4) {
$t4tab = 'land';
include_once("37_t4nav.tpl");
}
include_once("37_land.tpl");
} else if (defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4
&& $village->resarray['f' . $id] >= 1
+55 -14
View File
@@ -17,6 +17,9 @@
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
## NOTA: doar prezentarea a fost refacuta (aspect ca in Travian original). ##
## Logica - generarea ofertelor, pornirea aventurii, mesajele - e neschimbata.##
## Coloana "Expires in" a fost pastrata (nu exista in originalul T4). ##
#################################################################################
$t4Adventures = new HeroAdventure();
@@ -41,6 +44,32 @@ $t4Adventures->generateOffers($session->uid);
$t4Offers = $t4Adventures->getOffers($session->uid);
$t4Running = $t4Adventures->getRunning($session->uid);
$t4Now = time();
/**
* Numele locului tintit de aventura, ca in Travian: "Abandoned valley" pentru
* un teren liber, "Unoccupied oasis" pentru o oaza libera. Tintele sunt mereu
* tile-uri neocupate (vezi HeroAdventure::generateOffers).
* getCoor() e cache-uit per request, deci nu adauga query-uri pe rand.
*/
$t4PlaceInfo = function ($wref) use ($database, $generator) {
$tile = $database->getCoor((int) $wref);
if (!is_array($tile) || !isset($tile['x'])) {
return null;
}
$isOasis = isset($tile['oasistype']) && (int) $tile['oasistype'] > 0;
return array(
'name' => $isOasis
? (defined('UNOCCUPIED') && defined('OASIS') ? UNOCCUPIED . ' ' . strtolower(OASIS) : 'Unoccupied oasis')
: (defined('ABANDVALLEY') ? ABANDVALLEY : 'Abandoned valley'),
'x' => (int) $tile['x'],
'y' => (int) $tile['y'],
'wref' => (int) $wref,
'check' => $generator->getMapCheck((int) $wref),
);
};
?>
<?php if ($t4Msg !== '') { ?>
@@ -59,10 +88,11 @@ $t4Now = time();
<table id="distribution" cellpadding="1" cellspacing="1" style="margin-top:10px;">
<thead>
<tr><th colspan="4"><?php echo HERO_ADV_LIST; ?></th></tr>
<tr><th colspan="5"><?php echo HERO_ADV_LIST; ?></th></tr>
<tr>
<td><b><?php echo HERO_ADV_DIFFICULTY; ?></b></td>
<td><b><?php echo defined('HERO_ADV_PLACE') ? HERO_ADV_PLACE : 'Place'; ?></b></td>
<td><b><?php echo HERO_ADV_DURATION; ?></b></td>
<td><b><?php echo defined('HERO_ADV_DANGER') ? HERO_ADV_DANGER : 'Danger'; ?></b></td>
<td><b><?php echo HERO_ADV_EXPIRES; ?></b></td>
<td></td>
</tr>
@@ -70,31 +100,42 @@ $t4Now = time();
<tbody>
<?php if (count($t4Offers)) { ?>
<?php foreach ($t4Offers as $t4Offer) { ?>
<?php
$t4Place = $t4PlaceInfo($t4Offer['wref']);
$t4Hard = ((int) $t4Offer['difficulty'] === 1);
?>
<tr>
<td>
<?php if ((int)$t4Offer['difficulty'] === 1): ?>
<img src="img/hero/dangerGreat.gif" alt="hard" title="<?php echo HERO_ADV_DIFF_HARD; ?>" style="vertical-align:middle; margin-right:4px; width:16px; height:16px;">
<?php echo HERO_ADV_DIFF_HARD; ?>
<?php else: ?>
<img src="img/hero/danger.gif" alt="normal" title="<?php echo HERO_ADV_DIFF_NORMAL; ?>" style="vertical-align:middle; margin-right:4px; width:16px; height:16px;">
<?php echo HERO_ADV_DIFF_NORMAL; ?>
<?php endif; ?>
</td>
<?php echo $t4Place ? $t4Place['name'] : '&mdash;'; ?>
<?php if ($t4Place) { ?>
<a href="karte.php?d=<?php echo $t4Place['wref']; ?>&amp;c=<?php echo $t4Place['check']; ?>"><b>(<?php echo $t4Place['x']; ?>|<?php echo $t4Place['y']; ?>)</b></a>
<?php } ?>
</td>
<td><?php echo $generator->getTimeFormat((int) $t4Offer['duration']); ?></td>
<td style="text-align:center;">
<img src="img/hero/<?php echo $t4Hard ? 'dangerGreat.gif' : 'danger.gif'; ?>"
alt="<?php echo $t4Hard ? HERO_ADV_DIFF_HARD : HERO_ADV_DIFF_NORMAL; ?>"
title="<?php echo $t4Hard ? HERO_ADV_DIFF_HARD : HERO_ADV_DIFF_NORMAL; ?>"
style="width:16px;height:16px;vertical-align:middle;">
</td>
<td><span id="timer<?php echo ++$session->timer; ?>"><?php echo $generator->getTimeFormat(max(0, $t4Offer['expire'] - $t4Now)); ?></span></td>
<td style="width:140px;text-align:center;">
<td style="width:150px;">
<?php if (!$t4Running) { ?>
<form action="" method="POST" style="margin:0;">
<form action="" method="POST" style="margin:0;display:inline;">
<input type="hidden" name="t4action" value="startadv">
<input type="hidden" name="advid" value="<?php echo (int) $t4Offer['id']; ?>">
<input type="submit" value="<?php echo HERO_ADV_GO; ?>">
<button type="submit" style="background:none;border:0;padding:0;cursor:pointer;font-size:inherit;font-family:inherit;color:#3a6f1e;font-weight:bold;">&#9658; <?php echo HERO_ADV_GO; ?></button>
</form>
<?php } else { ?>-<?php } ?>
</td>
</tr>
<?php } ?>
<?php } else { ?>
<tr><td colspan="4"><?php echo HERO_ADV_NONE; ?></td></tr>
<tr><td colspan="5"><?php echo HERO_ADV_NONE; ?></td></tr>
<?php } ?>
</tbody>
</table>
+167 -8
View File
@@ -46,7 +46,11 @@ $heroStatColumns = [
$renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
$field = $heroStatColumns[$action];
if ($hero_info['points'] > 0 && $hero_info[$field] < 100) {
return "<a href=\"build.php?id=" . $id . "&add=" . $action . "\">(<b>+</b>)</a>";
// class + data-* pentru JS; linkul ramane un GET valid, deci fara JS
// pagina se comporta exact ca inainte (un punct per click, cu refresh).
return "<a href=\"build.php?id=" . $id . "&add=" . $action . "\""
. " class=\"t4AddPoint\" data-stat=\"" . $action . "\""
. " data-base=\"" . (int) $hero_info[$field] . "\">(<b>+</b>)</a>";
}
return "<span class=\"none\">(+)</span>";
};
@@ -71,7 +75,7 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
<td class="up"><span class="none">
<?php echo $renderAddLink('off'); ?>
</td>
<td class="po"><?php echo $hero_info['attack']; ?></td>
<td class="po" id="t4po_off"><?php echo $hero_info['attack']; ?></td>
</tr>
<tr>
<th><?php echo DEFENCE; ?></th>
@@ -80,7 +84,7 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
<td class="up"><span class="none">
<?php echo $renderAddLink('deff'); ?>
</td>
<td class="po"><?php echo $hero_info['defence']; ?></td>
<td class="po" id="t4po_deff"><?php echo $hero_info['defence']; ?></td>
</tr>
<tr>
<th><?php echo OFF_BONUS; ?></th>
@@ -89,7 +93,7 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
<td class="up"><span class="none">
<?php echo $renderAddLink('obonus'); ?>
</td>
<td class="po"><?php echo $hero_info['attackbonus']; ?></td>
<td class="po" id="t4po_obonus"><?php echo $hero_info['attackbonus']; ?></td>
</tr>
<tr>
<th><?php echo DEF_BONUS; ?></th>
@@ -98,7 +102,7 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
<td class="up"><span class="none">
<?php echo $renderAddLink('dbonus'); ?>
</td>
<td class="po"><?php echo $hero_info['defencebonus']; ?></td>
<td class="po" id="t4po_dbonus"><?php echo $hero_info['defencebonus']; ?></td>
</tr>
<tr>
<th><?php echo REGENERATION; ?></th>
@@ -107,7 +111,7 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
<td class="up"><span class="none">
<?php echo $renderAddLink('reg'); ?>
</td>
<td class="po"><?php echo $hero_info['regeneration']; ?></td>
<td class="po" id="t4po_reg"><?php echo $hero_info['regeneration']; ?></td>
</tr>
<tr>
<td colspan="5" class="empty"></td>
@@ -130,17 +134,111 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
<td class="val"><?php echo (int)$percent; ?>%</td>
<td class="xp"><img class="bar" src="img/x.gif" style="width:<?php echo $percent*2; ?>px;" alt="<?php echo (int)$percent; ?>%" title="<?php echo (int)$percent; ?>%" /></td>
<td class="up"></td>
<td class="rem"><?php echo $hero_info['points']; ?></td>
<td class="rem" id="t4rem"><?php echo $hero_info['points']; ?></td>
<?php }else{ ?>
<th title="<?php echo TZ_UNTIL_THE_NEXT_LEVEL; ?>"><?php echo EXPERIENCE; ?>:</th>
<td class="val">100%</td>
<td class="xp"><img class="bar" src="img/x.gif" style="width:200px;" alt="100%" title="100%" /></td>
<td class="up"></td>
<td class="rem"><?php echo $hero_info['points']; ?></td>
<td class="rem" id="t4rem"><?php echo $hero_info['points']; ?></td>
<?php } ?>
</tr>
</tbody>
</table>
<?php if ((int) $hero_info['points'] > 0) { ?>
<form id="t4PointsForm" action="" method="POST" style="margin:6px 0;">
<input type="hidden" name="t4points" value="1">
<?php foreach ($heroStatColumns as $t4Key => $t4Col) { ?>
<input type="hidden" name="p_<?php echo $t4Key; ?>" id="t4in_<?php echo $t4Key; ?>" value="0">
<?php } ?>
<div id="t4PointsBar" style="display:none;">
<button type="submit" id="t4PointsSave"><b>&#10003;</b> <?php echo defined('HERO_POINTS_SAVE') ? HERO_POINTS_SAVE : 'Save points'; ?> (<span id="t4PointsCount">0</span>)</button>
<a href="#" id="t4PointsCancel" style="margin-left:8px;"><?php echo defined('HERO_POINTS_CANCEL') ? HERO_POINTS_CANCEL : 'Cancel'; ?></a>
</div>
</form>
<script type="text/javascript">
/* Distribuirea punctelor fara reincarcarea paginii la fiecare click.
Fara JS, linkurile (+) raman GET-uri normale si merg ca inainte.
JS-ul doar ADUNA local; adevarul ramane la server, care valideaza totul
intr-un singur UPDATE atomic (vezi handlerul t4points din acest fisier). */
(function () {
var available = <?php echo (int) $hero_info['points']; ?>;
var links = document.getElementsByClassName('t4AddPoint');
if (!links.length) { return; }
var pending = {}, base = {};
var bar = document.getElementById('t4PointsBar');
var counter = document.getElementById('t4PointsCount');
var remCell = document.getElementById('t4rem');
function render() {
var used = 0, stat;
for (stat in pending) {
if (pending.hasOwnProperty(stat)) {
used += pending[stat];
var cell = document.getElementById('t4po_' + stat);
if (cell) {
cell.innerHTML = pending[stat] > 0
? base[stat] + ' <span style="color:#0a0;">(+' + pending[stat] + ')</span>'
: String(base[stat]);
}
var input = document.getElementById('t4in_' + stat);
if (input) { input.value = pending[stat]; }
}
}
if (remCell) { remCell.innerHTML = String(available - used); }
if (counter) { counter.innerHTML = String(used); }
if (bar) { bar.style.display = used > 0 ? '' : 'none'; }
}
for (var i = 0; i < links.length; i++) {
(function (link) {
var stat = link.getAttribute('data-stat');
base[stat] = parseInt(link.getAttribute('data-base'), 10) || 0;
pending[stat] = 0;
link.onclick = function (e) {
e.preventDefault();
var used = 0;
for (var k in pending) { if (pending.hasOwnProperty(k)) { used += pending[k]; } }
// nu poti aloca mai mult decat ai, nici trece de 100 pe o statistica
if (used >= available) { return false; }
if (base[stat] + pending[stat] >= 100) { return false; }
pending[stat]++;
render();
return false;
};
// click dreapta pe (+) scade alocarea, ca sa poti corecta fara reload
link.oncontextmenu = function (e) {
e.preventDefault();
if (pending[stat] > 0) { pending[stat]--; render(); }
return false;
};
})(links[i]);
}
var cancel = document.getElementById('t4PointsCancel');
if (cancel) {
cancel.onclick = function (e) {
e.preventDefault();
for (var k in pending) { if (pending.hasOwnProperty(k)) { pending[k] = 0; } }
render();
return false;
};
}
})();
</script>
<?php } ?>
<?php if(isset($_GET['e'])){
echo "<p><font size=\"1\" color=\"red\"><b>".ERROR_NAME_SHORT."</b></font></p>";
}
@@ -156,6 +254,67 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
// NOTE: the actions below are triggered by GET (?add=...) and modify
// data in the DB. This was the original (without CSRF), I did not change this aspect -
// it is an existing behavior in all build.php, not specific to this file.
// Distribuire IN BLOC (butonul "Save" din interfata cu JS).
// Siguranta: totul se valideaza pe server intr-un SINGUR UPDATE atomic, cu
// garzi in WHERE. Daca cineva trimite un POST modificat (mai multe puncte decat
// are, sau peste 100 la o statistica), conditiile nu se potrivesc, UPDATE-ul nu
// afecteaza niciun rand si nu se schimba nimic. Fiind o singura instructiune,
// nici doua cereri trimise simultan nu pot cheltui aceleasi puncte de doua ori.
if (isset($_POST['t4points'])) {
$t4Alloc = array();
$t4Total = 0;
foreach ($heroStatColumns as $t4Key => $t4Col) {
$t4Value = isset($_POST['p_' . $t4Key]) ? (int) $_POST['p_' . $t4Key] : 0;
if ($t4Value < 0) {
$t4Value = 0;
}
$t4Alloc[$t4Col] = $t4Value;
$t4Total += $t4Value;
}
if ($t4Total > 0) {
$t4Stmt = $database->dblink->prepare(
"UPDATE " . TB_PREFIX . "hero SET
`attack` = `attack` + ?,
`defence` = `defence` + ?,
`attackbonus` = `attackbonus` + ?,
`defencebonus` = `defencebonus` + ?,
`regeneration` = `regeneration` + ?,
`points` = `points` - ?
WHERE `heroid` = ?
AND `points` >= ?
AND `attack` + ? <= 100
AND `defence` + ? <= 100
AND `attackbonus` + ? <= 100
AND `defencebonus` + ? <= 100
AND `regeneration` + ? <= 100"
);
if ($t4Stmt) {
$t4HeroId = (int) $hero_info['heroid'];
$t4Stmt->bind_param(
'iiiiiiiiiiiii',
$t4Alloc['attack'], $t4Alloc['defence'], $t4Alloc['attackbonus'],
$t4Alloc['defencebonus'], $t4Alloc['regeneration'],
$t4Total, $t4HeroId, $t4Total,
$t4Alloc['attack'], $t4Alloc['defence'], $t4Alloc['attackbonus'],
$t4Alloc['defencebonus'], $t4Alloc['regeneration']
);
$t4Stmt->execute();
$t4Stmt->close();
}
}
header("Location: build.php?id=" . $id);
exit;
}
if (isset($_GET['add'])) {
$action = $_GET['add'];
+33 -1
View File
@@ -21,6 +21,27 @@
$t4Msg = '';
// Eroul poate schimba echipamentul doar cat timp e in sat (ca in Travian
// original). Motivul e calculat o singura data si folosit atat pentru mesaj,
// cat si pentru ascunderea butoanelor. Regula e impusa si in HeroItems, deci
// un POST trimis manual nu o poate ocoli.
$t4AwayReason = $t4HeroItems->heroAwayReason($session->uid);
$t4AwayText = '';
if ($t4AwayReason === 'nohero') {
$t4AwayText = defined('HERO_LOCKED_NOHERO') ? HERO_LOCKED_NOHERO
: 'You have no hero yet. Train one in the Hero\'s Mansion before equipping items.';
} elseif ($t4AwayReason === 'adventure') {
$t4AwayText = defined('HERO_LOCKED_ADVENTURE') ? HERO_LOCKED_ADVENTURE
: 'Your hero is on an adventure. Equipment can only be changed while the hero is in a village.';
} elseif ($t4AwayReason === 'attack') {
$t4AwayText = defined('HERO_LOCKED_ATTACK') ? HERO_LOCKED_ATTACK
: 'Your hero is on the move with the army. Equipment can only be changed while the hero is in a village.';
} elseif ($t4AwayReason === 'reinforcement') {
$t4AwayText = defined('HERO_LOCKED_REINFORCEMENT') ? HERO_LOCKED_REINFORCEMENT
: 'Your hero is reinforcing another village. Equipment can only be changed while the hero is in a village.';
}
if (isset($_POST['t4action'], $_POST['rowid'])) {
$t4RowId = (int) $_POST['rowid'];
@@ -53,6 +74,11 @@ $t4Inventory = $t4HeroItems->getInventory($session->uid);
$t4Equipped = $t4HeroItems->getEquipped($session->uid);
?>
<?php if ($t4AwayText !== '') { ?>
<p class="message" style="font-weight:bold;color:#8a6d3b;background:#fcf8e3;border:1px solid #faebcc;padding:6px 9px;border-radius:4px;">
<?php echo $t4AwayText; ?>
</p>
<?php } ?>
<?php if ($t4Msg !== '') { ?>
<p class="message" style="font-weight:bold;"><?php echo $t4Msg; ?></p>
<?php } ?>
@@ -69,11 +95,15 @@ $t4Equipped = $t4HeroItems->getEquipped($session->uid);
<td title="<?php echo htmlspecialchars(heroItemBonusText((int) $t4Row['itemid'])); ?>"><span class="heroT4Item item<?php echo (int) $t4Row['itemid']; ?>"></span> <?php echo $t4Row['name']; ?></td>
<td style="width:60px;text-align:center;"><?php echo 'T' . (int) $t4Row['def']['tier']; ?></td>
<td style="width:110px;text-align:center;">
<?php if ($t4AwayReason === '') { ?>
<form action="" method="POST" style="margin:0;">
<input type="hidden" name="t4action" value="unequip">
<input type="hidden" name="rowid" value="<?php echo (int) $t4Row['id']; ?>">
<input type="submit" value="<?php echo HERO_UNEQUIP; ?>">
</form>
<?php } else { ?>
<span style="color:#999;">&mdash;</span>
<?php } ?>
</td>
<?php } else { ?>
<td colspan="3" style="color:#888;">-</td>
@@ -110,9 +140,11 @@ $t4Equipped = $t4HeroItems->getEquipped($session->uid);
<input type="hidden" name="t4action" value="useitem">
<input type="text" name="qty" value="1" size="3" style="text-align:center;">
<input type="submit" value="<?php echo HERO_USE_ITEM; ?>">
<?php } else { ?>
<?php } elseif ($t4AwayReason === '') { ?>
<input type="hidden" name="t4action" value="equip">
<input type="submit" value="<?php echo HERO_EQUIP; ?>">
<?php } else { ?>
<span style="color:#999;">&mdash;</span>
<?php } ?>
</form>
</td>
+3
View File
@@ -24,6 +24,9 @@ $t4HeroItems = new HeroItems();
$t4Silver = $t4HeroItems->getSilver($session->uid);
$t4Tabs = [
'hero' => ['label' => HERO_T4_TAB_HERO, 'url' => 'build.php?id=' . $id],
// Oaze: pagina foloseste parametrul "land", nu "t4tab" (flux mai vechi).
'land' => ['label' => defined('HERO_T4_TAB_OASIS') ? HERO_T4_TAB_OASIS : 'Oasis',
'url' => 'build.php?id=' . $id . '&land'],
'items' => ['label' => HERO_T4_TAB_ITEMS, 'url' => 'build.php?id=' . $id . '&t4tab=items'],
'adventures' => ['label' => HERO_T4_TAB_ADVENTURES, 'url' => 'build.php?id=' . $id . '&t4tab=adventures'],
'auction' => ['label' => HERO_T4_TAB_AUCTION, 'url' => 'build.php?id=' . $id . '&t4tab=auction'],
+10
View File
@@ -54,6 +54,16 @@ define('CLEANUP_MESSAGES_DAYS', %CLEANUPMESSAGES%);
define('CLEANUP_INTERVAL', 3600);
define('CLEANUP_BATCH', 5000);
//////////////////////////////////
// ***** EROU *****//
//////////////////////////////////
// Regenerarea de BAZA a vietii eroului, in HP pe zi, independenta de punctele
// puse in atributul de regenerare (ca in Travian T4). Fara ea, un erou cu 0
// puncte in regenerare nu si-ar reface niciodata viata si ar muri inevitabil
// dupa destule aventuri. Se scaleaza cu viteza serverului, ca si regenerarea
// din atribute. 0 = dezactivata (comportamentul vechi).
define('HERO_BASE_REGEN', %HEROBASEREGEN%);
//////////////////////////////////
// ***** SERVER SETTINGS *****//
//////////////////////////////////
+8
View File
@@ -147,6 +147,14 @@ class Process {
$findReplace[$placeholder] = $value;
}
// Regenerarea de baza a vietii eroului (HP pe zi). Vezi HERO_BASE_REGEN.
$heroRegen = isset($_POST['hero_base_regen']) ? (int) $_POST['hero_base_regen'] : 10;
if ($heroRegen < 0) { $heroRegen = 0; }
if ($heroRegen > 100) { $heroRegen = 100; }
$findReplace["%HEROBASEREGEN%"] = $heroRegen;
$findReplace["%DOMAIN%"] = $_POST['domain'];
$findReplace["%HOMEPAGE%"] = $_POST['homepage'];
$findReplace["%SERVER%"] = $_POST['server'];
+1
View File
@@ -309,6 +309,7 @@ foreach($mechs as $k => $l){
<div><label>Delete unarchived reports after (days)</label><input class="input" name="cleanup_reports" id="cleanup_reports" value="14"></div>
<div><label>Delete chat messages after (days)</label><input class="input" name="cleanup_chat" id="cleanup_chat" value="7"></div>
<div><label>Delete messages erased by both sides (days)</label><input class="input" name="cleanup_messages" id="cleanup_messages" value="0"></div>
<div><label>Hero base regeneration (HP/day)</label><input class="input" name="hero_base_regen" id="hero_base_regen" value="10"></div>
</div>
<div class="f10" style="margin-top:8px;opacity:.75;">
Automation runs from cron.php instead of players' page loads. One cron invocation keeps working for