From 1e19f117caaa0b1c2078d8c8a3e20bc673f0f4a2 Mon Sep 17 00:00:00 2001
From: novgorodschi catalin
Date: Thu, 23 Jul 2026 15:16:51 +0300
Subject: [PATCH] Last fix for Hero T4 & V11
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
/*
|--------------------------------------------------------------------------
| 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.
|
*/
---
Admin/Templates/config.tpl | 1 +
Admin/Templates/editServerSet.tpl | 7 +
Admin/Templates/search2.tpl | 2 +-
GameEngine/Admin/Mods/config_template.php | 15 +-
GameEngine/Admin/Mods/editServerSet.php | 8 +-
GameEngine/Automation/AutomationHero.php | 62 ++++++-
.../Automation/AutomationVillageUpkeep.php | 76 +++++++-
GameEngine/HeroItems.php | 94 ++++++++++
GameEngine/Lang/en.php | 5 +
GameEngine/Lang/ro.php | 5 +
GameEngine/Technology.php | 68 +++++++
Templates/Build/37.tpl | 6 +
Templates/Build/37_adventures.tpl | 69 +++++--
Templates/Build/37_hero.tpl | 175 +++++++++++++++++-
Templates/Build/37_items.tpl | 34 +++-
Templates/Build/37_t4nav.tpl | 3 +
install/data/constant_format.tpl | 10 +
install/process.php | 8 +
install/templates/config.tpl | 1 +
19 files changed, 620 insertions(+), 29 deletions(-)
diff --git a/Admin/Templates/config.tpl b/Admin/Templates/config.tpl
index d04b4293..bd8f44bc 100644
--- a/Admin/Templates/config.tpl
+++ b/Admin/Templates/config.tpl
@@ -92,6 +92,7 @@ $editIcon = ' ?
? Enabled" : "Disabled "; ?>
?
+ Hero base regeneration ?Hit points the hero recovers per day, independent of the regeneration attribute and of equipped items. 0 disables it. HP / day
diff --git a/Admin/Templates/editServerSet.tpl b/Admin/Templates/editServerSet.tpl
index 3546d12f..29faa89f 100644
--- a/Admin/Templates/editServerSet.tpl
+++ b/Admin/Templates/editServerSet.tpl
@@ -342,6 +342,13 @@ function refresh(tz) {
+
+ Hero base regeneration ?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.
+
+ HP / day
+
+
diff --git a/Admin/Templates/search2.tpl b/Admin/Templates/search2.tpl
index 25cd40f3..8a6f3ca6 100644
--- a/Admin/Templates/search2.tpl
+++ b/Admin/Templates/search2.tpl
@@ -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']);
diff --git a/GameEngine/Admin/Mods/config_template.php b/GameEngine/Admin/Mods/config_template.php
index 180c9115..43aa7022 100644
--- a/GameEngine/Admin/Mods/config_template.php
+++ b/GameEngine/Admin/Mods/config_template.php
@@ -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;
diff --git a/GameEngine/Admin/Mods/editServerSet.php b/GameEngine/Admin/Mods/editServerSet.php
index b49e9d0c..b105c0ff 100755
--- a/GameEngine/Admin/Mods/editServerSet.php
+++ b/GameEngine/Admin/Mods/editServerSet.php
@@ -74,7 +74,13 @@ $fh = fopen($myFile, 'w') or die(" 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);
diff --git a/GameEngine/Automation/AutomationHero.php b/GameEngine/Automation/AutomationHero.php
index e3a53619..3cb20710 100644
--- a/GameEngine/Automation/AutomationHero.php
+++ b/GameEngine/Automation/AutomationHero.php
@@ -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;
diff --git a/GameEngine/Automation/AutomationVillageUpkeep.php b/GameEngine/Automation/AutomationVillageUpkeep.php
index a7ce1364..0938d51a 100644
--- a/GameEngine/Automation/AutomationVillageUpkeep.php
+++ b/GameEngine/Automation/AutomationVillageUpkeep.php
@@ -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.
diff --git a/GameEngine/HeroItems.php b/GameEngine/HeroItems.php
index cdb2cdb2..7cb2073e 100644
--- a/GameEngine/HeroItems.php
+++ b/GameEngine/HeroItems.php
@@ -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"
diff --git a/GameEngine/Lang/en.php b/GameEngine/Lang/en.php
index 275a307b..0185f601 100755
--- a/GameEngine/Lang/en.php
+++ b/GameEngine/Lang/en.php
@@ -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');
diff --git a/GameEngine/Lang/ro.php b/GameEngine/Lang/ro.php
index eb03c8a5..2c7ee2b2 100644
--- a/GameEngine/Lang/ro.php
+++ b/GameEngine/Lang/ro.php
@@ -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');
diff --git a/GameEngine/Technology.php b/GameEngine/Technology.php
index d2d454a7..13c55279 100755
--- a/GameEngine/Technology.php
+++ b/GameEngine/Technology.php
@@ -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;
diff --git a/Templates/Build/37.tpl b/Templates/Build/37.tpl
index 071ce54d..dabc80ec 100644
--- a/Templates/Build/37.tpl
+++ b/Templates/Build/37.tpl
@@ -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
diff --git a/Templates/Build/37_adventures.tpl b/Templates/Build/37_adventures.tpl
index 6811bd18..63798d42 100644
--- a/Templates/Build/37_adventures.tpl
+++ b/Templates/Build/37_adventures.tpl
@@ -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),
+ );
+};
?>
@@ -59,10 +88,11 @@ $t4Now = time();
-
+
-
+
+
@@ -70,31 +100,42 @@ $t4Now = time();
+
-
-
-
-
-
-
-
-
+
+
+ (|)
+
+
+
getTimeFormat((int) $t4Offer['duration']); ?>
+
+
+
+
+
getTimeFormat(max(0, $t4Offer['expire'] - $t4Now)); ?>
-
+
+
-
-
-
+
diff --git a/Templates/Build/37_hero.tpl b/Templates/Build/37_hero.tpl
index aa1bcf7b..373ac341 100644
--- a/Templates/Build/37_hero.tpl
+++ b/Templates/Build/37_hero.tpl
@@ -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 "(+ ) ";
+ // 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 "(+ ) ";
}
return "(+) ";
};
@@ -71,7 +75,7 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
-
+
@@ -80,7 +84,7 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
-
+
@@ -89,7 +93,7 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
-
+
@@ -98,7 +102,7 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
-
+
@@ -107,7 +111,7 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
-
+
@@ -130,17 +134,111 @@ $renderAddLink = function ($action) use ($hero_info, $id, $heroStatColumns) {
%
-
+
:
100%
-
+
+ 0) { ?>
+
+
+
+
".ERROR_NAME_SHORT."
";
}
@@ -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'];
diff --git a/Templates/Build/37_items.tpl b/Templates/Build/37_items.tpl
index 3d1c701d..caca4a8a 100644
--- a/Templates/Build/37_items.tpl
+++ b/Templates/Build/37_items.tpl
@@ -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);
?>
+
+
+
+
+
@@ -69,11 +95,15 @@ $t4Equipped = $t4HeroItems->getEquipped($session->uid);
+
+
+ —
+
-
@@ -110,9 +140,11 @@ $t4Equipped = $t4HeroItems->getEquipped($session->uid);
-
+
+
+ —
diff --git a/Templates/Build/37_t4nav.tpl b/Templates/Build/37_t4nav.tpl
index 4666a800..9baa2db4 100644
--- a/Templates/Build/37_t4nav.tpl
+++ b/Templates/Build/37_t4nav.tpl
@@ -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'],
diff --git a/install/data/constant_format.tpl b/install/data/constant_format.tpl
index 8603927a..d236c4ef 100644
--- a/install/data/constant_format.tpl
+++ b/install/data/constant_format.tpl
@@ -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 *****//
//////////////////////////////////
diff --git a/install/process.php b/install/process.php
index 9e87c8e6..8215dc5c 100644
--- a/install/process.php
+++ b/install/process.php
@@ -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'];
diff --git a/install/templates/config.tpl b/install/templates/config.tpl
index b1f20bf0..1a5ccbee 100644
--- a/install/templates/config.tpl
+++ b/install/templates/config.tpl
@@ -309,6 +309,7 @@ foreach($mechs as $k => $l){
Delete unarchived reports after (days)
Delete chat messages after (days)
Delete messages erased by both sides (days)
+ Hero base regeneration (HP/day)
Automation runs from cron.php instead of players' page loads. One cron invocation keeps working for