Add T4 Hero System (configurable on install)

Add T4 Hero System (configurable on install with TRUE/FALS) part of #285

-Adventures
-Merchants
-Equipment
This commit is contained in:
novgorodschi catalin
2026-07-10 09:15:42 +03:00
parent ed325b6d99
commit 3c99623a33
46 changed files with 4367 additions and 32 deletions
+1
View File
@@ -118,6 +118,7 @@ $editIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke=
<tr><td>Special Medals<em class="tooltip">?<span class="classic">Special Medals</span></em></td><td><?php echo NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
<tr><td>Server Milestones<em class="tooltip">?<span class="classic">Server Milestones</span></em></td><td><?php echo NEW_FUNCTIONS_MILESTONES ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
<tr><td>Server Medal Reset Timer<em class="tooltip">?<span class="classic">Server Medal Reset Timer</span></em></td><td><?php echo NEW_FUNCTIONS_MEDAL_RESET ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
<tr><td>T4 Hero (items, adventures, auction)<em class="tooltip">?<span class="classic">T4 Hero (items, adventures, auction)</span></em></td><td><?php echo NEW_FUNCTIONS_HERO_T4 ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
</table>
</div>
+185
View File
@@ -0,0 +1,185 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : editHeroT4.tpl ##
## Type : Admin Panel Frontend - T4 hero port controls ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
if (!isset($_GET['uid'])) { echo '<p>Missing uid.</p>'; return; }
$id = (int) $_GET['uid'];
include_once("../GameEngine/Data/hero_items.php");
include_once("../GameEngine/HeroItems.php");
include_once("../GameEngine/HeroAuction.php");
include_once("../GameEngine/HeroAdventure.php");
$t4HeroItems = new HeroItems();
$t4Auction = new HeroAuction();
$t4Adv = new HeroAdventure();
$t4Silver = $t4HeroItems->getSilver($id);
$t4Inventory = $t4HeroItems->getInventory($id);
$t4Sales = $t4Auction->getMySales($id);
$t4Bids = $t4Auction->getMyBids($id);
$t4Offers = $t4Adv->getOffers($id);
$t4Running = $t4Adv->getRunning($id);
$t4SlotNames = [1=>'Helmet',2=>'Body',3=>'Right hand',4=>'Left hand',5=>'Shoes',6=>'Horse',7=>'Bag'];
?>
<style>
.t4a-card{background:#fff;border:1px solid #e5e7eb;border-radius:10px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.04);margin-bottom:12px;}
.t4a-card h3{margin:0;padding:9px 14px;background:#f8fafc;border-bottom:1px solid #e5e7eb;font-size:12px;text-transform:uppercase;color:#475569;letter-spacing:.4px;font-weight:600;}
.t4a-card .body{padding:14px;}
.t4a-table{width:100%;border-collapse:collapse;font-size:13px;}
.t4a-table th{background:#f8fafc;padding:8px 10px;font-size:11px;text-transform:uppercase;color:#64748b;text-align:left;border-bottom:1px solid #e5e7eb;}
.t4a-table td{padding:8px 10px;border-bottom:1px solid #f1f5f9;vertical-align:middle;}
.t4a-table tr:last-child td{border-bottom:0;}
.t4a-btn{padding:5px 12px;border-radius:6px;border:1px solid #cbd5e1;background:#fff;cursor:pointer;font-size:12px;font-weight:600;color:#475569;}
.t4a-btn:hover{background:#66CCCC;color:#fff;border-color:#66CCCC;}
.t4a-btn.danger:hover{background:#dc2626;border-color:#dc2626;}
.t4a-inline{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
.t4a-inline input[type=text],.t4a-inline select{padding:6px 8px;border:1px solid #cbd5e1;border-radius:6px;font-size:13px;}
.t4a-msg{padding:8px 12px;border-radius:8px;margin-bottom:12px;font-size:13px;font-weight:600;}
.t4a-msg.ok{background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;}
.t4a-msg.err{background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;}
</style>
<h2>T4 Hero controls — user #<?php echo $id; ?>
<small><a href="admin.php?p=editHero&uid=<?php echo $id; ?>">&laquo; classic hero editor</a></small>
</h2>
<?php if (isset($_GET['ok'])) { ?><div class="t4a-msg ok">Action completed.</div><?php } ?>
<?php if (isset($_GET['e'])) { ?><div class="t4a-msg err">Action failed (check the values and try again).</div><?php } ?>
<div class="t4a-card">
<h3>Silver</h3>
<div class="body">
<form action="../GameEngine/Admin/Mods/editHeroT4.php" method="POST" class="t4a-inline">
<?php echo csrf_field(); ?>
<input type="hidden" name="t4admin" value="setsilver">
<input type="hidden" name="uid" value="<?php echo $id; ?>">
<input type="text" name="silver" value="<?php echo $t4Silver; ?>" size="10" style="text-align:right;">
<button type="submit" class="t4a-btn">Set balance</button>
</form>
</div>
</div>
<div class="t4a-card">
<h3>Grant item</h3>
<div class="body">
<form action="../GameEngine/Admin/Mods/editHeroT4.php" method="POST" class="t4a-inline">
<?php echo csrf_field(); ?>
<input type="hidden" name="t4admin" value="giveitem">
<input type="hidden" name="uid" value="<?php echo $id; ?>">
<select name="itemid">
<?php foreach ($heroItemCatalog as $t4Iid => $t4Def) { ?>
<option value="<?php echo $t4Iid; ?>">#<?php echo $t4Iid; ?> <?php echo $t4Def['name']; ?> (T<?php echo $t4Def['tier']; ?>, <?php echo $t4SlotNames[$t4Def['slot']]; ?>)</option>
<?php } ?>
</select>
Qty: <input type="text" name="qty" value="1" size="3" style="text-align:center;">
<button type="submit" class="t4a-btn">Grant</button>
</form>
</div>
</div>
<div class="t4a-card">
<h3>Inventory (<?php echo count($t4Inventory); ?>)</h3>
<div class="body">
<?php if (count($t4Inventory)) { ?>
<table class="t4a-table">
<tr><th>Row</th><th>Item</th><th>Slot</th><th>Qty</th><th>Equipped</th><th></th></tr>
<?php foreach ($t4Inventory as $t4Row) { ?>
<tr>
<td>#<?php echo (int) $t4Row['id']; ?></td>
<td><?php echo $t4Row['name']; ?><?php if ($t4Row['orphan']) echo ' <b style="color:#b91c1c;">(orphan itemid ' . (int) $t4Row['itemid'] . ')</b>'; ?></td>
<td><?php echo $t4SlotNames[(int) $t4Row['slot']] ?? (int) $t4Row['slot']; ?></td>
<td><?php echo (int) $t4Row['quantity']; ?></td>
<td><?php echo $t4Row['equipped'] ? 'yes' : '-'; ?></td>
<td>
<form action="../GameEngine/Admin/Mods/editHeroT4.php" method="POST" style="margin:0;"
onsubmit="return confirm('Delete this item row permanently?');">
<?php echo csrf_field(); ?>
<input type="hidden" name="t4admin" value="delitem">
<input type="hidden" name="uid" value="<?php echo $id; ?>">
<input type="hidden" name="rowid" value="<?php echo (int) $t4Row['id']; ?>">
<button type="submit" class="t4a-btn danger">Delete</button>
</form>
</td>
</tr>
<?php } ?>
</table>
<?php } else { ?>No items.<?php } ?>
</div>
</div>
<div class="t4a-card">
<h3>Open auctions by this player (<?php echo count($t4Sales); ?>) / bids (<?php echo count($t4Bids); ?>)</h3>
<div class="body">
<?php if (count($t4Sales) || count($t4Bids)) { ?>
<table class="t4a-table">
<tr><th>Auction</th><th>Role</th><th>Item</th><th>Price</th><th>Ends</th><th></th></tr>
<?php foreach ([['rows' => $t4Sales, 'role' => 'seller'], ['rows' => $t4Bids, 'role' => 'top bidder']] as $t4Group) { ?>
<?php foreach ($t4Group['rows'] as $t4A) { ?>
<tr>
<td>#<?php echo (int) $t4A['id']; ?></td>
<td><?php echo $t4Group['role']; ?></td>
<td><?php echo $t4A['name']; ?> (<?php echo (int) $t4A['quantity']; ?>x)</td>
<td><?php echo number_format((int) $t4A['silver_current']); ?></td>
<td><?php echo date('d.m H:i', (int) $t4A['time_end']); ?></td>
<td>
<form action="../GameEngine/Admin/Mods/editHeroT4.php" method="POST" style="margin:0;"
onsubmit="return confirm('Cancel this auction? Bidder is refunded, item returns to the seller.');">
<?php echo csrf_field(); ?>
<input type="hidden" name="t4admin" value="cancelauction">
<input type="hidden" name="uid" value="<?php echo $id; ?>">
<input type="hidden" name="aucid" value="<?php echo (int) $t4A['id']; ?>">
<button type="submit" class="t4a-btn danger">Cancel</button>
</form>
</td>
</tr>
<?php } ?>
<?php } ?>
</table>
<?php } else { ?>No open auction involvement.<?php } ?>
</div>
</div>
<div class="t4a-card">
<h3>Adventures</h3>
<div class="body">
<?php if ($t4Running) { ?>
<p><b>Running:</b> adventure #<?php echo (int) $t4Running['id']; ?>,
difficulty <?php echo (int) $t4Running['difficulty'] === 1 ? 'hard' : 'normal'; ?>,
arrives <?php echo date('d.m H:i:s', (int) $t4Running['endtime']); ?> (do not delete running adventures).</p>
<?php } ?>
<?php if (count($t4Offers)) { ?>
<table class="t4a-table">
<tr><th>Offer</th><th>Difficulty</th><th>Duration</th><th>Expires</th><th></th></tr>
<?php foreach ($t4Offers as $t4O) { ?>
<tr>
<td>#<?php echo (int) $t4O['id']; ?></td>
<td><?php echo (int) $t4O['difficulty'] === 1 ? 'hard' : 'normal'; ?></td>
<td><?php echo gmdate('H:i:s', (int) $t4O['duration']); ?></td>
<td><?php echo date('d.m H:i', (int) $t4O['expire']); ?></td>
<td>
<form action="../GameEngine/Admin/Mods/editHeroT4.php" method="POST" style="margin:0;">
<?php echo csrf_field(); ?>
<input type="hidden" name="t4admin" value="deladventure">
<input type="hidden" name="uid" value="<?php echo $id; ?>">
<input type="hidden" name="advid" value="<?php echo (int) $t4O['id']; ?>">
<button type="submit" class="t4a-btn danger">Remove</button>
</form>
</td>
</tr>
<?php } ?>
</table>
<?php } else { ?>No available offers.<?php } ?>
</div>
</div>
+11
View File
@@ -232,6 +232,17 @@ if($_SESSION['access'] < 9) die(ACCESS_DENIED_ADMIN);
<option value="False" <?php if(!defined('NEW_FUNCTIONS_MEDAL_RESET') || NEW_FUNCTIONS_MEDAL_RESET == false) echo "selected";?>>False</option>
</select>
</td>
</tr>
<tr>
<td class="b">T4 Hero (items, adventures, auction) <em class="tooltip">?<span class="classic">Enable (Disable) the "T4 Hero (items, adventures, auction)" System</span></em>
<?php if (!defined('NEW_FUNCTIONS_HERO_T4')): ?><br><span style="color:#c0392b;font-size:11px;font-weight:normal;text-transform:none;">Not present in config.php yet &mdash; saving this form once will add it (defaults to False until then).</span><?php endif; ?>
</td>
<td>
<select name="new_functions_hero_t4">
<option value="True" <?php if(defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4 == true) echo "selected";?>>True</option>
<option value="False" <?php if(!defined('NEW_FUNCTIONS_HERO_T4') || NEW_FUNCTIONS_HERO_T4 == false) echo "selected";?>>False</option>
</select>
</td>
</tr>
</tbody>
</table>
+1 -1
View File
@@ -173,7 +173,7 @@ body{margin:0;background:#f1f5f9;font-family:system-ui,-apple-system,Segoe UI,Ro
<?php
if ($criteria!= "") {
$artifactsEffect = ['-', VILLAGE_EFFECT, ACCOUNT_EFFECT, UNIQUE_EFFECT];
$array_tribe = ['-', TRIBE1, TRIBE2, TRIBE3, TRIBE4, TRIBE5, TRIBE6];
$array_tribe = ['-', TRIBE1, TRIBE2, TRIBE3, TRIBE4, TRIBE5];
$q = "SELECT v.wref, v.owner, v.name, v.capital, v.pop, u.username, u.tribe, u.access, w.x, w.y"
. ($includeSize? ", a.size" : "")
. " FROM ".TB_PREFIX."vdata AS v"
+5
View File
@@ -109,6 +109,11 @@ if ($hero !== false) {
<a class="hero-icon edit" href='admin.php?p=editHero&uid=<?php echo $id; ?>&amp;hid=<?php echo $h['heroid'] ?>' title="Edit">
<svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
</a>
<?php if (defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4) { ?>
<a class="hero-icon edit" href='admin.php?p=editHeroT4&uid=<?php echo $id; ?>' title="T4 items / silver / auctions">
<svg viewBox="0 0 24 24"><path d="M6 2l1.5 4h9L18 2"/><path d="M4 6h16l-2 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L4 6z"/></svg>
</a>
<?php } ?>
<a class="hero-icon kill" href='?action=killHero&uid=<?php echo $id; ?>' title="Kill">
<svg viewBox="0 0 24 24"><path d="M3 6h18M8 6V4h8v2m-1 0v14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V6h10z"/></svg>
</a>
+11 -1
View File
@@ -91,7 +91,7 @@ function admin_validated_page(string $raw): string
'editExtraSet', 'editAdminInfo', 'resetServer', 'player', 'editUser',
'deletion', 'Newmessage', 'editPlus', 'editSitter', 'editPassword',
'editProtection', 'editOverall',
'editWeek', 'userlogin', 'userillegallog', 'editHero', 'editAdditional',
'editWeek', 'userlogin', 'userillegallog', 'editHero', 'editHeroT4', 'editAdditional',
'village', 'editResources', 'addTroops', 'addABTroops', 'editVillage',
'villagelog', 'techlog', 'msg',
'alliance', 'editAli', 'delAli','editNewFunctions',
@@ -433,6 +433,16 @@ if ($page !== '') {
}
break;
case 'editHeroT4':
$uid = admin_input_id($_GET, 'uid');
if ($uid !== null) {
$user = $database->getUserArray($uid, 1);
$subpage = 'T4 Hero Controls (' . e($user['username']) . ')';
} else {
$subpage = 'T4 Hero Controls';
}
break;
case 'editAdditional':
$uid = admin_input_id($_GET, 'uid');
if ($uid !== null) {
+2
View File
@@ -73,6 +73,7 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM = (NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM == false ? 'false' : 'true');
$NEW_FUNCTIONS_MILESTONES = (NEW_FUNCTIONS_MILESTONES == false ? 'false' : 'true');
$NEW_FUNCTIONS_MEDAL_RESET = (NEW_FUNCTIONS_MEDAL_RESET == false ? 'false' : 'true');
$NEW_FUNCTIONS_HERO_T4 = (NEW_FUNCTIONS_HERO_T4 == false ? 'false' : 'true');
$text = admin_config_template_contents();
$text = preg_replace("'%ERRORREPORT%'", ERROR_REPORT, $text);
@@ -164,6 +165,7 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$text = preg_replace("'%NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM%'", $NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM, $text);
$text = preg_replace("'%NEW_FUNCTIONS_MILESTONES%'", $NEW_FUNCTIONS_MILESTONES, $text);
$text = preg_replace("'%NEW_FUNCTIONS_MEDAL_RESET%'", $NEW_FUNCTIONS_MEDAL_RESET, $text);
$text = preg_replace("'%NEW_FUNCTIONS_HERO_T4%'", $NEW_FUNCTIONS_HERO_T4, $text);
// PLUS settings need to be kept intact
$text = preg_replace("'%PLUS_TIME%'", PLUS_TIME, $text);
+131
View File
@@ -0,0 +1,131 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : editHeroT4.php ##
## Type : BACKEND (Admin Mod) - T4 hero port admin controls ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
## ##
## POST actions (all CSRF-verified, admin access >= 9, same conventions as ##
## editHero.php): ##
## t4admin=setsilver uid, silver -> absolute silver balance ##
## t4admin=giveitem uid, itemid, qty -> grant catalog item ##
## t4admin=delitem uid, rowid -> delete inventory row ##
## t4admin=cancelauction aucid -> refund + return item ##
## t4admin=deladventure advid -> remove an offer ##
## ##
#################################################################################
// Load CSRF helpers + admin_deny() before the access check (same as editHero.php #299).
require_once(__DIR__ . '/../csrf.php');
if (!isset($_SESSION)) {
session_start();
}
if (empty($_SESSION['access']) || $_SESSION['access'] < 9) {
admin_deny('You must be signed in as an administrator to view this page. Your session may have expired — please return to the admin panel and sign in again.');
}
// POSTed to directly, so it verifies the CSRF token itself (#139).
csrf_verify();
// ---------------------------------------------------------------------------
// Autoloader path (same discovery loop as the other Mods)
// ---------------------------------------------------------------------------
$autoprefix = '';
for ($i = 0; $i < 5; $i++) {
$autoprefix = str_repeat('../', $i);
if (file_exists($autoprefix . 'autoloader.php')) {
break;
}
}
include_once($autoprefix . "GameEngine/config.php");
include_once($autoprefix . "GameEngine/Database.php");
include_once($autoprefix . "GameEngine/Data/hero_items.php");
include_once($autoprefix . "GameEngine/HeroItems.php");
include_once($autoprefix . "GameEngine/HeroAuction.php");
$uid = (int) ($_POST['uid'] ?? 0);
$action = (string) ($_POST['t4admin'] ?? '');
$status = '&e=1'; // error by default; flipped to &ok=1 on success
$heroItems = new HeroItems();
switch ($action) {
case 'setsilver':
// Absolute set (not delta) - simplest to reason about as an admin.
$silver = max(0, (int) ($_POST['silver'] ?? 0));
if ($uid > 0) {
$stmt = $database->dblink->prepare(
"UPDATE " . TB_PREFIX . "hero SET silver = ? WHERE uid = ? LIMIT 1"
);
$stmt->bind_param('ii', $silver, $uid);
$stmt->execute();
if ($stmt->affected_rows > 0 || $heroItems->getSilver($uid) === $silver) {
$status = '&ok=1';
}
$stmt->close();
}
break;
case 'giveitem':
$itemid = (int) ($_POST['itemid'] ?? 0);
$qty = max(1, (int) ($_POST['qty'] ?? 1));
if ($uid > 0 && $heroItems->addItem($uid, $itemid, $qty) > 0) {
$status = '&ok=1';
}
break;
case 'delitem':
// Hard delete of an inventory row (equipped or not) - admin override.
$rowid = (int) ($_POST['rowid'] ?? 0);
if ($uid > 0 && $rowid > 0) {
$stmt = $database->dblink->prepare(
"DELETE FROM " . TB_PREFIX . "hero_items WHERE id = ? AND uid = ? LIMIT 1"
);
$stmt->bind_param('ii', $rowid, $uid);
$stmt->execute();
if ($stmt->affected_rows > 0) {
$status = '&ok=1';
}
$stmt->close();
}
break;
case 'cancelauction':
$aucid = (int) ($_POST['aucid'] ?? 0);
$auction = new HeroAuction();
if ($aucid > 0 && $auction->adminCancel($aucid)) {
$status = '&ok=1';
}
break;
case 'deladventure':
// Remove an available offer (status 0 only - never a running one,
// that would strand the hero mid-movement).
$advid = (int) ($_POST['advid'] ?? 0);
if ($advid > 0) {
$stmt = $database->dblink->prepare(
"DELETE FROM " . TB_PREFIX . "hero_adventure WHERE id = ? AND status = 0 LIMIT 1"
);
$stmt->bind_param('i', $advid);
$stmt->execute();
if ($stmt->affected_rows > 0) {
$status = '&ok=1';
}
$stmt->close();
}
break;
}
header("Location: ../../../Admin/admin.php?p=editHeroT4&uid=$uid$status");
exit;
+2
View File
@@ -69,6 +69,7 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM = (NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM == false ? 'false' : 'true');
$NEW_FUNCTIONS_MILESTONES = (NEW_FUNCTIONS_MILESTONES == false ? 'false' : 'true');
$NEW_FUNCTIONS_MEDAL_RESET = (NEW_FUNCTIONS_MEDAL_RESET == false ? 'false' : 'true');
$NEW_FUNCTIONS_HERO_T4 = (NEW_FUNCTIONS_HERO_T4 == false ? 'false' : 'true');
$text = admin_config_template_contents();
$text = preg_replace("'%ERRORREPORT%'", $ERRORREPORT, $text);
@@ -164,6 +165,7 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$text = preg_replace("'%NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM%'", $NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM, $text);
$text = preg_replace("'%NEW_FUNCTIONS_MILESTONES%'", $NEW_FUNCTIONS_MILESTONES, $text);
$text = preg_replace("'%NEW_FUNCTIONS_MEDAL_RESET%'", $NEW_FUNCTIONS_MEDAL_RESET, $text);
$text = preg_replace("'%NEW_FUNCTIONS_HERO_T4%'", $NEW_FUNCTIONS_HERO_T4, $text);
// PLUS settings need to be kept intact
$text = preg_replace("'%PLUS_TIME%'", PLUS_TIME, $text);
@@ -131,6 +131,7 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$text = preg_replace("'%NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM%'", $_POST['new_functions_special_medals_system'], $text);
$text = preg_replace("'%NEW_FUNCTIONS_MILESTONES%'", $_POST['new_functions_milestones'], $text);
$text = preg_replace("'%NEW_FUNCTIONS_MEDAL_RESET%'", $_POST['new_functions_medal_reset'], $text);
$text = preg_replace("'%NEW_FUNCTIONS_HERO_T4%'", $_POST['new_functions_hero_t4'], $text);
// PLUS settings need to be kept intact
$text = preg_replace("'%PLUS_TIME%'", PLUS_TIME, $text);
+2
View File
@@ -75,6 +75,7 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM = (NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM == false ? 'false' : 'true');
$NEW_FUNCTIONS_MILESTONES = (NEW_FUNCTIONS_MILESTONES == false ? 'false' : 'true');
$NEW_FUNCTIONS_MEDAL_RESET = (NEW_FUNCTIONS_MEDAL_RESET == false ? 'false' : 'true');
$NEW_FUNCTIONS_HERO_T4 = (NEW_FUNCTIONS_HERO_T4 == false ? 'false' : 'true');
$text = admin_config_template_contents();
$text = preg_replace("'%ERRORREPORT%'", $ERRORREPORT, $text);
@@ -170,6 +171,7 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$text = preg_replace("'%NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM%'", $NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM, $text);
$text = preg_replace("'%NEW_FUNCTIONS_MILESTONES%'", $NEW_FUNCTIONS_MILESTONES, $text);
$text = preg_replace("'%NEW_FUNCTIONS_MEDAL_RESET%'", $NEW_FUNCTIONS_MEDAL_RESET, $text);
$text = preg_replace("'%NEW_FUNCTIONS_HERO_T4%'", $NEW_FUNCTIONS_HERO_T4, $text);
// PLUS settings need to be kept intact
$text = preg_replace("'%PLUS_TIME%'", PLUS_TIME, $text);
+2
View File
@@ -57,6 +57,7 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM = (NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM == false ? 'false' : 'true');
$NEW_FUNCTIONS_MILESTONES = (NEW_FUNCTIONS_MILESTONES == false ? 'false' : 'true');
$NEW_FUNCTIONS_MEDAL_RESET = (NEW_FUNCTIONS_MEDAL_RESET == false ? 'false' : 'true');
$NEW_FUNCTIONS_HERO_T4 = (NEW_FUNCTIONS_HERO_T4 == false ? 'false' : 'true');
// SERVER SETTINGS - we need to keep these intact
$text = preg_replace("'%ERRORREPORT%'", ERROR_REPORT, $text);
@@ -148,6 +149,7 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$text = preg_replace("'%NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM%'", $NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM, $text);
$text = preg_replace("'%NEW_FUNCTIONS_MILESTONES%'", $NEW_FUNCTIONS_MILESTONES, $text);
$text = preg_replace("'%NEW_FUNCTIONS_MEDAL_RESET%'", $NEW_FUNCTIONS_MEDAL_RESET, $text);
$text = preg_replace("'%NEW_FUNCTIONS_HERO_T4%'", $NEW_FUNCTIONS_HERO_T4, $text);
// PLUS SETTINGS
$text = preg_replace("'%PLUS_TIME%'", $_POST['plus_time'], $text);
+2
View File
@@ -68,6 +68,7 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM = (NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM == false ? 'false' : 'true');
$NEW_FUNCTIONS_MILESTONES = (NEW_FUNCTIONS_MILESTONES == false ? 'false' : 'true');
$NEW_FUNCTIONS_MEDAL_RESET = (NEW_FUNCTIONS_MEDAL_RESET == false ? 'false' : 'true');
$NEW_FUNCTIONS_HERO_T4 = (NEW_FUNCTIONS_HERO_T4 == false ? 'false' : 'true');
$text = admin_config_template_contents();
$text = preg_replace("'%ERRORREPORT%'", $_POST['error'], $text);
@@ -159,6 +160,7 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
$text = preg_replace("'%NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM%'", $NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM, $text);
$text = preg_replace("'%NEW_FUNCTIONS_MILESTONES%'", $NEW_FUNCTIONS_MILESTONES, $text);
$text = preg_replace("'%NEW_FUNCTIONS_MEDAL_RESET%'", $NEW_FUNCTIONS_MEDAL_RESET, $text);
$text = preg_replace("'%NEW_FUNCTIONS_HERO_T4%'", $NEW_FUNCTIONS_HERO_T4, $text);
// PLUS settings need to be kept intact
$text = preg_replace("'%PLUS_TIME%'", PLUS_TIME, $text);
+48 -2
View File
@@ -95,7 +95,8 @@ class Automation {
"trainingComplete", "starvation", "celebrationComplete", "festivalComplete",
"sendUnitsComplete", "loyaltyRegeneration", "sendreinfunitsComplete",
"returnunitsComplete", "sendSettlersComplete", "spawnNatars",
"spawnWWVillages", "spawnWWBuildingPlans", "activateArtifacts"];
"spawnWWVillages", "spawnWWBuildingPlans", "activateArtifacts",
"heroAdventureComplete"];
foreach($methodsArrays as $method){
$file = fopen($autoprefix."GameEngine/Prevention/".$method.".txt", "w");
@@ -2885,9 +2886,17 @@ class Automation {
{
$returningTroops = [];
for($i = 1; $i <= 11; $i++) $returningTroops['t'.$i] = $data['t'.$i] - $traped[$i] - $dead[$i];
// T4 hero port (Phase 6): bandages revive part of the
// losses when the hero comes home alive; revived units
// are written back to the attacks row (the source
// returnunitsComplete reads on arrival). No-op when off.
$returningTroops = HeroBattleBonus::applyBandages($from['owner'], $returningTroops, $dead, (int) $data['ref']);
$troopsTime = $units->getWalkingTroopsTime($from['wref'], $to['wref'], $from['owner'], $owntribe, $returningTroops, 1, 't');
$endtime = $database->getArtifactsValueInfluence($from['owner'], $from['wref'], 2, $troopsTime);
$endtime += $AttackArrivalTime;
// T4 hero port (Phase 5): the map shortens the return
// leg when the SURVIVING hero travels home. No-op when off.
$endtime = HeroBattleBonus::adjustReturnEndtime($from['owner'], $returningTroops['t11'], $AttackArrivalTime, $endtime);
if($type == 1){
if($from['owner'] == 3){ // fix natar report by ronix
$database->addNotice($to['owner'], $to['wref'], $targetally, 20, '' . addslashes($from['name']) . ' scouts ' . addslashes($to['name']) . '', $data2, $AttackArrivalTime);
@@ -3130,6 +3139,15 @@ class Automation {
$enforDefender = $defUnits['enforDefender'];
$enforcementarray = $defUnits['enforcementarray'];
// T4 hero port (Phase 8): cages capture animals on an
// UNOCCUPIED oasis before the fight - captured animals do
// not defend and are stationed at home as reinforcements.
// No-op when the flag is off, no hero rides along, the
// oasis is owned, or the attacker has no cages.
if ($isoasis != 0 && (int) $conqureby === 0 && (int) $data['t11'] > 0) {
$Defender = HeroBattleBonus::applyCages($from['owner'], (int) $data['from'], (int) $data['to'], $Defender);
}
// attacker army built — extracted to buildAttackerUnits() [#155]
$atkUnits = $this->buildAttackerUnits($dataarray[$data_num], $owntribe, $isoasis);
$Attacker = $atkUnits['Attacker'];
@@ -3363,7 +3381,10 @@ class Automation {
$avcrop = $resAfter['avcrop'];
// bounty distributed across the resources available after cranny protection (extracted to applyBounty() [#155])
$steal = $this->applyBounty([$avwood, $avclay, $aviron, $avcrop], $battlepart['bounty']);
// T4 hero port (Phase 5): thief sacks raise the carry
// bounty when the attacker's living hero joined the strike.
$heroBounty = (int) round($battlepart['bounty'] * HeroBattleBonus::raidMultiplier($from['owner'], $data['t11']));
$steal = $this->applyBounty([$avwood, $avclay, $aviron, $avcrop], $heroBounty);
//chiefing village — extracted to handleConquest() [#155]
if (!isset($village_destroyed)) $village_destroyed = 0;
@@ -3431,6 +3452,9 @@ class Automation {
$troopsTime = $units->getWalkingTroopsTime($from['wref'], $to['wref'], $from['owner'], $owntribe, $data, 1, 't');
$endtime = $database->getArtifactsValueInfluence($from['owner'], $from['wref'], 2, $troopsTime);
$endtime += $AttackArrivalTime;
// T4 hero port (Phase 5): map return-speed bonus, same as
// the combat return leg above. No-op when the flag is off.
$endtime = HeroBattleBonus::adjustReturnEndtime($from['owner'], $data['t11'], $AttackArrivalTime, $endtime);
$database->setMovementProc($data['moveid']);
$database->addMovement(4, $to['wref'], $from['wref'], $data['ref'], $AttackArrivalTime, $endtime);
@@ -4338,6 +4362,28 @@ class Automation {
}
/**
* T4 Hero port (Phase 3+4): process adventure arrivals (sort_type 20,
* rewards + ntype 26 report + return movement), returns (sort_type 21,
* hero re-enters units.hero, loot credited), top up offer lists, then
* finalize ended auctions and restock the NPC merchant.
* Fully feature-flagged - a no-op unless NEW_FUNCTIONS_HERO_T4 is enabled.
*/
private function heroAdventureComplete() {
if (!defined('NEW_FUNCTIONS_HERO_T4') || !NEW_FUNCTIONS_HERO_T4) {
return;
}
$adventures = new HeroAdventure();
$adventures->processArrivals();
$adventures->processReturns();
$adventures->generateOffersBatch();
$auctions = new HeroAuction();
$auctions->processFinished();
$auctions->seedNpcAuctions();
}
private function updateHero() {
global $database, $hero_levels;
+32 -1
View File
@@ -190,6 +190,16 @@ class Battle {
$h_ob = 1 + (0.010 * ($attackBonus / 5));
$h_db = 1 + (0.010 * ($defenceBonus / 5));
// T4 hero port (Phase 5): flat fighting strength from equipped items
// (weapons/cuirasses/shields) counts on whichever side the hero fights.
// No-ops to 0 when NEW_FUNCTIONS_HERO_T4 is off.
$itemStrength = HeroBattleBonus::statBonus($uid);
if ($itemStrength > 0) {
$h_atk += $itemStrength;
$h_di += $itemStrength;
$h_dc += $itemStrength;
}
return [
'heroid' => (int)$hero['heroid'],
'unit' => $heroUnit,
@@ -198,6 +208,8 @@ class Battle {
'dc' => $h_dc,
'ob' => $h_ob,
'db' => $h_db,
// T4 hero port: carried along for the offense/defense/damage hooks.
'uid' => (int)$uid,
'health' => isset($hero['health'])
? (int)$hero['health']
: 0
@@ -550,6 +562,14 @@ class Battle {
$units['Def_unit']['hero'] = $Defender['hero'];
$own_cdp += $defenderhero['dc'];
$own_dp += $defenderhero['di'];
// T4 hero port (Phase 5): weapon adds +N defense per unit of
// its type in the defending army, counted in both pools
// (see HeroBattleBonus::unitDefense). No-op when flag is off.
$itemDef = HeroBattleBonus::unitDefense($defenderhero['uid'] ?? 0, $Defender);
$own_dp += $itemDef;
$own_cdp += $itemDef;
$own_dp *= $defenderhero['db'];
$own_cdp *= $defenderhero['db'];
}
@@ -762,7 +782,14 @@ class Battle {
$ap *= $atkhero['ob'];
$cap *= $atkhero['ob'];
$ap += $atkhero['atk'];
// T4 hero port (Phase 5): hunting horn boosts the HERO's own
// contribution vs the Natars (uid 3); weapon adds +N attack per
// accompanying unit of its type. Neutral no-ops when the flag is off.
$ap += $atkhero['atk'] * HeroBattleBonus::natarMultiplier($atkhero['uid'] ?? 0, $DefenderID);
list($itemAp, $itemCap) = HeroBattleBonus::unitOffense($atkhero['uid'] ?? 0, $Attacker, $calvaryLookup);
$ap += $itemAp;
$cap += $itemCap;
}
if ($offhero > 0 || $hero_strenght > 0) {
@@ -1136,6 +1163,10 @@ class Battle {
$hero_health = (int)$fdb['health'];
$damage_health = round(100 * $result[1]);
// T4 hero port (Phase 5): armors reduce the health damage the
// hero takes in one battle (flat, floored at 0). No-op when off.
$damage_health = HeroBattleBonus::reduceDamage($atkhero['uid'] ?? 0, $damage_health);
if ($hero_health <= $damage_health || $damage_health > 90) {
$result['casualties_attacker'][11] = 1;
+307
View File
@@ -0,0 +1,307 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : hero_items.php ##
## Type : Static Data / Catalog for T4 Hero port ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
## ##
## SINGLE SOURCE OF TRUTH for the T4 hero port (Phase 2 - full catalog, ##
## built from the design spreadsheet "Obiecte_Erou_Travian.xlsx"). ##
## Pure data + guarded constants, no side effects - safe to include anywhere. ##
## ##
## Display names are stored as English strings in the catalog. For future ##
## localisation, heroItemName() checks for a HERO_ITEM_<id> lang constant ##
## first and falls back to the English catalog name. ##
## ##
#################################################################################
/* ============================================================================
* GAME CONSTANTS (unchanged from Phase 1; guarded against re-includes)
* movement.sort_type in use: 0,2,3,4,5,6,10,11,12,13 -> 20/21 free
* attacks.attack_type in use: 1,2 -> 9 free
* ndata.ntype mapped up to 25 (settlers) -> 26 free
* ------------------------------------------------------------------------- */
if (!defined('MOVEMENT_ADVENTURE_OUT')) define('MOVEMENT_ADVENTURE_OUT', 20);
if (!defined('MOVEMENT_ADVENTURE_BACK')) define('MOVEMENT_ADVENTURE_BACK', 21);
if (!defined('ATTACK_TYPE_ADVENTURE')) define('ATTACK_TYPE_ADVENTURE', 9);
if (!defined('NTYPE_ADVENTURE_REPORT')) define('NTYPE_ADVENTURE_REPORT', 26);
if (!defined('NTYPE_AUCTION_REPORT')) define('NTYPE_AUCTION_REPORT', 27);
/* Equipment slots */
if (!defined('HSLOT_HELMET')) define('HSLOT_HELMET', 1);
if (!defined('HSLOT_BODY')) define('HSLOT_BODY', 2);
if (!defined('HSLOT_RIGHT')) define('HSLOT_RIGHT', 3); // weapon
if (!defined('HSLOT_LEFT')) define('HSLOT_LEFT', 4); // shield/horn/sack/map/pennant/standard
if (!defined('HSLOT_SHOES')) define('HSLOT_SHOES', 5); // boots + spurs
if (!defined('HSLOT_HORSE')) define('HSLOT_HORSE', 6);
if (!defined('HSLOT_BAG')) define('HSLOT_BAG', 7); // stackable consumables
/* ============================================================================
* BONUS TYPES (keys inside $item['bonus'])
* Flat vs percent is documented per constant. Consumers: HeroItems::getBonuses()
* (Phase 2), Battle/Automation/a2b integration (Phase 5).
* ------------------------------------------------------------------------- */
if (!defined('HB_FIGHT')) define('HB_FIGHT', 'fight_strength'); // flat hero fighting strength
if (!defined('HB_UNIT_BONUS')) define('HB_UNIT_BONUS', 'unit_bonus'); // ['unit'=>uX,'per_unit'=>N] +N off & +N def per unit of that type in the hero's army
if (!defined('HB_VS_NATARS')) define('HB_VS_NATARS', 'vs_natars'); // +% hero strength vs Natars
if (!defined('HB_RAID')) define('HB_RAID', 'raid_percent'); // +% resources stolen on raids
if (!defined('HB_RETURN_SPEED')) define('HB_RETURN_SPEED', 'return_speed'); // +% troop return speed
if (!defined('HB_SPEED_OWN')) define('HB_SPEED_OWN', 'speed_own'); // +% speed between own villages
if (!defined('HB_SPEED_ALLY')) define('HB_SPEED_ALLY', 'speed_ally'); // +% speed between ally villages
if (!defined('HB_XP')) define('HB_XP', 'xp_percent'); // +% hero experience
if (!defined('HB_REGEN_HP')) define('HB_REGEN_HP', 'regen_hp'); // flat +HP per day
if (!defined('HB_CP')) define('HB_CP', 'culture_points'); // flat +culture points per day
if (!defined('HB_TRAIN_CAV')) define('HB_TRAIN_CAV', 'train_cavalry'); // -% cavalry training time
if (!defined('HB_TRAIN_INF')) define('HB_TRAIN_INF', 'train_infantry'); // -% infantry training time
if (!defined('HB_DMG_REDUCE')) define('HB_DMG_REDUCE', 'damage_reduce'); // flat damage reduction per hit
if (!defined('HB_TROOP_SPEED_20')) define('HB_TROOP_SPEED_20', 'troop_speed_20'); // +% troop speed beyond 20 tiles
if (!defined('HB_MOUNT_SPEED')) define('HB_MOUNT_SPEED', 'mount_speed'); // flat +fields/hour, ONLY while a horse is equipped (spurs)
if (!defined('HB_HORSE_SPEED')) define('HB_HORSE_SPEED', 'horse_speed'); // flat hero speed in fields/hour (horse)
/* consumables */
if (!defined('HB_HEAL_SELF')) define('HB_HEAL_SELF', 'heal_self'); // +1% hero HP per unit (max 99%, hero must be alive)
if (!defined('HB_SCROLL')) define('HB_SCROLL', 'scroll_xp'); // +XP per scroll
if (!defined('HB_BUCKET')) define('HB_BUCKET', 'bucket'); // instant free revive
if (!defined('HB_LOYALTY')) define('HB_LOYALTY', 'loyalty'); // +1% own village loyalty per tablet (max 125%)
if (!defined('HB_RESET')) define('HB_RESET', 'reset_points'); // reset attribute points
if (!defined('HB_ARTWORK')) define('HB_ARTWORK', 'artwork_cp'); // CP = daily production (cap: 5000 normal / 2500 speed)
if (!defined('HB_HEAL_TROOP')) define('HB_HEAL_TROOP', 'heal_troops'); // heals % of the hero's army (bandages)
if (!defined('HB_CAGE')) define('HB_CAGE', 'cage'); // traps 1 oasis animal per cage
/* ============================================================================
* ITEM CATALOG
* itemid => [
* 'name' => English display name,
* 'slot' => HSLOT_*,
* 'tier' => 1..3,
* 'bonus' => [HB_* => value, ...],
* 'unit' => uX (weapons only: hero must BE this unit to equip),
* 'x2_on_speed' => true (value doubles on speed servers, per design sheet:
* "+3 (1x) / +6 (3x)" - runtime multiplies by 2 when SPEED >= 3),
* 'requires_horse' => true (spurs: bonus only counts with a horse equipped)
* ]
*
* ID ranges: 1-15 helmets | 20-31 body | 40-57 left hand | 60-68 shoes
* 70-72 horses | 101-145 weapons | 200-208 consumables
* ------------------------------------------------------------------------- */
$heroItemCatalog = array(
/* ---------------- HELMETS (sheet: Coifuri) ---------------- */
1 => array('name' => 'Helmet of Awareness', 'slot' => HSLOT_HELMET, 'tier' => 1, 'bonus' => array(HB_XP => 15)),
2 => array('name' => 'Helmet of Enlightenment', 'slot' => HSLOT_HELMET, 'tier' => 2, 'bonus' => array(HB_XP => 20)),
3 => array('name' => 'Helmet of Wisdom', 'slot' => HSLOT_HELMET, 'tier' => 3, 'bonus' => array(HB_XP => 25)),
4 => array('name' => 'Helmet of Regeneration', 'slot' => HSLOT_HELMET, 'tier' => 1, 'bonus' => array(HB_REGEN_HP => 10)),
5 => array('name' => 'Helmet of Health', 'slot' => HSLOT_HELMET, 'tier' => 2, 'bonus' => array(HB_REGEN_HP => 15)),
6 => array('name' => 'Helmet of Healing', 'slot' => HSLOT_HELMET, 'tier' => 3, 'bonus' => array(HB_REGEN_HP => 20)),
7 => array('name' => 'Helmet of the Gladiator', 'slot' => HSLOT_HELMET, 'tier' => 1, 'bonus' => array(HB_CP => 100), 'x2_on_speed' => false), // sheet: 100/day (1x), 50 (3x) -> HALVED on speed; handled below
8 => array('name' => 'Helmet of the Tribune', 'slot' => HSLOT_HELMET, 'tier' => 2, 'bonus' => array(HB_CP => 400), 'x2_on_speed' => false),
9 => array('name' => 'Helmet of the Consul', 'slot' => HSLOT_HELMET, 'tier' => 3, 'bonus' => array(HB_CP => 800), 'x2_on_speed' => false),
10 => array('name' => 'Helmet of the Horseman', 'slot' => HSLOT_HELMET, 'tier' => 1, 'bonus' => array(HB_TRAIN_CAV => 10)),
11 => array('name' => 'Helmet of the Cavalry', 'slot' => HSLOT_HELMET, 'tier' => 2, 'bonus' => array(HB_TRAIN_CAV => 15)),
12 => array('name' => 'Helmet of the Heavy Cavalry', 'slot' => HSLOT_HELMET, 'tier' => 3, 'bonus' => array(HB_TRAIN_CAV => 20)),
13 => array('name' => 'Helmet of the Mercenary', 'slot' => HSLOT_HELMET, 'tier' => 1, 'bonus' => array(HB_TRAIN_INF => 10)),
14 => array('name' => 'Helmet of the Warrior', 'slot' => HSLOT_HELMET, 'tier' => 2, 'bonus' => array(HB_TRAIN_INF => 15)),
15 => array('name' => 'Helmet of the Ruler', 'slot' => HSLOT_HELMET, 'tier' => 3, 'bonus' => array(HB_TRAIN_INF => 20)),
/* ---------------- BODY ARMOUR (sheet: Armuri) ---------------- */
20 => array('name' => 'Light Breastplate of Regeneration', 'slot' => HSLOT_BODY, 'tier' => 1, 'bonus' => array(HB_REGEN_HP => 20)),
21 => array('name' => 'Breastplate of Regeneration', 'slot' => HSLOT_BODY, 'tier' => 2, 'bonus' => array(HB_REGEN_HP => 30)),
22 => array('name' => 'Heavy Breastplate of Regeneration', 'slot' => HSLOT_BODY, 'tier' => 3, 'bonus' => array(HB_REGEN_HP => 40)),
23 => array('name' => 'Light Armor', 'slot' => HSLOT_BODY, 'tier' => 1, 'bonus' => array(HB_DMG_REDUCE => 4, HB_REGEN_HP => 10)),
24 => array('name' => 'Armor', 'slot' => HSLOT_BODY, 'tier' => 2, 'bonus' => array(HB_DMG_REDUCE => 6, HB_REGEN_HP => 15)),
25 => array('name' => 'Heavy Armor', 'slot' => HSLOT_BODY, 'tier' => 3, 'bonus' => array(HB_DMG_REDUCE => 8, HB_REGEN_HP => 20)),
26 => array('name' => 'Light Cuirass', 'slot' => HSLOT_BODY, 'tier' => 1, 'bonus' => array(HB_FIGHT => 500)),
27 => array('name' => 'Cuirass', 'slot' => HSLOT_BODY, 'tier' => 2, 'bonus' => array(HB_FIGHT => 1000)),
28 => array('name' => 'Heavy Cuirass', 'slot' => HSLOT_BODY, 'tier' => 3, 'bonus' => array(HB_FIGHT => 1500)),
29 => array('name' => 'Light Segmented Armor', 'slot' => HSLOT_BODY, 'tier' => 1, 'bonus' => array(HB_DMG_REDUCE => 3, HB_FIGHT => 250)),
30 => array('name' => 'Segmented Armor', 'slot' => HSLOT_BODY, 'tier' => 2, 'bonus' => array(HB_DMG_REDUCE => 4, HB_FIGHT => 500)),
31 => array('name' => 'Heavy Segmented Armor', 'slot' => HSLOT_BODY, 'tier' => 3, 'bonus' => array(HB_DMG_REDUCE => 5, HB_FIGHT => 750)),
/* ---------------- LEFT HAND (sheets: Mana_Stanga + Diverse) ---------------- */
40 => array('name' => 'Small Shield', 'slot' => HSLOT_LEFT, 'tier' => 1, 'bonus' => array(HB_FIGHT => 500)),
41 => array('name' => 'Shield', 'slot' => HSLOT_LEFT, 'tier' => 2, 'bonus' => array(HB_FIGHT => 1000)),
42 => array('name' => 'Great Shield', 'slot' => HSLOT_LEFT, 'tier' => 3, 'bonus' => array(HB_FIGHT => 1500)),
43 => array('name' => 'Small Hunting Horn', 'slot' => HSLOT_LEFT, 'tier' => 1, 'bonus' => array(HB_VS_NATARS => 20)),
44 => array('name' => 'Hunting Horn', 'slot' => HSLOT_LEFT, 'tier' => 2, 'bonus' => array(HB_VS_NATARS => 25)),
45 => array('name' => 'Great Hunting Horn', 'slot' => HSLOT_LEFT, 'tier' => 3, 'bonus' => array(HB_VS_NATARS => 30)),
46 => array('name' => "Thief's Satchel", 'slot' => HSLOT_LEFT, 'tier' => 1, 'bonus' => array(HB_RAID => 10)),
47 => array('name' => "Thief's Bag", 'slot' => HSLOT_LEFT, 'tier' => 2, 'bonus' => array(HB_RAID => 15)),
48 => array('name' => "Thief's Sack", 'slot' => HSLOT_LEFT, 'tier' => 3, 'bonus' => array(HB_RAID => 20)),
49 => array('name' => 'Small Map', 'slot' => HSLOT_LEFT, 'tier' => 1, 'bonus' => array(HB_RETURN_SPEED => 30)),
50 => array('name' => 'Map', 'slot' => HSLOT_LEFT, 'tier' => 2, 'bonus' => array(HB_RETURN_SPEED => 40)),
51 => array('name' => 'Great Map', 'slot' => HSLOT_LEFT, 'tier' => 3, 'bonus' => array(HB_RETURN_SPEED => 50)),
52 => array('name' => 'Small Pennant', 'slot' => HSLOT_LEFT, 'tier' => 1, 'bonus' => array(HB_SPEED_OWN => 30)),
53 => array('name' => 'Pennant', 'slot' => HSLOT_LEFT, 'tier' => 2, 'bonus' => array(HB_SPEED_OWN => 40)),
54 => array('name' => 'Great Pennant', 'slot' => HSLOT_LEFT, 'tier' => 3, 'bonus' => array(HB_SPEED_OWN => 50)),
55 => array('name' => 'Small Standard', 'slot' => HSLOT_LEFT, 'tier' => 1, 'bonus' => array(HB_SPEED_ALLY => 15)),
56 => array('name' => 'Standard', 'slot' => HSLOT_LEFT, 'tier' => 2, 'bonus' => array(HB_SPEED_ALLY => 20)),
57 => array('name' => 'Great Standard', 'slot' => HSLOT_LEFT, 'tier' => 3, 'bonus' => array(HB_SPEED_ALLY => 25)),
/* ---------------- SHOES (sheet: Incaltaminte_Cai - boots + spurs) ---------------- */
60 => array('name' => 'Boots of Regeneration', 'slot' => HSLOT_SHOES, 'tier' => 1, 'bonus' => array(HB_REGEN_HP => 10)),
61 => array('name' => 'Boots of Recovery', 'slot' => HSLOT_SHOES, 'tier' => 2, 'bonus' => array(HB_REGEN_HP => 15)),
62 => array('name' => 'Boots of Healing', 'slot' => HSLOT_SHOES, 'tier' => 3, 'bonus' => array(HB_REGEN_HP => 20)),
63 => array('name' => 'Boots of the Mercenary', 'slot' => HSLOT_SHOES, 'tier' => 1, 'bonus' => array(HB_TROOP_SPEED_20 => 25)),
64 => array('name' => 'Boots of the Warrior', 'slot' => HSLOT_SHOES, 'tier' => 2, 'bonus' => array(HB_TROOP_SPEED_20 => 50)),
65 => array('name' => 'Boots of the Ruler', 'slot' => HSLOT_SHOES, 'tier' => 3, 'bonus' => array(HB_TROOP_SPEED_20 => 75)),
// Spurs share the shoes slot (worn on boots) and only apply while a horse is equipped.
// Design sheet: "+3 f/h (1x) / +6 (3x)" -> base value is the 1x one, doubled on speed servers.
66 => array('name' => 'Small Spurs', 'slot' => HSLOT_SHOES, 'tier' => 1, 'bonus' => array(HB_MOUNT_SPEED => 3), 'x2_on_speed' => true, 'requires_horse' => true),
67 => array('name' => 'Spurs', 'slot' => HSLOT_SHOES, 'tier' => 2, 'bonus' => array(HB_MOUNT_SPEED => 4), 'x2_on_speed' => true, 'requires_horse' => true),
68 => array('name' => 'Great Spurs', 'slot' => HSLOT_SHOES, 'tier' => 3, 'bonus' => array(HB_MOUNT_SPEED => 5), 'x2_on_speed' => true, 'requires_horse' => true),
/* ---------------- HORSES (sheet: Incaltaminte_Cai) ---------------- */
70 => array('name' => 'Riding Horse', 'slot' => HSLOT_HORSE, 'tier' => 1, 'bonus' => array(HB_HORSE_SPEED => 14), 'x2_on_speed' => true),
71 => array('name' => 'Thoroughbred', 'slot' => HSLOT_HORSE, 'tier' => 2, 'bonus' => array(HB_HORSE_SPEED => 17), 'x2_on_speed' => true),
72 => array('name' => 'Warhorse', 'slot' => HSLOT_HORSE, 'tier' => 3, 'bonus' => array(HB_HORSE_SPEED => 20), 'x2_on_speed' => true),
/* ---------------- WEAPONS (sheets: Romani / Barbari / Daci) ----------------
* Weapons are unit-bound: the hero must be trained from that unit.
* Each gives flat fight strength (500/1000/1500 by tier) plus
* +N off AND +N def per unit of that type accompanying the hero. */
// Romans - u1 Legionnaire
101 => array('name' => "Legionnaire's Short Sword", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 1, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 1, 'per_unit' => 3))),
102 => array('name' => "Legionnaire's Sword", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 1, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 1, 'per_unit' => 4))),
103 => array('name' => "Legionnaire's Long Sword", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 1, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 1, 'per_unit' => 5))),
// Romans - u2 Praetorian
104 => array('name' => "Praetorian's Short Sword", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 2, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 2, 'per_unit' => 3))),
105 => array('name' => "Praetorian's Sword", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 2, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 2, 'per_unit' => 4))),
106 => array('name' => "Praetorian's Long Sword", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 2, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 2, 'per_unit' => 5))),
// Romans - u3 Imperian
107 => array('name' => "Imperian's Short Sword", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 3, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 3, 'per_unit' => 3))),
108 => array('name' => "Imperian's Sword", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 3, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 3, 'per_unit' => 4))),
109 => array('name' => "Imperian's Long Sword", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 3, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 3, 'per_unit' => 5))),
// Romans - u5 Equites Imperatoris
110 => array('name' => "Imperatoris' Short Saber", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 5, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 5, 'per_unit' => 9))),
111 => array('name' => "Imperatoris' Saber", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 5, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 5, 'per_unit' => 12))),
112 => array('name' => "Imperatoris' Long Saber", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 5, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 5, 'per_unit' => 15))),
// Romans - u6 Equites Caesaris
113 => array('name' => "Caesaris' Light Lance", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 6, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 6, 'per_unit' => 12))),
114 => array('name' => "Caesaris' Lance", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 6, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 6, 'per_unit' => 16))),
115 => array('name' => "Caesaris' Heavy Lance", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 6, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 6, 'per_unit' => 20))),
// Teutons - u11 Clubswinger
116 => array('name' => "Clubswinger's Cudgel", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 11, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 11, 'per_unit' => 3))),
117 => array('name' => "Clubswinger's Club", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 11, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 11, 'per_unit' => 4))),
118 => array('name' => "Clubswinger's Mace", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 11, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 11, 'per_unit' => 5))),
// Teutons - u12 Spearman
119 => array('name' => "Spearman's Pitchfork", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 12, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 12, 'per_unit' => 3))),
120 => array('name' => "Spearman's Pike", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 12, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 12, 'per_unit' => 4))),
121 => array('name' => "Spearman's Spear", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 12, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 12, 'per_unit' => 5))),
// Teutons - u13 Axeman
122 => array('name' => "Axeman's Hatchet", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 13, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 13, 'per_unit' => 3))),
123 => array('name' => "Axeman's Axe", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 13, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 13, 'per_unit' => 4))),
124 => array('name' => "Axeman's Battle Axe", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 13, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 13, 'per_unit' => 5))),
// Teutons - u15 Paladin
125 => array('name' => "Paladin's Small Hammer", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 15, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 15, 'per_unit' => 6))),
126 => array('name' => "Paladin's Hammer", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 15, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 15, 'per_unit' => 8))),
127 => array('name' => "Paladin's Sledgehammer", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 15, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 15, 'per_unit' => 10))),
// Teutons - u16 Teutonic Knight
128 => array('name' => "Teutonic Knight's Short Sword", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 16, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 16, 'per_unit' => 9))),
129 => array('name' => "Teutonic Knight's Sword", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 16, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 16, 'per_unit' => 12))),
130 => array('name' => "Teutonic Knight's Long Sword", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 16, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 16, 'per_unit' => 15))),
// Gauls - u21 Phalanx (design sheet tribe "Daci": Scutier)
131 => array('name' => "Phalanx's Pitchfork", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 21, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 21, 'per_unit' => 3))),
132 => array('name' => "Phalanx's Spear", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 21, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 21, 'per_unit' => 4))),
133 => array('name' => "Phalanx's Lance", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 21, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 21, 'per_unit' => 5))),
// Gauls - u22 Swordsman (Pedestras)
134 => array('name' => "Swordsman's Short Sword", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 22, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 22, 'per_unit' => 3))),
135 => array('name' => "Swordsman's Sword", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 22, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 22, 'per_unit' => 4))),
136 => array('name' => "Swordsman's Long Sword", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 22, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 22, 'per_unit' => 5))),
// Gauls - u24 Theutates Thunder (Calaret Fulger)
137 => array('name' => "Thunder's Short Bow", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 24, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 24, 'per_unit' => 6))),
138 => array('name' => "Thunder's Bow", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 24, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 24, 'per_unit' => 8))),
139 => array('name' => "Thunder's Long Bow", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 24, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 24, 'per_unit' => 10))),
// Gauls - u25 Druidrider
140 => array('name' => "Druidrider's Baton", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 25, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 25, 'per_unit' => 6))),
141 => array('name' => "Druidrider's Staff", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 25, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 25, 'per_unit' => 8))),
142 => array('name' => "Druidrider's Great Staff", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 25, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 25, 'per_unit' => 10))),
// Gauls - u26 Haeduan (Tarabostes)
143 => array('name' => "Haeduan's Light Lance", 'slot' => HSLOT_RIGHT, 'tier' => 1, 'unit' => 26, 'bonus' => array(HB_FIGHT => 500, HB_UNIT_BONUS => array('unit' => 26, 'per_unit' => 9))),
144 => array('name' => "Haeduan's Lance", 'slot' => HSLOT_RIGHT, 'tier' => 2, 'unit' => 26, 'bonus' => array(HB_FIGHT => 1000, HB_UNIT_BONUS => array('unit' => 26, 'per_unit' => 12))),
145 => array('name' => "Haeduan's Heavy Lance", 'slot' => HSLOT_RIGHT, 'tier' => 3, 'unit' => 26, 'bonus' => array(HB_FIGHT => 1500, HB_UNIT_BONUS => array('unit' => 26, 'per_unit' => 15))),
/* ---------------- CONSUMABLES (sheet: Consumabile; stackable, bag) ---------------- */
200 => array('name' => 'Ointment', 'slot' => HSLOT_BAG, 'tier' => 1, 'bonus' => array(HB_HEAL_SELF => 1)), // +1% HP per unit, max 99%, hero must be alive
201 => array('name' => 'Scroll', 'slot' => HSLOT_BAG, 'tier' => 1, 'bonus' => array(HB_SCROLL => 10)), // +10 XP per scroll
202 => array('name' => 'Bucket of Water', 'slot' => HSLOT_BAG, 'tier' => 2, 'bonus' => array(HB_BUCKET => 1)), // instant revive, no resource cost
203 => array('name' => 'Law Tablet', 'slot' => HSLOT_BAG, 'tier' => 2, 'bonus' => array(HB_LOYALTY => 1)), // +1% own village loyalty, max 125%
204 => array('name' => 'Book of Wisdom', 'slot' => HSLOT_BAG, 'tier' => 3, 'bonus' => array(HB_RESET => 1)), // reset attribute points
205 => array('name' => 'Artwork', 'slot' => HSLOT_BAG, 'tier' => 3, 'bonus' => array(HB_ARTWORK => 1)), // CP = daily production, cap 5000 (1x) / 2500 (speed)
206 => array('name' => 'Small Bandage', 'slot' => HSLOT_BAG, 'tier' => 1, 'bonus' => array(HB_HEAL_TROOP => 25)), // heals 25% of hero's army
207 => array('name' => 'Bandage', 'slot' => HSLOT_BAG, 'tier' => 2, 'bonus' => array(HB_HEAL_TROOP => 33)), // heals 33% of hero's army
208 => array('name' => 'Cage', 'slot' => HSLOT_BAG, 'tier' => 1, 'bonus' => array(HB_CAGE => 1)), // traps 1 oasis animal per cage
);
/* Helmets 7-9 (culture) are HALVED on speed servers per the design sheet
* ("+100/day (1x) / +50 (3x)"). Runtime consumers must apply:
* value = (SPEED >= 3) ? value / 2 : value
* whenever 'x2_on_speed' === false is explicitly set (see HeroItems::scaledBonusValue()). */
/* ============================================================================
* ADVENTURE CONFIGURATION (Phase 1, itemids updated to the new catalog;
* equipment drops added per design feedback: adventures are the primary
* T4 source of gear, rarer than consumables and weighted toward low tiers)
* ------------------------------------------------------------------------- */
$heroAdventureConfig = array(
'offer_lifetime' => 24 * 3600,
'max_offers' => 3,
'refresh_interval' => 8 * 3600,
// Tier weights for equipment drops (percent, must sum to 100).
'equip_tier_weights' => array(1 => 70, 2 => 25, 3 => 5),
0 => array( // NORMAL
'label' => 'HERO_ADV_NORMAL',
'exp' => array(6, 12),
'silver' => array(0, 40),
'hp_loss' => array(0, 8),
'resources' => array(0, 400),
'equip_chance' => 3, // % chance: 1 random piece of gear
'item_chance' => 5, // % chance: consumables (rolled only if no gear dropped)
'consumable' => array(200, 201, 208), // Ointment, Scroll, Cage
),
1 => array( // HARD
'label' => 'HERO_ADV_HARD',
'exp' => array(18, 30),
'silver' => array(30, 120),
'hp_loss' => array(10, 35),
'resources' => array(200, 1200),
'equip_chance' => 10,
'item_chance' => 20,
'consumable' => array(200, 201, 203, 206, 208), // + Law Tablet, Small Bandage
),
);
/* Convenience index: itemids grouped by slot. */
$heroSlotIndex = array();
foreach ($heroItemCatalog as $iid => $def) {
$heroSlotIndex[$def['slot']][] = $iid;
}
/**
* Resolve the display name of an item. Checks for a HERO_ITEM_<id> lang
* constant first (future localisation hook), falls back to the English
* catalog name, and finally to 'Unknown item' for ids not in the catalog.
*/
if (!function_exists('heroItemName')) {
function heroItemName($itemid) {
global $heroItemCatalog;
$const = 'HERO_ITEM_' . (int) $itemid;
if (defined($const)) {
return constant($const);
}
return isset($heroItemCatalog[$itemid]['name']) ? $heroItemCatalog[$itemid]['name'] : 'Unknown item';
}
}
+9
View File
@@ -5863,6 +5863,15 @@ References: User ID/Message ID, Mode
case 8:
$q = "SELECT * FROM " . TB_PREFIX . "movement, " . TB_PREFIX . "attacks where " . TB_PREFIX . "movement.`" . $where . "` IN(".implode(', ', $village).") and " . TB_PREFIX . "movement.ref = " . TB_PREFIX . "attacks.id and " . TB_PREFIX . "movement.proc = 0 and " . TB_PREFIX . "movement.sort_type = 3 and " . TB_PREFIX . "attacks.attack_type = 1 ORDER BY endtime ASC";
break;
// T4 hero port: hero travelling to an adventure (plain select,
// ref points to hero_adventure - NOT to attacks, so no join).
case 20:
$q = "SELECT * FROM " . TB_PREFIX . "movement where " . TB_PREFIX . "movement.`" . $where . "` IN(".implode(', ', $village).") and sort_type = 20 and proc = 0 ORDER BY endtime ASC";
break;
// T4 hero port: hero returning from an adventure.
case 21:
$q = "SELECT * FROM " . TB_PREFIX . "movement where " . TB_PREFIX . "movement.`" . $where . "` IN(".implode(', ', $village).") and sort_type = 21 and proc = 0 ORDER BY endtime ASC";
break;
case 34:
$q = "SELECT * FROM " . TB_PREFIX . "movement, " . TB_PREFIX . "attacks where " . TB_PREFIX . "movement.`" . $where . "` IN(".implode(', ', $village).") and " . TB_PREFIX . "movement.ref = " . TB_PREFIX . "attacks.id and " . TB_PREFIX . "movement.proc = 0 and " . TB_PREFIX . "movement.sort_type = 3 or " . TB_PREFIX . "movement.`" . $where . "` IN(".implode(', ', $village).") and " . TB_PREFIX . "movement.ref = " . TB_PREFIX . "attacks.id and " . TB_PREFIX . "movement.proc = 0 and " . TB_PREFIX . "movement.sort_type = 4 ORDER BY endtime ASC";
break;
+643
View File
@@ -0,0 +1,643 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : HeroAdventure.php ##
## Type : T4 Hero port - adventures backend (Phase 3) ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
## ##
## Adventure lifecycle: ##
## offer (status 0) -> running (status 1, movement sort_type 20) ##
## -> arrival processed by Automation (rewards + ntype 26 report, ##
## status 2, return movement sort_type 21 carrying resources) ##
## -> return processed by Automation (hero back in units.hero, ##
## resources credited) ##
## ##
## Death: if HP loss would drop health to 0, the hero dies AT the site ##
## (dead=1, health=0, loot forfeited, no return movement). units.hero was ##
## already decremented at send - identical end state to dying in battle, ##
## so the existing revive flow (37_revive.tpl + handleReviveCompletion) ##
## works unchanged. ##
## ##
## XP: only `experience` is incremented (with the helmet HB_XP% bonus). ##
## Level-up/points are intentionally NOT touched here - Automation's ##
## updateHero()/calculateLevelUp() already handles that every tick. ##
## ##
#################################################################################
class HeroAdventure
{
/** Hero base speed on foot, fields/hour (T4 convention). */
const BASE_HERO_SPEED = 7;
/** How far from the hero's village adventure sites are picked (tiles). */
const SITE_MIN_RADIUS = 3;
const SITE_MAX_RADIUS = 25;
/** Max users to top-up with fresh offers per automation tick (bounded work). */
const BATCH_LIMIT = 50;
/** startAdventure() result codes */
const START_OK = 1;
const START_INVALID = 0; // offer missing/expired/not yours/already running
const START_NO_HERO = -1; // no living hero
const START_HERO_AWAY = -2; // hero not at home village
/** @var mysqli */
private $db;
public function __construct()
{
global $database;
$this->db = $database->dblink;
global $heroItemCatalog, $heroAdventureConfig;
if (!isset($heroAdventureConfig)) {
include_once __DIR__ . '/Data/hero_items.php';
}
}
/* =========================================================================
* OFFERS
* ===================================================================== */
/** Non-expired, still-available offers for a user. */
public function getOffers($uid)
{
$uid = (int) $uid; $now = time();
$stmt = $this->db->prepare(
"SELECT id, uid, wref, difficulty, duration, created, expire, status, moveid
FROM " . TB_PREFIX . "hero_adventure
WHERE uid = ? AND status = 0 AND expire > ?
ORDER BY difficulty ASC, duration ASC"
);
$stmt->bind_param('ii', $uid, $now);
$stmt->execute();
$res = $stmt->get_result();
$out = array();
while ($row = $res->fetch_assoc()) {
$out[] = $row;
}
$stmt->close();
return $out;
}
/** The adventure the hero is currently running, or null. */
public function getRunning($uid)
{
$uid = (int) $uid;
$stmt = $this->db->prepare(
"SELECT a.id, a.wref, a.difficulty, a.moveid, m.endtime, m.sort_type
FROM " . TB_PREFIX . "hero_adventure a
JOIN " . TB_PREFIX . "movement m ON m.moveid = a.moveid
WHERE a.uid = ? AND a.status = 1 AND m.proc = 0 LIMIT 1"
);
$stmt->bind_param('i', $uid);
$stmt->execute();
$res = $stmt->get_result();
$row = $res->fetch_assoc();
$stmt->close();
return $row ?: null;
}
/**
* Top up a single user's offer list (expires stale ones first).
* Respects max_offers and refresh_interval from $heroAdventureConfig.
* Returns the number of offers created.
*/
public function generateOffers($uid)
{
global $database, $heroAdventureConfig;
$uid = (int) $uid; $now = time();
// Living hero needed - offers are anchored to the hero's village.
$hero = $this->getLivingHero($uid);
if (!$hero) {
return 0;
}
// Expire stale offers (kept as rows with status untouched would clutter
// the uid_status index; a hard delete of expired *available* offers is safe).
$stmt = $this->db->prepare(
"DELETE FROM " . TB_PREFIX . "hero_adventure WHERE uid = ? AND status = 0 AND expire <= ?"
);
$stmt->bind_param('ii', $uid, $now);
$stmt->execute();
$stmt->close();
// Respect refresh interval: don't regenerate too often.
$stmt = $this->db->prepare(
"SELECT COUNT(*) AS cnt, COALESCE(MAX(created), 0) AS latest
FROM " . TB_PREFIX . "hero_adventure WHERE uid = ? AND status = 0"
);
$stmt->bind_param('i', $uid);
$stmt->execute();
$res = $stmt->get_result();
$info = $res->fetch_assoc();
$stmt->close();
$have = (int) $info['cnt'];
$latest = (int) $info['latest'];
$max = (int) $heroAdventureConfig['max_offers'];
if ($have >= $max) {
return 0;
}
if ($latest > 0 && ($now - $latest) < (int) $heroAdventureConfig['refresh_interval']) {
return 0;
}
// Pick random unoccupied tiles around the hero's village.
$coor = $database->getCoor($hero['wref']);
if (!$coor) {
return 0;
}
$x = (int) $coor['x']; $y = (int) $coor['y']; $r = self::SITE_MAX_RADIUS;
$need = $max - $have;
// Min radius enforced in SQL so LIMIT never undercounts (the bounding
// box doesn't wrap around the map seam, so plain coordinate distance
// is exact within it).
$stmt = $this->db->prepare(
"SELECT id, x, y FROM " . TB_PREFIX . "wdata
WHERE occupied = 0 AND id <> ?
AND x BETWEEN ? AND ? AND y BETWEEN ? AND ?
AND (POW(x - ?, 2) + POW(y - ?, 2)) >= ?
ORDER BY RAND() LIMIT ?"
);
$wref = (int) $hero['wref'];
$x1 = $x - $r; $x2 = $x + $r; $y1 = $y - $r; $y2 = $y + $r;
$minSq = self::SITE_MIN_RADIUS * self::SITE_MIN_RADIUS;
$stmt->bind_param('iiiiiiiii', $wref, $x1, $x2, $y1, $y2, $x, $y, $minSq, $need);
$stmt->execute();
$res = $stmt->get_result();
$tiles = array();
while ($row = $res->fetch_assoc()) {
$tiles[] = $row;
}
$stmt->close();
$created = 0;
foreach ($tiles as $tile) {
$dist = $database->getDistance($x, $y, $tile['x'], $tile['y']);
// Hard adventures ~30% of offers.
$difficulty = (mt_rand(1, 100) <= 30) ? 1 : 0;
$duration = $this->travelSeconds($uid, $dist);
$expire = $now + (int) $heroAdventureConfig['offer_lifetime'];
$stmt = $this->db->prepare(
"INSERT INTO " . TB_PREFIX . "hero_adventure
(uid, wref, difficulty, duration, created, expire, status, moveid)
VALUES (?, ?, ?, ?, ?, ?, 0, 0)"
);
$tileId = (int) $tile['id'];
$stmt->bind_param('iiiiii', $uid, $tileId, $difficulty, $duration, $now, $expire);
$stmt->execute();
$stmt->close();
$created++;
}
return $created;
}
/**
* Batch top-up used by Automation: refresh offers for users with living,
* at-home heroes that are below max_offers. Work bounded by BATCH_LIMIT.
*/
public function generateOffersBatch()
{
global $heroAdventureConfig;
$now = time();
$threshold = $now - (int) $heroAdventureConfig['refresh_interval'];
$max = (int) $heroAdventureConfig['max_offers'];
$limit = self::BATCH_LIMIT;
// Users with a living hero whose available-offer count is below max
// and whose newest offer (if any) is older than the refresh interval.
$stmt = $this->db->prepare(
"SELECT h.uid
FROM " . TB_PREFIX . "hero h
LEFT JOIN " . TB_PREFIX . "hero_adventure a
ON a.uid = h.uid AND a.status = 0 AND a.expire > ?
WHERE h.dead = 0
GROUP BY h.uid
HAVING COUNT(a.id) < ? AND COALESCE(MAX(a.created), 0) < ?
LIMIT ?"
);
$stmt->bind_param('iiii', $now, $max, $threshold, $limit);
$stmt->execute();
$res = $stmt->get_result();
$uids = array();
while ($row = $res->fetch_assoc()) {
$uids[] = (int) $row['uid'];
}
$stmt->close();
foreach ($uids as $uid) {
$this->generateOffers($uid);
}
return count($uids);
}
/* =========================================================================
* START
* ===================================================================== */
/**
* Send the hero on an available adventure offer.
* Returns one of the START_* class constants.
*/
public function startAdventure($uid, $adventureId)
{
$uid = (int) $uid; $adventureId = (int) $adventureId; $now = time();
$hero = $this->getLivingHero($uid);
if (!$hero) {
return self::START_NO_HERO;
}
// Only one adventure at a time.
if ($this->getRunning($uid)) {
return self::START_INVALID;
}
// Load the offer (must be this user's, available, not expired).
$stmt = $this->db->prepare(
"SELECT id, wref, difficulty, duration FROM " . TB_PREFIX . "hero_adventure
WHERE id = ? AND uid = ? AND status = 0 AND expire > ? LIMIT 1"
);
$stmt->bind_param('iii', $adventureId, $uid, $now);
$stmt->execute();
$res = $stmt->get_result();
$offer = $res->fetch_assoc();
$stmt->close();
if (!$offer) {
return self::START_INVALID;
}
// Hero must be AT HOME: race-safe conditional decrement of units.hero.
$homeWref = (int) $hero['wref'];
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "units SET hero = hero - 1 WHERE vref = ? AND hero >= 1 LIMIT 1"
);
$stmt->bind_param('i', $homeWref);
$stmt->execute();
$atHome = $stmt->affected_rows > 0;
$stmt->close();
if (!$atHome) {
return self::START_HERO_AWAY;
}
// Claim the offer atomically; if someone double-clicked and a parallel
// request claimed it first, put the hero back and bail out.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero_adventure SET status = 1 WHERE id = ? AND status = 0 LIMIT 1"
);
$stmt->bind_param('i', $adventureId);
$stmt->execute();
$claimed = $stmt->affected_rows > 0;
$stmt->close();
if (!$claimed) {
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "units SET hero = hero + 1 WHERE vref = ? LIMIT 1"
);
$stmt->bind_param('i', $homeWref);
$stmt->execute();
$stmt->close();
return self::START_INVALID;
}
// Movement out: from = home village, to = adventure tile, ref = adventure id.
// Recompute duration now (equipment may have changed since the offer was made).
global $database;
$coorHome = $database->getCoor($homeWref);
$coorSite = $database->getCoor((int) $offer['wref']);
$dist = $database->getDistance($coorHome['x'], $coorHome['y'], $coorSite['x'], $coorSite['y']);
$duration = $this->travelSeconds($uid, $dist);
$endtime = $now + $duration;
$stmt = $this->db->prepare(
"INSERT INTO " . TB_PREFIX . "movement
(sort_type, `from`, `to`, ref, ref2, starttime, endtime, proc, send, wood, clay, iron, crop, marker)
VALUES (?, ?, ?, ?, 0, ?, ?, 0, 1, 0, 0, 0, 0, 0)"
);
$sortType = MOVEMENT_ADVENTURE_OUT; $siteWref = (int) $offer['wref'];
$stmt->bind_param('iiiiii', $sortType, $homeWref, $siteWref, $adventureId, $now, $endtime);
$stmt->execute();
$moveid = (int) $stmt->insert_id;
$stmt->close();
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero_adventure SET moveid = ? WHERE id = ? LIMIT 1"
);
$stmt->bind_param('ii', $moveid, $adventureId);
$stmt->execute();
$stmt->close();
return self::START_OK;
}
/* =========================================================================
* AUTOMATION PROCESSORS
* ===================================================================== */
/**
* Process heroes ARRIVING at adventure sites (sort_type 20).
* Rolls rewards, applies HP loss/XP/silver/items, files the ntype 26
* report, and creates the return movement carrying the resource loot.
*/
public function processArrivals()
{
global $database, $heroAdventureConfig;
$now = time();
$q = "SELECT m.moveid, m.`from`, m.`to`, m.ref, m.endtime,
a.id AS advid, a.uid, a.difficulty
FROM " . TB_PREFIX . "movement m
JOIN " . TB_PREFIX . "hero_adventure a ON a.id = m.ref
WHERE m.sort_type = " . (int) MOVEMENT_ADVENTURE_OUT . "
AND m.proc = 0 AND m.endtime < $now";
$rows = $database->query_return($q);
if (!$rows || !count($rows)) {
return 0;
}
$processed = 0;
foreach ($rows as $data) {
if (!$this->claimMovement($data['moveid'])) {
continue;
}
$uid = (int) $data['uid'];
$hero = $this->getLivingHero($uid);
$cfg = $heroAdventureConfig[(int) $data['difficulty']];
// Hero vanished/died meanwhile (edge case): close the adventure quietly.
if (!$hero) {
$this->finishAdventure($data['advid']);
continue;
}
/* ---- Roll rewards ---- */
$exp = mt_rand($cfg['exp'][0], $cfg['exp'][1]);
$silver = mt_rand($cfg['silver'][0], $cfg['silver'][1]);
$hpLoss = mt_rand($cfg['hp_loss'][0], $cfg['hp_loss'][1]);
$wood = mt_rand($cfg['resources'][0], $cfg['resources'][1]);
$clay = mt_rand($cfg['resources'][0], $cfg['resources'][1]);
$iron = mt_rand($cfg['resources'][0], $cfg['resources'][1]);
$crop = mt_rand($cfg['resources'][0], $cfg['resources'][1]);
// Item drop: gear first (rarer, tier-weighted toward T1), and only
// if no gear dropped, roll the consumable pool. One item max.
$itemId = 0; $itemQty = 0;
if (mt_rand(1, 100) <= (int) ($cfg['equip_chance'] ?? 0)) {
$itemId = $this->rollEquipment();
$itemQty = ($itemId > 0) ? 1 : 0;
}
if ($itemId === 0 && mt_rand(1, 100) <= (int) $cfg['item_chance']) {
$pool = $cfg['consumable'];
$itemId = (int) $pool[array_rand($pool)];
$itemQty = 1;
}
$health = (float) $hero['health'];
$newHealth = $health - $hpLoss;
if ($newHealth <= 0) {
/* ---- Hero dies at the site: loot forfeited, no return trip. ---- */
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero SET dead = 1, health = 0, lastupdate = ? WHERE heroid = ? LIMIT 1"
);
$heroid = (int) $hero['heroid'];
$stmt->bind_param('ii', $now, $heroid);
$stmt->execute();
$stmt->close();
$payload = 'difficulty=' . (int) $data['difficulty'] . '&died=1&hp=' . $hpLoss;
$database->addNotice($uid, (int) $data['from'], 0, NTYPE_ADVENTURE_REPORT,
'Hero fell on an adventure', $payload, $now);
$this->finishAdventure($data['advid']);
$processed++;
continue;
}
/* ---- Hero survives: apply rewards ---- */
$heroItems = new HeroItems();
// XP with helmet bonus; level-up handled by Automation::updateHero().
$bonuses = $heroItems->getBonuses($uid);
$xpGain = (int) floor($exp * (100 + (int) $bonuses[HB_XP]) / 100);
$database->modifyHeroXp('experience', $xpGain, (int) $hero['heroid']);
// Health loss.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero SET health = ? WHERE heroid = ? LIMIT 1"
);
$heroid = (int) $hero['heroid'];
$stmt->bind_param('di', $newHealth, $heroid);
$stmt->execute();
$stmt->close();
// Silver + item drop.
if ($silver > 0) {
$heroItems->addSilver($uid, $silver);
}
if ($itemId > 0) {
$heroItems->addItem($uid, $itemId, $itemQty);
}
/* ---- Return movement carries the resource loot home ---- */
// Same travel time back as out: reuse the adventure's stored one-way duration.
$legDuration = $this->travelLegDuration($data);
$stmt = $this->db->prepare(
"INSERT INTO " . TB_PREFIX . "movement
(sort_type, `from`, `to`, ref, ref2, starttime, endtime, proc, send, wood, clay, iron, crop, marker)
VALUES (?, ?, ?, ?, 0, ?, ?, 0, 1, ?, ?, ?, ?, 0)"
);
$sortType = MOVEMENT_ADVENTURE_BACK;
$fromSite = (int) $data['to']; $toHome = (int) $data['from'];
$advid = (int) $data['advid'];
$end = $now + $legDuration;
$stmt->bind_param('iiiiiiiiii', $sortType, $fromSite, $toHome, $advid, $now, $end, $wood, $clay, $iron, $crop);
$stmt->execute();
$stmt->close();
/* ---- Report (ntype 26) ---- */
$payload = 'difficulty=' . (int) $data['difficulty']
. '&exp=' . $xpGain . '&silver=' . $silver . '&hp=' . $hpLoss
. '&wood=' . $wood . '&clay=' . $clay . '&iron=' . $iron . '&crop=' . $crop;
if ($itemId > 0) {
$payload .= '&itemid=' . $itemId . '&itemqty=' . $itemQty;
}
$database->addNotice($uid, $toHome, 0, NTYPE_ADVENTURE_REPORT,
'Hero returned from an adventure', $payload, $now);
$this->finishAdventure($data['advid']);
$processed++;
}
return $processed;
}
/**
* Process heroes RETURNING home (sort_type 21): hero re-enters units.hero,
* resource loot is credited to the village.
*/
public function processReturns()
{
global $database;
$now = time();
$q = "SELECT moveid, `from`, `to`, wood, clay, iron, crop
FROM " . TB_PREFIX . "movement
WHERE sort_type = " . (int) MOVEMENT_ADVENTURE_BACK . "
AND proc = 0 AND endtime < $now";
$rows = $database->query_return($q);
if (!$rows || !count($rows)) {
return 0;
}
$processed = 0;
foreach ($rows as $data) {
if (!$this->claimMovement($data['moveid'])) {
continue;
}
$home = (int) $data['to'];
// Hero is home again.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "units SET hero = hero + 1 WHERE vref = ? LIMIT 1"
);
$stmt->bind_param('i', $home);
$stmt->execute();
$stmt->close();
// Credit the loot (same pattern as returnunitsComplete).
if ($data['wood'] + $data['clay'] + $data['iron'] + $data['crop'] > 0) {
$database->modifyResource($home, $data['wood'], $data['clay'], $data['iron'], $data['crop'], 1);
$database->addStarvationData($home);
}
$processed++;
}
return $processed;
}
/**
* Roll one random piece of EQUIPMENT (any non-bag catalog item, including
* weapons for other tribes - unusable finds feed the auction house, same
* as T4). Tier picked by 'equip_tier_weights' (T1 common, T3 very rare),
* then a uniform pick inside that tier. Returns an itemid, or 0 if the
* rolled tier happens to be empty (defensive; never true with the
* shipped catalog).
*/
public function rollEquipment()
{
global $heroItemCatalog, $heroAdventureConfig;
$weights = $heroAdventureConfig['equip_tier_weights'];
$roll = mt_rand(1, 100); $acc = 0; $tier = 1;
foreach ($weights as $t => $w) {
$acc += (int) $w;
if ($roll <= $acc) { $tier = (int) $t; break; }
}
$pool = array();
foreach ($heroItemCatalog as $iid => $def) {
if ((int) $def['slot'] !== HSLOT_BAG && (int) $def['tier'] === $tier) {
$pool[] = $iid;
}
}
return count($pool) ? (int) $pool[array_rand($pool)] : 0;
}
/* =========================================================================
* INTERNALS
* ===================================================================== */
/**
* Travel time in seconds for a given distance, honoring equipment:
* horse replaces the on-foot base speed, spurs add on top (only with a
* horse - already enforced inside HeroItems::getBonuses()).
*/
public function travelSeconds($uid, $distance)
{
$heroItems = new HeroItems();
$bonuses = $heroItems->getBonuses((int) $uid);
$speed = ((int) $bonuses[HB_HORSE_SPEED] > 0)
? (int) $bonuses[HB_HORSE_SPEED] + (int) $bonuses[HB_MOUNT_SPEED]
: self::BASE_HERO_SPEED;
$increase = defined('INCREASE_SPEED') ? max(1, (int) INCREASE_SPEED) : 1;
return max(60, (int) round($distance / $speed * 3600 / $increase));
}
/** Duration of the just-finished leg (endtime - starttime is not selected; use stored offer duration). */
private function travelLegDuration($data)
{
// The return leg mirrors the outbound one. We recompute from the
// adventure's stored one-way duration to avoid drift when Automation
// runs late (endtime long in the past).
$stmt = $this->db->prepare(
"SELECT duration FROM " . TB_PREFIX . "hero_adventure WHERE id = ? LIMIT 1"
);
$advid = (int) $data['advid'];
$stmt->bind_param('i', $advid);
$stmt->execute();
$stmt->bind_result($duration);
$found = $stmt->fetch();
$stmt->close();
return $found ? max(60, (int) $duration) : 3600;
}
/** Same claim pattern as Automation::claimMovementRecord (race-safe). */
private function claimMovement($moveid)
{
$moveid = (int) $moveid;
if ($moveid <= 0) {
return false;
}
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "movement SET proc = 1 WHERE moveid = ? AND proc = 0 LIMIT 1"
);
$stmt->bind_param('i', $moveid);
$stmt->execute();
$ok = $stmt->affected_rows === 1;
$stmt->close();
return $ok;
}
private function finishAdventure($advid)
{
$advid = (int) $advid;
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero_adventure SET status = 2 WHERE id = ? LIMIT 1"
);
$stmt->bind_param('i', $advid);
$stmt->execute();
$stmt->close();
}
private function getLivingHero($uid)
{
$uid = (int) $uid;
$stmt = $this->db->prepare(
"SELECT heroid, uid, unit, wref, level, experience, health, dead
FROM " . TB_PREFIX . "hero WHERE uid = ? AND dead = 0 LIMIT 1"
);
$stmt->bind_param('i', $uid);
$stmt->execute();
$res = $stmt->get_result();
$row = $res->fetch_assoc();
$stmt->close();
return $row ?: null;
}
}
+533
View File
@@ -0,0 +1,533 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : HeroAuction.php ##
## Type : T4 Hero port - auction house backend (Phase 4) ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
## ##
## T4-style auction house with PROXY BIDDING (eBay model): ##
## - a bid is a secret MAXIMUM (bid_max); the visible price ##
## (silver_current) only rises to (loser's max + 1), capped at the ##
## winner's max. ##
## ##
## SILVER ESCROW MODEL (documented invariant): ##
## - when a bidder becomes top bidder, their FULL bid_max is deducted; ##
## - when outbid, their full bid_max is refunded immediately; ##
## - at finalization the winner is refunded (bid_max - silver_current), ##
## so they only ever pay the visible price. ##
## => at any moment: sum of all player silver + escrowed bid_max of top ##
## bidders is constant (minus the seller fee at finalization). ##
## ##
## SELLER FEE: player sellers receive silver_current minus AUCTION_FEE_PCT ##
## (10%, T4 convention). NPC-seller (seller = 0) proceeds vanish (silver ##
## sink). Unsold player items are returned to the seller's inventory. ##
## ##
## No cancellation once listed (T4 convention) - flagged, not implemented. ##
## ##
#################################################################################
class HeroAuction
{
/** Seller fee percent taken from the hammer price (player sellers only). */
const AUCTION_FEE_PCT = 10;
/** Minimum bid increment over the visible price. */
const MIN_INCREMENT = 1;
/** Allowed listing durations, seconds (4h / 8h / 24h like T4). */
public static $allowedDurations = array(14400, 28800, 86400);
/** How many NPC auctions to keep open at once. */
const NPC_OPEN_TARGET = 10;
/** NPC listing duration: 8 hours. */
const NPC_DURATION = 28800;
/** status values (mirror the auction table docs from Phase 1) */
const ST_OPEN = 0;
const ST_SOLD = 1; // reserved intermediate state; finalization goes straight to 2
const ST_PROCESSED = 2;
const ST_EXPIRED = 3;
/** result codes for placeBid() / createAuction() */
const BID_OK = 1;
const BID_OUTBID = 2; // accepted, but instantly below the holder's secret max
const BID_INVALID = 0; // bad auction/own auction/too low
const BID_NO_SILVER = -1;
/** @var mysqli */
private $db;
public function __construct()
{
global $database;
$this->db = $database->dblink;
global $heroItemCatalog;
if (!isset($heroItemCatalog)) {
include_once __DIR__ . '/Data/hero_items.php';
}
}
/* =========================================================================
* READS
* ===================================================================== */
/** All open, unexpired auctions (newest ending first optional; default: ending soonest). */
public function getOpenAuctions($limit = 50)
{
$now = time(); $limit = max(1, (int) $limit);
$stmt = $this->db->prepare(
"SELECT id, seller, itemid, slot, stat_value, quantity,
silver_start, silver_current, bidder, created, time_end, status
FROM " . TB_PREFIX . "auction
WHERE status = " . self::ST_OPEN . " AND time_end > ?
ORDER BY time_end ASC LIMIT ?"
);
$stmt->bind_param('ii', $now, $limit);
$stmt->execute();
$res = $stmt->get_result();
$out = array();
while ($row = $res->fetch_assoc()) {
$row['name'] = heroItemName($row['itemid']);
// bid_max intentionally NOT selected - it's the bidder's secret.
$out[] = $row;
}
$stmt->close();
return $out;
}
/** Auctions where $uid is the current top bidder. */
public function getMyBids($uid)
{
$uid = (int) $uid; $now = time();
$stmt = $this->db->prepare(
"SELECT id, seller, itemid, quantity, silver_current, bid_max, time_end
FROM " . TB_PREFIX . "auction
WHERE bidder = ? AND status = " . self::ST_OPEN . " AND time_end > ?
ORDER BY time_end ASC"
);
$stmt->bind_param('ii', $uid, $now);
$stmt->execute();
$res = $stmt->get_result();
$out = array();
while ($row = $res->fetch_assoc()) {
$row['name'] = heroItemName($row['itemid']);
$out[] = $row;
}
$stmt->close();
return $out;
}
/** Open auctions listed by $uid. */
public function getMySales($uid)
{
$uid = (int) $uid;
$stmt = $this->db->prepare(
"SELECT id, itemid, quantity, silver_start, silver_current, bidder, time_end, status
FROM " . TB_PREFIX . "auction
WHERE seller = ? AND status = " . self::ST_OPEN . "
ORDER BY time_end ASC"
);
$stmt->bind_param('i', $uid);
$stmt->execute();
$res = $stmt->get_result();
$out = array();
while ($row = $res->fetch_assoc()) {
$row['name'] = heroItemName($row['itemid']);
$out[] = $row;
}
$stmt->close();
return $out;
}
/* =========================================================================
* LISTING
* ===================================================================== */
/**
* List an owned, UNEQUIPPED item (or part of a consumable stack).
* The item leaves the inventory immediately (escrowed in the auction row).
* Returns the new auction id, or 0 on failure.
*/
public function createAuction($uid, $inventoryRowId, $quantity, $startPrice, $duration)
{
$uid = (int) $uid; $inventoryRowId = (int) $inventoryRowId;
$quantity = max(1, (int) $quantity);
$startPrice = max(1, (int) $startPrice);
$duration = (int) $duration;
if (!in_array($duration, self::$allowedDurations, true)) {
return 0;
}
$heroItems = new HeroItems();
$row = null;
foreach ($heroItems->getInventory($uid) as $r) {
if ((int) $r['id'] === $inventoryRowId) { $row = $r; break; }
}
// Must exist, be catalog-known, unequipped, and cover the quantity.
if (!$row || $row['orphan'] || (int) $row['equipped'] === 1 || (int) $row['quantity'] < $quantity) {
return 0;
}
// Gear always lists as a single piece.
if ((int) $row['def']['slot'] !== HSLOT_BAG) {
$quantity = 1;
}
// Take the item out of the inventory first (race-safe decrement inside).
if (!$heroItems->removeItem($uid, $inventoryRowId, $quantity)) {
return 0;
}
$now = time(); $end = $now + $duration;
$itemid = (int) $row['itemid']; $slot = (int) $row['def']['slot'];
$statValue = (int) $row['stat_value'];
$stmt = $this->db->prepare(
"INSERT INTO " . TB_PREFIX . "auction
(seller, itemid, slot, stat_value, quantity,
silver_start, silver_current, bidder, bid_max, created, time_end, status)
VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, " . self::ST_OPEN . ")"
);
$stmt->bind_param('iiiiiiiii', $uid, $itemid, $slot, $statValue, $quantity,
$startPrice, $startPrice, $now, $end);
$stmt->execute();
$auctionId = (int) $stmt->insert_id;
$stmt->close();
return $auctionId;
}
/* =========================================================================
* BIDDING (proxy model)
* ===================================================================== */
/**
* Place a maximum bid. Returns a BID_* code.
*
* Escrow choreography (all conditional statements, race-safe):
* 1. take $maxBid silver from the bidder (refused if balance is short);
* 2. try to become top bidder atomically
* (WHERE bid_max < $maxBid guards the proxy comparison);
* 3a. success -> refund the PREVIOUS top bidder's full escrow;
* 3b. failure -> we lost against the holder's secret max: bump the
* visible price to min(holderMax, ourMax + 1) and refund ourselves.
*/
public function placeBid($uid, $auctionId, $maxBid)
{
$uid = (int) $uid; $auctionId = (int) $auctionId; $maxBid = (int) $maxBid;
$now = time();
// Snapshot for validation (authoritative checks are in the UPDATEs).
// bid_max is only used for the self-raise delta below - it is the
// caller's OWN secret in that case, never exposed to rivals.
$stmt = $this->db->prepare(
"SELECT seller, silver_current, bidder, bid_max FROM " . TB_PREFIX . "auction
WHERE id = ? AND status = " . self::ST_OPEN . " AND time_end > ? LIMIT 1"
);
$stmt->bind_param('ii', $auctionId, $now);
$stmt->execute();
$res = $stmt->get_result();
$auction = $res->fetch_assoc();
$stmt->close();
if (!$auction || (int) $auction['seller'] === $uid) {
return self::BID_INVALID;
}
// A fresh auction (no bidder) can be won AT the start price;
// an active one needs current + increment.
$minBid = (int) $auction['silver_current']
+ ((int) $auction['bidder'] > 0 ? self::MIN_INCREMENT : 0);
if ($maxBid < $minBid) {
return self::BID_INVALID;
}
// Self-raise: the old escrow (old bid_max) is already held, so only
// the difference is taken; a raise below the current max is pointless.
$selfRaise = ((int) $auction['bidder'] === $uid);
if ($selfRaise && $maxBid <= (int) $auction['bid_max']) {
return self::BID_INVALID;
}
$escrow = $selfRaise ? $maxBid - (int) $auction['bid_max'] : $maxBid;
$heroItems = new HeroItems();
// 1. escrow (conditional - fails on insufficient silver).
if (!$heroItems->spendSilver($uid, $escrow)) {
return self::BID_NO_SILVER;
}
// 2. capture the current holder as the refund target, then claim.
// Correctness anchor is the conditional UPDATE below - this read
// only records who to refund if we displace them.
$stmt = $this->db->prepare(
"SELECT bidder, bid_max FROM " . TB_PREFIX . "auction WHERE id = ? LIMIT 1"
);
$stmt->bind_param('i', $auctionId);
$stmt->execute();
$res = $stmt->get_result();
$prev = $res->fetch_assoc();
$stmt->close();
if ($selfRaise) {
// Deepening our own secret max never moves the visible price and
// never refunds anyone - the old escrow stays held, only the
// delta was added above.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "auction
SET bid_max = ?
WHERE id = ? AND status = " . self::ST_OPEN . " AND time_end > ?
AND bidder = ? AND bid_max < ?"
);
$stmt->bind_param('iiiii', $maxBid, $auctionId, $now, $uid, $maxBid);
$stmt->execute();
$won = $stmt->affected_rows > 0;
$stmt->close();
if ($won) {
return self::BID_OK;
}
// Race lost: a rival displaced us between snapshot and update, so
// their winning path already refunded our OLD escrow - the delta
// is all we still hold. Return it.
$heroItems->addSilver($uid, $escrow);
return self::BID_OUTBID;
}
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "auction
SET silver_current = LEAST(?, GREATEST(silver_current, bid_max + " . self::MIN_INCREMENT . ")),
bidder = ?, bid_max = ?
WHERE id = ? AND status = " . self::ST_OPEN . " AND time_end > ?
AND bid_max < ? AND seller <> ? AND bidder <> ?"
);
$stmt->bind_param('iiiiiiii', $maxBid, $uid, $maxBid, $auctionId, $now, $maxBid, $uid, $uid);
$stmt->execute();
$won = $stmt->affected_rows > 0;
$stmt->close();
if ($won) {
// 3a. refund the displaced bidder's full escrow.
if ($prev && (int) $prev['bidder'] > 0 && (int) $prev['bidder'] !== $uid && (int) $prev['bid_max'] > 0) {
$heroItems->addSilver((int) $prev['bidder'], (int) $prev['bid_max']);
}
return self::BID_OK;
}
// 3b. lost against the holder's secret max (or the auction closed in
// the meantime): bump the visible price if still open, refund us.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "auction
SET silver_current = LEAST(bid_max, ? + " . self::MIN_INCREMENT . ")
WHERE id = ? AND status = " . self::ST_OPEN . " AND time_end > ? AND bid_max >= ?"
);
$stmt->bind_param('iiii', $maxBid, $auctionId, $now, $maxBid);
$stmt->execute();
$stmt->close();
$heroItems->addSilver($uid, $escrow);
return self::BID_OUTBID;
}
/* =========================================================================
* AUTOMATION PROCESSORS
* ===================================================================== */
/**
* Finalize ended auctions:
* - with a bidder: award the item, refund (bid_max - silver_current) to
* the winner, pay the player seller (minus fee), file reports;
* - without: return the item to a player seller, mark expired.
* NPC (seller 0) items simply vanish when unsold; NPC proceeds vanish too.
*/
public function processFinished()
{
global $database;
$now = time();
$q = "SELECT id, seller, itemid, stat_value, quantity,
silver_current, bidder, bid_max
FROM " . TB_PREFIX . "auction
WHERE status = " . self::ST_OPEN . " AND time_end <= $now";
$rows = $database->query_return($q);
if (!$rows || !count($rows)) {
return 0;
}
$heroItems = new HeroItems();
$processed = 0;
foreach ($rows as $a) {
// Claim the row (same race-safe pattern as movement claiming).
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "auction SET status = " . self::ST_PROCESSED . "
WHERE id = ? AND status = " . self::ST_OPEN . " LIMIT 1"
);
$aid = (int) $a['id'];
$stmt->bind_param('i', $aid);
$stmt->execute();
$claimed = $stmt->affected_rows > 0;
$stmt->close();
if (!$claimed) {
continue;
}
$bidder = (int) $a['bidder'];
$seller = (int) $a['seller'];
$price = (int) $a['silver_current'];
if ($bidder > 0) {
/* ---- SOLD ---- */
$heroItems->addItem($bidder, (int) $a['itemid'], (int) $a['quantity'], (int) $a['stat_value']);
// Winner escrowed bid_max; they only pay the hammer price.
$refund = (int) $a['bid_max'] - $price;
if ($refund > 0) {
$heroItems->addSilver($bidder, $refund);
}
// Player seller gets the price minus the fee; NPC proceeds sink.
if ($seller > 0) {
$fee = (int) floor($price * self::AUCTION_FEE_PCT / 100);
$heroItems->addSilver($seller, $price - $fee);
$database->addNotice($seller, 0, 0, NTYPE_AUCTION_REPORT,
'Auction sold',
'role=seller&itemid=' . (int) $a['itemid'] . '&qty=' . (int) $a['quantity']
. '&price=' . $price . '&fee=' . $fee, $now);
}
$database->addNotice($bidder, 0, 0, NTYPE_AUCTION_REPORT,
'Auction won',
'role=winner&itemid=' . (int) $a['itemid'] . '&qty=' . (int) $a['quantity']
. '&price=' . $price . '&refund=' . max(0, $refund), $now);
} else {
/* ---- UNSOLD ---- */
if ($seller > 0) {
$heroItems->addItem($seller, (int) $a['itemid'], (int) $a['quantity'], (int) $a['stat_value']);
$database->addNotice($seller, 0, 0, NTYPE_AUCTION_REPORT,
'Auction expired',
'role=seller&expired=1&itemid=' . (int) $a['itemid'] . '&qty=' . (int) $a['quantity'], $now);
}
// Correct the claimed status for the unsold case.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "auction SET status = " . self::ST_EXPIRED . " WHERE id = ? LIMIT 1"
);
$stmt->bind_param('i', $aid);
$stmt->execute();
$stmt->close();
}
$processed++;
}
return $processed;
}
/**
* ADMIN-ONLY: cancel an open auction outright (Phase 7 admin panel).
* Player-facing cancellation intentionally does not exist (T4 convention);
* this is the moderation escape hatch. Refunds the top bidder's full
* escrow and returns the item to a player seller (NPC items vanish).
* Returns true when an open auction was cancelled.
*/
public function adminCancel($auctionId)
{
$auctionId = (int) $auctionId;
// Snapshot before claiming so we know who to refund.
$stmt = $this->db->prepare(
"SELECT seller, itemid, stat_value, quantity, bidder, bid_max
FROM " . TB_PREFIX . "auction
WHERE id = ? AND status = " . self::ST_OPEN . " LIMIT 1"
);
$stmt->bind_param('i', $auctionId);
$stmt->execute();
$res = $stmt->get_result();
$a = $res->fetch_assoc();
$stmt->close();
if (!$a) {
return false;
}
// Race-safe claim straight to EXPIRED so processFinished can't touch it.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "auction SET status = " . self::ST_EXPIRED . "
WHERE id = ? AND status = " . self::ST_OPEN . " LIMIT 1"
);
$stmt->bind_param('i', $auctionId);
$stmt->execute();
$claimed = $stmt->affected_rows > 0;
$stmt->close();
if (!$claimed) {
return false;
}
$heroItems = new HeroItems();
if ((int) $a['bidder'] > 0 && (int) $a['bid_max'] > 0) {
$heroItems->addSilver((int) $a['bidder'], (int) $a['bid_max']);
}
if ((int) $a['seller'] > 0) {
$heroItems->addItem((int) $a['seller'], (int) $a['itemid'], (int) $a['quantity'], (int) $a['stat_value']);
}
return true;
}
/**
* Keep ~NPC_OPEN_TARGET NPC-seller auctions open so the market always has
* stock. Random catalog items; consumables list in small stacks.
* Start price scales with tier so tier-3 gear stays expensive.
*/
public function seedNpcAuctions()
{
global $heroItemCatalog;
$now = time();
$stmt = $this->db->prepare(
"SELECT COUNT(*) FROM " . TB_PREFIX . "auction
WHERE seller = 0 AND status = " . self::ST_OPEN . " AND time_end > ?"
);
$stmt->bind_param('i', $now);
$stmt->execute();
$stmt->bind_result($open);
$stmt->fetch();
$stmt->close();
$need = self::NPC_OPEN_TARGET - (int) $open;
if ($need <= 0) {
return 0;
}
$ids = array_keys($heroItemCatalog);
$created = 0;
for ($i = 0; $i < $need; $i++) {
$itemid = (int) $ids[array_rand($ids)];
$def = $heroItemCatalog[$itemid];
$isBag = ((int) $def['slot'] === HSLOT_BAG);
$qty = $isBag ? mt_rand(3, 15) : 1;
// Start price: tier-scaled for gear, per-piece for consumables.
$start = $isBag ? $qty * mt_rand(2, 5) : (int) $def['tier'] * mt_rand(40, 80);
$end = $now + self::NPC_DURATION;
$slot = (int) $def['slot'];
$stmt = $this->db->prepare(
"INSERT INTO " . TB_PREFIX . "auction
(seller, itemid, slot, stat_value, quantity,
silver_start, silver_current, bidder, bid_max, created, time_end, status)
VALUES (0, ?, ?, 0, ?, ?, ?, 0, 0, ?, ?, " . self::ST_OPEN . ")"
);
$stmt->bind_param('iiiiiii', $itemid, $slot, $qty, $start, $start, $now, $end);
$stmt->execute();
$stmt->close();
$created++;
}
return $created;
}
}
+529
View File
@@ -0,0 +1,529 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : HeroBattleBonus.php ##
## Type : T4 Hero port - battle & speed integration (Phase 5) ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
## ##
## Thin, static bridge between the hero item system and the combat/speed ##
## engine. Every entry point: ##
## - is a NO-OP returning neutral values when NEW_FUNCTIONS_HERO_T4 is ##
## off, so vanilla battle math is bit-for-bit unchanged; ##
## - memoizes per uid for the duration of the request (Automation ticks ##
## process many battles; bonuses must not re-query per battle). ##
## ##
## Consumers (surgical hooks): ##
## Battle::getBattleHero() -> statBonus(), attaches uid/unit_bonus/ ##
## vs_natars/dmg_reduce to the hero array ##
## Battle HERO OFFENSE block -> unitOffense(), natarMultiplier() ##
## Battle defender hero block -> unitDefense() ##
## Battle HERO DAMAGE block -> reduceDamage() ##
## Automation bounty callsite -> raidMultiplier() ##
## Automation return callsites -> adjustReturnEndtime() ##
## Units::getWalkingTroopsTime -> adjustTravelTime() ##
## ##
#################################################################################
class HeroBattleBonus
{
/** @var array per-request bonus cache: uid => bonuses array */
private static $cache = array();
/** @var array per-request "does uid have a living hero" cache */
private static $livingCache = array();
/** Feature flag gate used by every public entry point. */
public static function enabled()
{
return defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4;
}
/** Cached HeroItems::getBonuses(). Returns null when the feature is off. */
public static function bonuses($uid)
{
if (!self::enabled()) {
return null;
}
$uid = (int) $uid;
if (!array_key_exists($uid, self::$cache)) {
$heroItems = new HeroItems();
self::$cache[$uid] = $heroItems->getBonuses($uid);
}
return self::$cache[$uid];
}
/* =========================================================================
* BATTLE MATH
* ===================================================================== */
/**
* Flat fighting strength from items (weapons, cuirasses, shields).
* Added to the hero's atk AND di/dc - T4 fight strength is a single stat
* that counts on whichever side the hero fights.
*/
public static function statBonus($uid)
{
$b = self::bonuses($uid);
return $b ? (int) $b[HB_FIGHT] : 0;
}
/** [unitId => perUnit] map from the equipped weapon (empty when none). */
public static function unitBonusMap($uid)
{
$b = self::bonuses($uid);
return ($b && !empty($b[HB_UNIT_BONUS])) ? $b[HB_UNIT_BONUS] : array();
}
/**
* Extra offense from the weapon's per-unit bonus:
* +N attack per accompanying unit of the weapon's type.
* $isCavalry tells the caller whether to add it to ap or cap.
* Returns [addedInfantryAp, addedCavalryAp].
*/
public static function unitOffense($uid, array $attackerUnits, array $cavalryLookup)
{
$map = self::unitBonusMap($uid);
if (empty($map)) {
return array(0, 0);
}
$ap = 0; $cap = 0;
foreach ($map as $unitId => $perUnit) {
$count = isset($attackerUnits['u' . $unitId]) ? (int) $attackerUnits['u' . $unitId] : 0;
if ($count <= 0) {
continue;
}
if (isset($cavalryLookup[$unitId])) {
$cap += $perUnit * $count;
} else {
$ap += $perUnit * $count;
}
}
return array($ap, $cap);
}
/**
* Extra defense from the weapon's per-unit bonus:
* +N defense per accompanying unit of the weapon's type. The design sheet
* grants a single "+N def" figure, so it is added to BOTH the infantry
* (dp) and cavalry (cdp) defense pools.
* Returns the flat amount to add to each pool.
*/
public static function unitDefense($uid, array $defenderUnits)
{
$map = self::unitBonusMap($uid);
if (empty($map)) {
return 0;
}
$add = 0;
foreach ($map as $unitId => $perUnit) {
$count = isset($defenderUnits['u' . $unitId]) ? (int) $defenderUnits['u' . $unitId] : 0;
if ($count > 0) {
$add += $perUnit * $count;
}
}
return $add;
}
/**
* Hunting horn: multiplier applied to the HERO's own attack contribution
* when the defender is the Natars (uid 3, see Artifacts::NATARS_UID).
*/
public static function natarMultiplier($uid, $defenderUid)
{
if ((int) $defenderUid !== 3) {
return 1.0;
}
$b = self::bonuses($uid);
if (!$b || (int) $b[HB_VS_NATARS] <= 0) {
return 1.0;
}
return 1 + ((int) $b[HB_VS_NATARS] / 100);
}
/**
* Armor damage reduction: flat points subtracted from the health damage
* the hero takes in one battle (never below 0).
*/
public static function reduceDamage($uid, $damage)
{
$b = self::bonuses($uid);
if (!$b || (int) $b[HB_DMG_REDUCE] <= 0) {
return (int) $damage;
}
return max(0, (int) $damage - (int) $b[HB_DMG_REDUCE]);
}
/* =========================================================================
* RAID BOUNTY (thief sacks)
* ===================================================================== */
/**
* Sacks increase the resources stolen. Applies only when the attacker's
* living hero actually took part in the strike ($heroSent > 0). The hero
* that died in this very battle was already flagged dead by Battle.php
* before the bounty step runs, so livingHero() naturally excludes them.
*/
public static function raidMultiplier($uid, $heroSent)
{
if ((int) $heroSent <= 0 || !self::livingHero($uid)) {
return 1.0;
}
$b = self::bonuses($uid);
if (!$b || (int) $b[HB_RAID] <= 0) {
return 1.0;
}
return 1 + ((int) $b[HB_RAID] / 100);
}
/* =========================================================================
* TRAVEL TIME (boots / pennant / standard / map)
* ===================================================================== */
/**
* Adjust an OUTBOUND troop-travel time (seconds) computed by
* Generator::procDistanceTime(). Only applies when the hero rides along.
*
* - Pennant (HB_SPEED_OWN): +% speed when the target village belongs
* to the same player;
* - Standard (HB_SPEED_ALLY): +% speed when the target belongs to an
* alliance mate (pennant wins if both match);
* - Boots (HB_TROOP_SPEED_20): +% speed for the part of the journey
* beyond 20 tiles, same piecewise idea as the Tournament Square
* (issue #304). NOTE: applied proportionally on the final time, which
* is exact on its own; when a Tournament Square is also active the two
* boosts compose multiplicatively on the >20 segment - a documented,
* deliberate approximation.
*/
public static function adjustTravelTime($ownerUid, $heroInArmy, $fromWref, $toWref, $seconds)
{
global $database;
if (!self::enabled() || (int) $heroInArmy <= 0 || $seconds <= 0) {
return (int) $seconds;
}
$b = self::bonuses($ownerUid);
if (!$b) {
return (int) $seconds;
}
$seconds = (float) $seconds;
// --- pennant / standard ---
// Pennant: BOTH endpoints must belong to the player (journeys between
// own villages). Standard: both endpoints inside the same alliance.
// Destination-only checks would wrongly buff every return leg home.
$pct = 0;
$fromOwner = (int) $database->getVillageField((int) $fromWref, 'owner');
$targetOwner = (int) $database->getVillageField((int) $toWref, 'owner');
if ($fromOwner > 0 && $targetOwner > 0) {
if ($fromOwner === (int) $ownerUid && $targetOwner === (int) $ownerUid) {
$pct = (int) $b[HB_SPEED_OWN];
} elseif ((int) $b[HB_SPEED_ALLY] > 0) {
$ownAlly = (int) $database->getUserField($fromOwner, 'alliance', 0);
$targetAlly = (int) $database->getUserField($targetOwner, 'alliance', 0);
if ($ownAlly > 0 && $ownAlly === $targetAlly) {
$pct = (int) $b[HB_SPEED_ALLY];
}
}
}
if ($pct > 0) {
$seconds = $seconds / (1 + $pct / 100);
}
// --- boots: only the beyond-20-tiles share of the journey ---
$bootsPct = (int) $b[HB_TROOP_SPEED_20];
if ($bootsPct > 0) {
$fromCoor = $database->getCoor((int) $fromWref);
$toCoor = $database->getCoor((int) $toWref);
if ($fromCoor && $toCoor) {
$distance = (float) $database->getDistance($fromCoor['x'], $fromCoor['y'], $toCoor['x'], $toCoor['y']);
if ($distance > 20) {
$shareBeyond = ($distance - 20) / $distance; // time share past 20 tiles
$timeBeyond = $seconds * $shareBeyond;
$seconds = ($seconds - $timeBeyond) + $timeBeyond / (1 + $bootsPct / 100);
}
}
}
return max(1, (int) round($seconds));
}
/**
* Map: shorten the RETURN leg after a battle. Receives the arrival time
* and the planned return endtime; compresses the leg duration by the
* map's percentage when the (still living) hero is coming home.
*/
public static function adjustReturnEndtime($ownerUid, $heroSent, $arrivalTime, $endtime)
{
if (!self::enabled() || (int) $heroSent <= 0 || !self::livingHero($ownerUid)) {
return $endtime;
}
$b = self::bonuses($ownerUid);
if (!$b || (int) $b[HB_RETURN_SPEED] <= 0) {
return $endtime;
}
$leg = (float) $endtime - (float) $arrivalTime;
if ($leg <= 0) {
return $endtime;
}
return $arrivalTime + $leg / (1 + (int) $b[HB_RETURN_SPEED] / 100);
}
/* =========================================================================
* BANDAGES (post-battle troop healing)
* ===================================================================== */
/**
* Heal part of the attacker's fallen troops after a battle (Phase 6):
* up to pct% of each unit type's losses return home, consuming ONE
* bandage per healed unit. Better bandages (33%, itemid 207) are used
* before small ones (25%, itemid 206). Applies only when the hero came
* home alive ($returning['t11'] > 0). The hero itself is never healable.
*
* NOTE: hero XP was already granted on the full casualty count before
* this runs - healed troops still award XP (deliberate, documented).
*
* @param int $uid Attacking player.
* @param array $returning ['t1'..'t11' => surviving counts] (modified copy returned).
* @param array $dead [1..11 => losses per unit index].
* @param int $attackRef attacks.id of this strike. returnunitsComplete
* reads the homecoming troop counts from the
* attacks row (movement.ref join), NOT from the
* in-memory array - healed units MUST be written
* back there or they would vanish on arrival.
* @return array The adjusted returning-troops array.
*/
public static function applyBandages($uid, array $returning, array $dead, $attackRef = 0)
{
global $database;
if (!self::enabled() || (int) ($returning['t11'] ?? 0) <= 0) {
return $returning;
}
$heroItems = new HeroItems();
// Bandage stacks, strongest first: [rowId, remaining, pct]
$stacks = array();
foreach ($heroItems->getInventory((int) $uid) as $row) {
if ($row['orphan'] || $row['equipped'] == 1) {
continue;
}
if ((int) $row['itemid'] === 207) {
$stacks[0] = array('id' => (int) $row['id'], 'qty' => (int) $row['quantity'], 'pct' => 33);
} elseif ((int) $row['itemid'] === 206) {
$stacks[1] = array('id' => (int) $row['id'], 'qty' => (int) $row['quantity'], 'pct' => 25);
}
}
if (empty($stacks)) {
return $returning;
}
ksort($stacks);
$healedPerUnit = array();
foreach ($stacks as $stack) {
if ($stack['qty'] <= 0) {
continue;
}
$budget = $stack['qty'];
$used = 0;
for ($i = 1; $i <= 10 && $budget > 0; $i++) {
$lost = isset($dead[$i]) ? (int) $dead[$i] : 0;
if ($lost <= 0) {
continue;
}
$healable = (int) floor($lost * $stack['pct'] / 100);
$healed = min($healable, $budget);
if ($healed <= 0) {
continue;
}
$returning['t' . $i] = (int) ($returning['t' . $i] ?? 0) + $healed;
$dead[$i] -= $healed; // a unit is only healed once across stacks
$budget -= $healed;
$used += $healed;
$healedPerUnit[$i] = ($healedPerUnit[$i] ?? 0) + $healed;
}
if ($used > 0) {
$heroItems->removeItem((int) $uid, $stack['id'], $used);
}
}
// Persist the revived units into the attacks row: it is the source of
// truth returnunitsComplete hands back to the village on arrival.
if (!empty($healedPerUnit) && (int) $attackRef > 0) {
$sets = array();
foreach ($healedPerUnit as $i => $healed) {
$sets[] = 't' . (int) $i . ' = t' . (int) $i . ' + ' . (int) $healed;
}
mysqli_query(
$database->dblink,
"UPDATE " . TB_PREFIX . "attacks SET " . implode(', ', $sets)
. " WHERE id = " . (int) $attackRef . " LIMIT 1"
);
}
return $returning;
}
/* =========================================================================
* CAGES (oasis animal capture)
* ===================================================================== */
/**
* Cages (itemid 208): when the hero attacks an UNOCCUPIED oasis, up to
* one animal per cage is captured BEFORE the fight - captured animals do
* not defend. They are stationed as defensive reinforcements in the
* attack's home village (enforcement row vref=home, from=home; nature
* units u31-u40 count in defense through the normal enforcement path).
*
* Deliberate, documented simplification vs T4: the animals appear at
* home the moment the battle resolves instead of travelling back with
* the hero (the attacks row can only carry the attacker's own tribe
* units, and a schema change for this isn't worth it).
*
* Removal from the oasis is a CONDITIONAL decrement per unit type
* (u31 >= N guards), so a parallel process can never drive counts
* negative; on a lost race the capture simply shrinks.
*
* @param int $uid Attacking player.
* @param int $homeWref Village the attack was sent from.
* @param int $oasisWref The unoccupied oasis under attack.
* @param array $Defender Defender unit array (modified copy returned).
* @return array The Defender array minus whatever was captured.
*/
public static function applyCages($uid, $homeWref, $oasisWref, array $Defender)
{
global $database;
if (!self::enabled()) {
return $Defender;
}
// Animals present?
$animals = 0;
for ($i = 31; $i <= 40; $i++) {
$animals += max(0, (int) ($Defender['u' . $i] ?? 0));
}
if ($animals <= 0) {
return $Defender;
}
// Cage stack (unequipped, itemid 208).
$heroItems = new HeroItems();
$cageRow = null;
foreach ($heroItems->getInventory((int) $uid) as $row) {
if (!$row['orphan'] && (int) $row['itemid'] === 208 && $row['equipped'] == 0) {
$cageRow = $row;
break;
}
}
if (!$cageRow || (int) $cageRow['quantity'] <= 0) {
return $Defender;
}
$budget = (int) $cageRow['quantity'];
$captured = array();
for ($i = 31; $i <= 40 && $budget > 0; $i++) {
$have = max(0, (int) ($Defender['u' . $i] ?? 0));
if ($have <= 0) {
continue;
}
$take = min($have, $budget);
// Conditional removal from the oasis - authoritative count.
$stmt = $database->dblink->prepare(
"UPDATE " . TB_PREFIX . "units
SET u{$i} = u{$i} - ?
WHERE vref = ? AND u{$i} >= ? LIMIT 1"
);
$stmt->bind_param('iii', $take, $oasisWref, $take);
$stmt->execute();
$removed = $stmt->affected_rows > 0;
$stmt->close();
if (!$removed) {
continue; // lost a race; leave these animals to fight
}
$Defender['u' . $i] = $have - $take;
$captured[$i] = $take;
$budget -= $take;
}
if (empty($captured)) {
return $Defender;
}
$used = array_sum($captured);
$heroItems->removeItem((int) $uid, (int) $cageRow['id'], $used);
// Station the animals at home: reuse the self-enforcement row if one
// exists (vref = from = home), otherwise create it.
$homeWref = (int) $homeWref;
$res = mysqli_query(
$database->dblink,
"SELECT id FROM " . TB_PREFIX . "enforcement
WHERE vref = $homeWref AND `from` = $homeWref LIMIT 1"
);
$rowE = $res ? mysqli_fetch_assoc($res) : null;
if (!$rowE) {
mysqli_query(
$database->dblink,
"INSERT INTO " . TB_PREFIX . "enforcement (vref, `from`) VALUES ($homeWref, $homeWref)"
);
$enfId = (int) mysqli_insert_id($database->dblink);
} else {
$enfId = (int) $rowE['id'];
}
$sets = array();
foreach ($captured as $i => $n) {
$sets[] = 'u' . (int) $i . ' = u' . (int) $i . ' + ' . (int) $n;
}
mysqli_query(
$database->dblink,
"UPDATE " . TB_PREFIX . "enforcement SET " . implode(', ', $sets)
. " WHERE id = $enfId LIMIT 1"
);
return $Defender;
}
/* =========================================================================
* INTERNALS
* ===================================================================== */
/** Cached "uid has a living hero" check. */
private static function livingHero($uid)
{
global $database;
$uid = (int) $uid;
if (!array_key_exists($uid, self::$livingCache)) {
$res = mysqli_query(
$database->dblink,
"SELECT heroid FROM " . TB_PREFIX . "hero WHERE uid = $uid AND dead = 0 LIMIT 1"
);
self::$livingCache[$uid] = ($res && mysqli_fetch_row($res)) ? true : false;
}
return self::$livingCache[$uid];
}
/** Test helper: clear per-request caches (mirrors HeroItems invalidation). */
public static function flushCache()
{
self::$cache = array();
self::$livingCache = array();
}
}
+632
View File
@@ -0,0 +1,632 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : HeroItems.php ##
## Type : T4 Hero port - items backend (Phase 2) ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
## ##
## Backend for hero inventory / equipment / consumables / silver. ##
## - All statements are prepared (mysqli), all ids cast to int. ##
## - Per-request memoization caches (invalidated on every write), matching ##
## the Database.php cache convention. ##
## - Consumables whose game mechanic lands in a later phase (bandages in ##
## battle, cages in oasis attacks, law tablets in loyalty, artwork CP) ##
## return HeroItems::USE_DEFERRED so the caller knows the item exists ##
## but its effect is wired elsewhere. They are NOT silently consumed. ##
## ##
#################################################################################
class HeroItems
{
/** useItem() result codes */
const USE_OK = 1; // consumed, effect applied here
const USE_DEFERRED = 2; // valid item, but its effect is applied by another subsystem (Phase 3/5)
const USE_INVALID = 0; // unknown item / not owned / not usable now
/** @var array per-request caches, keyed by uid */
private static $inventoryCache = array();
private static $bonusCache = array();
/** @var mysqli */
private $db;
public function __construct()
{
global $database;
// Reuse the game's single mysqli link.
$this->db = $database->dblink;
// Catalog is idempotent to include (guarded defines).
global $heroItemCatalog, $heroAdventureConfig, $heroSlotIndex;
if (!isset($heroItemCatalog)) {
include_once __DIR__ . '/Data/hero_items.php';
}
}
/* =========================================================================
* READS
* ===================================================================== */
/**
* Full inventory of a user (equipped + bag), catalog-joined.
* Rows whose itemid vanished from the catalog are returned flagged with
* 'orphan' => true instead of being hidden - anomaly surfaced, not altered.
*/
public function getInventory($uid)
{
global $heroItemCatalog;
$uid = (int) $uid;
if (isset(self::$inventoryCache[$uid])) {
return self::$inventoryCache[$uid];
}
$stmt = $this->db->prepare(
"SELECT id, uid, heroid, itemid, slot, stat_value, quantity, equipped, tstamp
FROM " . TB_PREFIX . "hero_items
WHERE uid = ? ORDER BY equipped DESC, slot ASC, itemid ASC"
);
$stmt->bind_param('i', $uid);
$stmt->execute();
$res = $stmt->get_result();
$rows = array();
while ($row = $res->fetch_assoc()) {
if (isset($heroItemCatalog[$row['itemid']])) {
$row['def'] = $heroItemCatalog[$row['itemid']];
$row['name'] = heroItemName($row['itemid']);
$row['orphan'] = false;
} else {
$row['def'] = null;
$row['name'] = 'Unknown item #' . $row['itemid'];
$row['orphan'] = true;
}
$rows[] = $row;
}
$stmt->close();
self::$inventoryCache[$uid] = $rows;
return $rows;
}
/** Equipped items only, indexed by slot (one item per slot). */
public function getEquipped($uid)
{
$equipped = array();
foreach ($this->getInventory($uid) as $row) {
if ($row['equipped'] == 1 && !$row['orphan']) {
$equipped[(int) $row['slot']] = $row;
}
}
return $equipped;
}
/** Silver balance (0 if the user has no hero row). */
public function getSilver($uid)
{
$uid = (int) $uid;
$stmt = $this->db->prepare("SELECT silver FROM " . TB_PREFIX . "hero WHERE uid = ? LIMIT 1");
$stmt->bind_param('i', $uid);
$stmt->execute();
$stmt->bind_result($silver);
$found = $stmt->fetch();
$stmt->close();
return $found ? (int) $silver : 0;
}
/* =========================================================================
* BONUS AGGREGATION
* Returns a normalized array all later phases read from:
* [
* 'fight_strength' => int, 'damage_reduce' => int,
* 'regen_hp' => int, 'xp_percent' => int,
* 'culture_points' => int, 'train_cavalry' => int, 'train_infantry' => int,
* 'vs_natars' => int, 'raid_percent' => int,
* 'return_speed' => int, 'speed_own' => int, 'speed_ally' => int,
* 'troop_speed_20' => int, 'horse_speed' => int, 'mount_speed' => int,
* 'unit_bonus' => [ unitId => perUnitValue, ... ]
* ]
* ===================================================================== */
public function getBonuses($uid)
{
$uid = (int) $uid;
if (isset(self::$bonusCache[$uid])) {
return self::$bonusCache[$uid];
}
$bonuses = array(
HB_FIGHT => 0, HB_DMG_REDUCE => 0, HB_REGEN_HP => 0, HB_XP => 0,
HB_CP => 0, HB_TRAIN_CAV => 0, HB_TRAIN_INF => 0, HB_VS_NATARS => 0,
HB_RAID => 0, HB_RETURN_SPEED => 0, HB_SPEED_OWN => 0, HB_SPEED_ALLY => 0,
HB_TROOP_SPEED_20 => 0, HB_HORSE_SPEED => 0, HB_MOUNT_SPEED => 0,
HB_UNIT_BONUS => array(),
);
$equipped = $this->getEquipped($uid);
$hasHorse = isset($equipped[HSLOT_HORSE]);
foreach ($equipped as $row) {
$def = $row['def'];
// Spurs count only while a horse is equipped.
if (!empty($def['requires_horse']) && !$hasHorse) {
continue;
}
foreach ($def['bonus'] as $type => $value) {
if ($type === HB_UNIT_BONUS) {
$u = (int) $value['unit'];
if (!isset($bonuses[HB_UNIT_BONUS][$u])) {
$bonuses[HB_UNIT_BONUS][$u] = 0;
}
$bonuses[HB_UNIT_BONUS][$u] += (int) $value['per_unit'];
continue;
}
if (isset($bonuses[$type])) {
$bonuses[$type] += $this->scaledBonusValue($def, (int) $value);
}
}
}
self::$bonusCache[$uid] = $bonuses;
return $bonuses;
}
/**
* Apply the server-speed scaling rules from the design sheet:
* 'x2_on_speed' => true -> value * 2 on speed servers (spurs, horses)
* 'x2_on_speed' => false -> value / 2 on speed servers (culture helmets)
* key absent -> value unchanged
* "Speed server" = SPEED >= 3 (per the sheet's "(1x) / (3x)" notation).
*/
public function scaledBonusValue(array $def, $value)
{
if (!array_key_exists('x2_on_speed', $def)) {
return $value;
}
$isSpeed = defined('SPEED') && SPEED >= 3;
if (!$isSpeed) {
return $value;
}
return $def['x2_on_speed'] ? $value * 2 : (int) floor($value / 2);
}
/* =========================================================================
* WRITES
* ===================================================================== */
/**
* Grant an item to a user (adventure loot, auction win, admin).
* Consumables (bag slot) stack onto an existing row; gear inserts a new row.
* Returns the hero_items.id of the affected row, or 0 on failure.
*/
public function addItem($uid, $itemid, $quantity = 1, $statValue = 0)
{
global $heroItemCatalog;
$uid = (int) $uid; $itemid = (int) $itemid;
$quantity = max(1, (int) $quantity); $statValue = (int) $statValue;
if (!isset($heroItemCatalog[$itemid])) {
return 0;
}
$def = $heroItemCatalog[$itemid];
$slot = (int) $def['slot'];
$now = time();
if ($slot === HSLOT_BAG) {
// Stack consumables.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero_items
SET quantity = quantity + ?, tstamp = ?
WHERE uid = ? AND itemid = ? AND equipped = 0 LIMIT 1"
);
$stmt->bind_param('iiii', $quantity, $now, $uid, $itemid);
$stmt->execute();
$affected = $stmt->affected_rows;
$stmt->close();
if ($affected > 0) {
$this->invalidateCaches($uid);
$row = $this->findRow($uid, $itemid);
return $row ? (int) $row['id'] : 0;
}
}
$equipped = 0;
$stmt = $this->db->prepare(
"INSERT INTO " . TB_PREFIX . "hero_items
(uid, heroid, itemid, slot, stat_value, quantity, equipped, tstamp)
VALUES (?, 0, ?, ?, ?, ?, ?, ?)"
);
$stmt->bind_param('iiiiiii', $uid, $itemid, $slot, $statValue, $quantity, $equipped, $now);
$stmt->execute();
$newId = (int) $stmt->insert_id;
$stmt->close();
$this->invalidateCaches($uid);
return $newId;
}
/**
* Equip an owned item into its slot. Returns true on success.
* Rules enforced:
* - item must belong to $uid and not be a consumable
* - weapons ('unit' key) require the LIVING hero to be that exact unit
* - anything already in that slot is unequipped first (atomic enough for
* a per-user action; both statements touch only this user's rows)
*/
public function equipItem($uid, $rowId)
{
global $database;
$uid = (int) $uid; $rowId = (int) $rowId;
$row = $this->findRowById($uid, $rowId);
if (!$row || $row['orphan'] || (int) $row['def']['slot'] === HSLOT_BAG) {
return false;
}
// Weapon unit restriction.
if (isset($row['def']['unit'])) {
$hero = $database->getHero($uid);
$heroUnit = 0;
if (is_array($hero)) {
foreach ($hero as $h) {
if ($h['dead'] != 1) { $heroUnit = (int) $h['unit']; break; }
}
}
if ($heroUnit !== (int) $row['def']['unit']) {
return false;
}
}
$slot = (int) $row['def']['slot'];
// Vacate the slot.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero_items SET equipped = 0 WHERE uid = ? AND slot = ? AND equipped = 1"
);
$stmt->bind_param('ii', $uid, $slot);
$stmt->execute();
$stmt->close();
// Equip the requested row.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero_items SET equipped = 1 WHERE uid = ? AND id = ? LIMIT 1"
);
$stmt->bind_param('ii', $uid, $rowId);
$stmt->execute();
$ok = $stmt->affected_rows > 0;
$stmt->close();
$this->invalidateCaches($uid);
return $ok;
}
/** Unequip a specific owned row. Returns true on success. */
public function unequipItem($uid, $rowId)
{
$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"
);
$stmt->bind_param('ii', $uid, $rowId);
$stmt->execute();
$ok = $stmt->affected_rows > 0;
$stmt->close();
$this->invalidateCaches($uid);
return $ok;
}
/**
* Remove quantity from an owned row (used by useItem, auction listing).
* Deletes the row when the stack reaches zero. Returns true on success.
*/
public function removeItem($uid, $rowId, $quantity = 1)
{
$uid = (int) $uid; $rowId = (int) $rowId; $quantity = max(1, (int) $quantity);
// Conditional decrement - never goes below zero even under races.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero_items
SET quantity = quantity - ?
WHERE uid = ? AND id = ? AND quantity >= ? LIMIT 1"
);
$stmt->bind_param('iiii', $quantity, $uid, $rowId, $quantity);
$stmt->execute();
$ok = $stmt->affected_rows > 0;
$stmt->close();
if ($ok) {
$stmt = $this->db->prepare(
"DELETE FROM " . TB_PREFIX . "hero_items WHERE uid = ? AND id = ? AND quantity <= 0 LIMIT 1"
);
$stmt->bind_param('ii', $uid, $rowId);
$stmt->execute();
$stmt->close();
}
$this->invalidateCaches($uid);
return $ok;
}
/**
* Use a consumable. Applies immediate effects (ointment, scroll, bucket,
* book of wisdom, law tablets, artwork); returns USE_DEFERRED for
* consumables whose mechanic is wired into battle processing (bandages,
* cages - Phase 6) WITHOUT consuming them.
* $vid: target village, required for law tablets (loyalty target).
* Returns one of the USE_* class constants.
*/
public function useItem($uid, $rowId, $quantity = 1, $vid = 0)
{
global $database;
$uid = (int) $uid; $rowId = (int) $rowId; $quantity = max(1, (int) $quantity);
$row = $this->findRowById($uid, $rowId);
if (!$row || $row['orphan'] || (int) $row['def']['slot'] !== HSLOT_BAG) {
return self::USE_INVALID;
}
if ((int) $row['quantity'] < $quantity) {
return self::USE_INVALID;
}
$bonus = $row['def']['bonus'];
/* ---- Ointment: +1% HP per unit, cap 99, hero must be alive ---- */
if (isset($bonus[HB_HEAL_SELF])) {
$hero = $this->getLivingHeroRow($uid);
if (!$hero) {
return self::USE_INVALID;
}
$health = (float) $hero['health'];
if ($health >= 99) {
return self::USE_INVALID;
}
// Only consume as many as actually heal (no waste past the 99 cap).
$usable = min($quantity, (int) ceil(99 - $health));
$newHealth = min(99, $health + $usable * (int) $bonus[HB_HEAL_SELF]);
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero SET health = ? WHERE heroid = ? LIMIT 1"
);
$heroid = (int) $hero['heroid'];
$stmt->bind_param('di', $newHealth, $heroid);
$stmt->execute();
$stmt->close();
$this->removeItem($uid, $rowId, $usable);
return self::USE_OK;
}
/* ---- Scroll: +10 XP each ---- */
if (isset($bonus[HB_SCROLL])) {
$hero = $this->getLivingHeroRow($uid);
if (!$hero) {
return self::USE_INVALID;
}
$xp = (int) $bonus[HB_SCROLL] * $quantity;
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero SET experience = experience + ? WHERE heroid = ? LIMIT 1"
);
$heroid = (int) $hero['heroid'];
$stmt->bind_param('ii', $xp, $heroid);
$stmt->execute();
$stmt->close();
$this->removeItem($uid, $rowId, $quantity);
return self::USE_OK;
}
/* ---- Bucket of Water: instant free revive of the dead hero ---- */
if (isset($bonus[HB_BUCKET])) {
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero
SET dead = 0, health = 100, inrevive = 0, lastupdate = ?
WHERE uid = ? AND dead = 1 LIMIT 1"
);
$now = time();
$stmt->bind_param('ii', $now, $uid);
$stmt->execute();
$revived = $stmt->affected_rows > 0;
$stmt->close();
if (!$revived) {
return self::USE_INVALID; // no dead hero to revive
}
$this->removeItem($uid, $rowId, 1); // one bucket per revive
return self::USE_OK;
}
/* ---- Book of Wisdom: reset attribute points ---- */
if (isset($bonus[HB_RESET])) {
$hero = $this->getLivingHeroRow($uid);
if (!$hero) {
return self::USE_INVALID;
}
// Refund all spent points: 5 free points per level gained (same
// convention as Automation::calculateLevelUp). Attributes return to 0.
$level = (int) $hero['level'];
$points = $level * 5;
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero
SET points = ?, attack = 0, defence = 0, attackbonus = 0,
defencebonus = 0, regeneration = 0
WHERE heroid = ? LIMIT 1"
);
$heroid = (int) $hero['heroid'];
$stmt->bind_param('ii', $points, $heroid);
$stmt->execute();
$stmt->close();
$this->removeItem($uid, $rowId, 1);
return self::USE_OK;
}
/* ---- Law Tablet: +1% loyalty per tablet on an OWN village, max 125% ---- */
if (isset($bonus[HB_LOYALTY])) {
$vid = (int) $vid;
if ($vid <= 0) {
return self::USE_INVALID;
}
// Village must belong to the user; cap enforced in SQL so
// concurrent uses can never overshoot 125.
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "vdata
SET loyalty = LEAST(125, loyalty + ?)
WHERE wref = ? AND owner = ? AND loyalty < 125 LIMIT 1"
);
$gain = (int) $bonus[HB_LOYALTY] * $quantity;
$stmt->bind_param('iii', $gain, $vid, $uid);
$stmt->execute();
$ok = $stmt->affected_rows > 0;
$stmt->close();
if (!$ok) {
return self::USE_INVALID; // not owner, or already at cap
}
$this->removeItem($uid, $rowId, $quantity);
return self::USE_OK;
}
/* ---- Artwork: culture points equal to the account's daily CP
* production, capped at 5000 (normal) / 2500 (speed >= 3).
* users.cp is the accumulator culturePoints() ticks into. ---- */
if (isset($bonus[HB_ARTWORK])) {
$stmt = $this->db->prepare(
"SELECT COALESCE(SUM(cp), 0) FROM " . TB_PREFIX . "vdata WHERE owner = ?"
);
$stmt->bind_param('i', $uid);
$stmt->execute();
$stmt->bind_result($dailyCp);
$stmt->fetch();
$stmt->close();
$cap = (defined('SPEED') && SPEED >= 3) ? 2500 : 5000;
$gain = min($cap, (int) $dailyCp) * $quantity;
if ($gain <= 0) {
return self::USE_INVALID;
}
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "users SET cp = cp + ? WHERE id = ? LIMIT 1"
);
$stmt->bind_param('ii', $gain, $uid);
$stmt->execute();
$ok = $stmt->affected_rows > 0;
$stmt->close();
if (!$ok) {
return self::USE_INVALID;
}
$this->removeItem($uid, $rowId, $quantity);
return self::USE_OK;
}
/* ---- Deferred consumables (mechanic lands in Phase 6 with the UI:
* bandages consume on post-battle healing, cages on oasis
* attacks). NOT consumed here. ---- */
if (isset($bonus[HB_HEAL_TROOP]) || isset($bonus[HB_CAGE])) {
return self::USE_DEFERRED;
}
return self::USE_INVALID;
}
/* =========================================================================
* SILVER (race-safe conditional updates)
* ===================================================================== */
/** Add silver to the user's hero row. Returns true if a hero row exists. */
public function addSilver($uid, $amount)
{
$uid = (int) $uid; $amount = max(0, (int) $amount);
if ($amount === 0) {
return true;
}
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero SET silver = silver + ? WHERE uid = ? LIMIT 1"
);
$stmt->bind_param('ii', $amount, $uid);
$stmt->execute();
$ok = $stmt->affected_rows > 0;
$stmt->close();
return $ok;
}
/**
* Spend silver. The WHERE silver >= ? guard makes this race-safe:
* two concurrent spends can never take the balance negative.
*/
public function spendSilver($uid, $amount)
{
$uid = (int) $uid; $amount = max(0, (int) $amount);
if ($amount === 0) {
return true;
}
$stmt = $this->db->prepare(
"UPDATE " . TB_PREFIX . "hero
SET silver = silver - ?
WHERE uid = ? AND silver >= ? LIMIT 1"
);
$stmt->bind_param('iii', $amount, $uid, $amount);
$stmt->execute();
$ok = $stmt->affected_rows > 0;
$stmt->close();
return $ok;
}
/* =========================================================================
* INTERNALS
* ===================================================================== */
private function invalidateCaches($uid)
{
unset(self::$inventoryCache[(int) $uid], self::$bonusCache[(int) $uid]);
}
private function findRow($uid, $itemid)
{
foreach ($this->getInventory($uid) as $row) {
if ((int) $row['itemid'] === (int) $itemid && $row['equipped'] == 0) {
return $row;
}
}
return null;
}
private function findRowById($uid, $rowId)
{
foreach ($this->getInventory($uid) as $row) {
if ((int) $row['id'] === (int) $rowId) {
return $row;
}
}
return null;
}
/** Fetch the raw LIVING hero row directly (health/level/points included). */
private function getLivingHeroRow($uid)
{
$uid = (int) $uid;
$stmt = $this->db->prepare(
"SELECT heroid, uid, unit, level, points, experience, health, dead
FROM " . TB_PREFIX . "hero WHERE uid = ? AND dead = 0 LIMIT 1"
);
$stmt->bind_param('i', $uid);
$stmt->execute();
$res = $stmt->get_result();
$row = $res->fetch_assoc();
$stmt->close();
return $row ?: null;
}
}
+107
View File
@@ -2678,7 +2678,114 @@ if (!function_exists('tz_loc_topic')) {
'Unoccupied Oasis' => TZ_RT_UNOCC_OASIS,
'New village founded' => TZ_RT_NEW_VILLAGE,
'Settlers returned - valley occupied' => TZ_RT_VALLEY_OCCUPIED,
// T4 hero port (Phase 6)
'Hero returned from an adventure' => TZ_RT_ADV_RETURNED,
'Hero fell on an adventure' => TZ_RT_ADV_FELL,
'Auction won' => TZ_RT_AUC_WON,
'Auction sold' => TZ_RT_AUC_SOLD,
'Auction expired' => TZ_RT_AUC_EXPIRED,
);
return strtr($s, $map);
}
}
/* =============================================================================
* T4 HERO PORT (Phase 6) - items / adventures / auction house
* ========================================================================== */
tz_def('HERO_T4_TAB_HERO', 'Hero');
tz_def('HERO_T4_TAB_ITEMS', 'Inventory');
tz_def('HERO_T4_TAB_ADVENTURES', 'Adventures');
tz_def('HERO_T4_TAB_AUCTION', 'Auctions');
tz_def('HERO_SILVER', 'Silver');
tz_def('HERO_EXPERIENCE', 'Experience');
tz_def('RESOURCES', 'Resources');
/* Equipment slots */
tz_def('HERO_SLOT_1', 'Helmet');
tz_def('HERO_SLOT_2', 'Body');
tz_def('HERO_SLOT_3', 'Right hand');
tz_def('HERO_SLOT_4', 'Left hand');
tz_def('HERO_SLOT_5', 'Shoes');
tz_def('HERO_SLOT_6', 'Horse');
tz_def('HERO_SLOT_7', 'Bag');
/* Inventory actions */
tz_def('HERO_ITEMS_EQUIPPED', 'Equipped');
tz_def('HERO_ITEMS_BAG', 'Inventory');
tz_def('HERO_ITEMS_EMPTY', 'Your hero owns no items yet. Adventures and the auction house are good places to find some.');
tz_def('HERO_EQUIP', 'Equip');
tz_def('HERO_UNEQUIP', 'Take off');
tz_def('HERO_USE_ITEM', 'Use');
tz_def('HERO_QUANTITY', 'Amount');
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_EQUIP_FAIL', 'This item cannot be equipped (wrong hero unit or item type).');
tz_def('HERO_UNEQUIP_OK', 'Item taken off.');
/* Adventures */
tz_def('HERO_ADV_NORMAL', 'a normal adventure');
tz_def('HERO_ADV_HARD', 'a hard adventure');
tz_def('HERO_ADV_LIST', 'Available adventures');
tz_def('HERO_ADV_NONE', 'No adventures available right now. New ones appear over time.');
tz_def('HERO_ADV_DIFFICULTY', 'Difficulty');
tz_def('HERO_ADV_DIFF_NORMAL', 'Normal');
tz_def('HERO_ADV_DIFF_HARD', 'Hard');
tz_def('HERO_ADV_DURATION', 'Travel time (one way)');
tz_def('HERO_ADV_EXPIRES', 'Expires in');
tz_def('HERO_ADV_GO', 'Start adventure');
tz_def('HERO_ADV_RUNNING', 'Your hero is on an adventure and will arrive in');
tz_def('HERO_ADV_START_OK', 'Your hero has set off on the adventure.');
tz_def('HERO_ADV_START_NOHERO', 'You need a living hero to start an adventure.');
tz_def('HERO_ADV_START_AWAY', 'Your hero is not at home.');
tz_def('HERO_ADV_START_FAIL', 'This adventure is no longer available.');
tz_def('HERO_ADV_RETURNED', 'Your hero has returned from');
tz_def('HERO_ADV_REWARD', 'Reward');
tz_def('HERO_ADV_AMOUNT', 'Amount');
tz_def('HERO_ADV_ITEM_FOUND', 'Item found');
tz_def('HERO_ADV_HP_LOST', 'Health lost');
tz_def('HERO_ADV_DIED', 'Your hero fell on');
tz_def('HERO_ADV_DIED_INFO', "All loot was lost. The hero can be revived at the Hero's Mansion.");
/* Auction house */
tz_def('HERO_AUC_OPEN', 'Open auctions');
tz_def('HERO_AUC_NONE', 'There are no open auctions at the moment.');
tz_def('HERO_AUC_ITEM', 'Item');
tz_def('HERO_AUC_PRICE', 'Current price');
tz_def('HERO_AUC_FINAL_PRICE', 'Final price');
tz_def('HERO_AUC_TIME_LEFT', 'Ends in');
tz_def('HERO_AUC_YOUR_MAX', 'Your maximum');
tz_def('HERO_AUC_BID', 'Bid');
tz_def('HERO_AUC_BID_OK', 'Your bid was placed. You are the highest bidder.');
tz_def('HERO_AUC_BID_OUTBID', 'Your bid was immediately outbid by a higher maximum.');
tz_def('HERO_AUC_BID_FAIL', 'Your bid was not accepted.');
tz_def('HERO_AUC_BID_NOSILVER', 'You do not have enough silver for this bid.');
tz_def('HERO_AUC_MY_BIDS', 'My bids');
tz_def('HERO_AUC_MY_SALES', 'My sales');
tz_def('HERO_AUC_SELL', 'Sell an item');
tz_def('HERO_AUC_SELL_OK', 'Your item has been listed.');
tz_def('HERO_AUC_SELL_FAIL', 'This item cannot be listed (equipped items must be taken off first).');
tz_def('HERO_AUC_START_PRICE', 'Starting price');
tz_def('HERO_AUC_DURATION', 'Duration');
tz_def('HERO_AUC_LIST', 'List item');
tz_def('HERO_AUC_SELLER_NPC', 'Merchant');
tz_def('HERO_AUC_WON', 'You won the auction for');
tz_def('HERO_AUC_SOLD', 'Your auction sold:');
tz_def('HERO_AUC_REFUND', 'Refunded from your maximum bid');
tz_def('HERO_AUC_FEE', 'Auction fee');
tz_def('HERO_AUC_PAYOUT', 'Payout');
tz_def('HERO_AUC_EXPIRED', 'Your auction ended without bids. The item was returned to your inventory:');
/* Report topics (raw English strings live in ndata.topic) */
tz_def('TZ_RT_ADV_RETURNED', 'Hero returned from an adventure');
tz_def('TZ_RT_ADV_FELL', 'Hero fell on an adventure');
tz_def('TZ_RT_AUC_WON', 'Auction won');
tz_def('TZ_RT_AUC_SOLD', 'Auction sold');
tz_def('TZ_RT_AUC_EXPIRED', 'Auction expired');
/* T4 hero port - movement display (dorf1 + rally point) */
tz_def('HERO_ADV_MOV_OUT', 'Hero on an adventure');
tz_def('HERO_ADV_MOV_BACK', 'Hero returning from an adventure');
tz_def('HERO_ADV_MOV_SHORT', 'Adventure');
+8
View File
@@ -452,6 +452,14 @@ class Message
case 24:
case 25:
return 24;
// Hero adventure report (T4 hero port)
case 26:
return 26;
// Hero auction report (T4 hero port)
case 27:
return 27;
}
return $type;
}
+6 -1
View File
@@ -796,7 +796,12 @@ class Units {
$speeds[] = $GLOBALS['u'.$heroUnit]['speed'];
}
return $generator->procDistanceTime($fromCor, $toCor, min($speeds), $mode, $from);
$walkTime = $generator->procDistanceTime($fromCor, $toCor, min($speeds), $mode, $from);
// T4 hero port (Phase 5): boots (>20 tiles), pennant (own villages)
// and standard (ally villages) shorten the trip when the hero rides
// along. Returns the input unchanged when NEW_FUNCTIONS_HERO_T4 is off.
return HeroBattleBonus::adjustTravelTime($owner, $unitArray[10] ?? 0, $from, $to, $walkTime);
}
public function startRaidList($post){
+5
View File
@@ -207,6 +207,11 @@ $hasRally = $village->resarray['f39'] > 0;
$set = $database->getMovement(5,$village->wid,0);
$cnt = count($set);
foreach($out as $u) if($u['vref']==$village->wid) $cnt++;
// T4 hero port: adventure legs count as troops on their way too.
if (defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4) {
$cnt += count($database->getMovement(20,$village->wid,0));
$cnt += count($database->getMovement(21,$village->wid,1));
}
?>
<?php if($cnt>=1): ?>
<h4><?= TROOPS_ON_THEIR_WAY ?></h4>
+35
View File
@@ -20,6 +20,41 @@
#################################################################################
$outgoing = $database->getMovement(3, $village->wid, 0);
// T4 hero port: hero adventure legs (out = from this village, back = to it).
$advWalking = [];
if (defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4) {
foreach ($database->getMovement(20, $village->wid, 0) as $m) { $m['t4adv'] = 'out'; $advWalking[] = $m; }
foreach ($database->getMovement(21, $village->wid, 1) as $m) { $m['t4adv'] = 'back'; $advWalking[] = $m; }
}
?>
<?php foreach ($advWalking as $m):
$session->timer++;
$action = $m['t4adv'] === 'out'
? (defined('HERO_ADV_MOV_OUT') ? HERO_ADV_MOV_OUT : 'Hero on an adventure')
: (defined('HERO_ADV_MOV_BACK') ? HERO_ADV_MOV_BACK : 'Hero returning from an adventure');
$dt = $generator->procMtime($m['endtime']);
?>
<table class="troop_details" cellpadding="1" cellspacing="1">
<thead><tr>
<td class="role"><a href="karte.php?d=<?= $village->wid?>&c=<?= $generator->getMapCheck($village->wid)?>"><?= $village->vname?></a></td>
<td colspan="2"><?= $action?></td>
</tr></thead>
<tbody class="units">
<tr><th>&nbsp;</th><td style="width:40px;"><img src="img/x.gif" class="unit uhero" title="<?php echo U0; ?>"></td><td>&nbsp;</td></tr>
<tr><th><?= TROOPS?></th><td>1</td><td>&nbsp;</td></tr>
</tbody>
<tbody class="infos"><tr>
<th><?= ARRIVAL?></th>
<td colspan="2">
<div class="in small"><span id="timer<?= $session->timer?>"><?= $generator->getTimeFormat($m['endtime']-time())?></span> h</div>
<div class="at"><?= $dt[0]!='today'?ON.' '.$dt[0].' ':''?><?= AT?> <?= $dt[1]?></div>
</td>
</tr></tbody>
</table>
<?php endforeach;?>
<?php
?>
<?php foreach ($outgoing as $m):
+16
View File
@@ -71,7 +71,23 @@
if(isset($_GET['land']) && $village->resarray['f' . $id] >= 1) {
include_once("37_land.tpl");
} else if (defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4
&& $village->resarray['f' . $id] >= 1
&& isset($_GET['t4tab'])
&& in_array($_GET['t4tab'], ['items', 'adventures', 'auction'], true)) {
// T4 hero port (Phase 6): items / adventures / auction tabs.
// The classic hero flow below stays byte-identical when the
// feature flag is off or no tab is selected.
$t4tab = $_GET['t4tab'];
include_once("37_t4nav.tpl");
include_once("37_" . $t4tab . ".tpl");
} else if ($village->resarray['f' . $id] >= 1) {
if (defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4) {
$t4tab = 'hero';
include_once("37_t4nav.tpl");
}
$include_training = true;
$include_revive = false;
if (isset($heroes) && is_array($heroes) && count($heroes)) {
+78
View File
@@ -0,0 +1,78 @@
<?php
#################################################################################
# T4 hero adventures tab (37_adventures.tpl) - Phase 6 #
# POST: t4action=startadv, advid. Countdown spans reuse the existing #
# $session->timer JS convention (same as 37.tpl's training timer). #
#################################################################################
$t4Adventures = new HeroAdventure();
$t4Msg = '';
if (isset($_POST['t4action'], $_POST['advid']) && $_POST['t4action'] === 'startadv') {
$t4Result = $t4Adventures->startAdventure($session->uid, (int) $_POST['advid']);
if ($t4Result === HeroAdventure::START_OK) {
$t4Msg = HERO_ADV_START_OK;
} elseif ($t4Result === HeroAdventure::START_NO_HERO) {
$t4Msg = HERO_ADV_START_NOHERO;
} elseif ($t4Result === HeroAdventure::START_HERO_AWAY) {
$t4Msg = HERO_ADV_START_AWAY;
} else {
$t4Msg = HERO_ADV_START_FAIL;
}
}
// Top up the list opportunistically (respects max/refresh limits internally).
$t4Adventures->generateOffers($session->uid);
$t4Offers = $t4Adventures->getOffers($session->uid);
$t4Running = $t4Adventures->getRunning($session->uid);
$t4Now = time();
?>
<?php if ($t4Msg !== '') { ?>
<p class="message" style="font-weight:bold;"><?php echo $t4Msg; ?></p>
<?php } ?>
<?php if ($t4Running) { ?>
<table id="distribution" cellpadding="1" cellspacing="1">
<thead>
<tr><th><?php echo HERO_ADV_RUNNING; ?>
<span id="timer<?php echo ++$session->timer; ?>"><?php echo $generator->getTimeFormat(max(0, $t4Running['endtime'] - $t4Now)); ?></span>
</th></tr>
</thead>
</table>
<?php } ?>
<table id="distribution" cellpadding="1" cellspacing="1" style="margin-top:10px;">
<thead>
<tr><th colspan="4"><?php echo HERO_ADV_LIST; ?></th></tr>
<tr>
<td><b><?php echo HERO_ADV_DIFFICULTY; ?></b></td>
<td><b><?php echo HERO_ADV_DURATION; ?></b></td>
<td><b><?php echo HERO_ADV_EXPIRES; ?></b></td>
<td></td>
</tr>
</thead>
<tbody>
<?php if (count($t4Offers)) { ?>
<?php foreach ($t4Offers as $t4Offer) { ?>
<tr>
<td><?php echo ((int) $t4Offer['difficulty'] === 1) ? HERO_ADV_DIFF_HARD : HERO_ADV_DIFF_NORMAL; ?></td>
<td><?php echo $generator->getTimeFormat((int) $t4Offer['duration']); ?></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;">
<?php if (!$t4Running) { ?>
<form action="" method="POST" style="margin:0;">
<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; ?>">
</form>
<?php } else { ?>-<?php } ?>
</td>
</tr>
<?php } ?>
<?php } else { ?>
<tr><td colspan="4"><?php echo HERO_ADV_NONE; ?></td></tr>
<?php } ?>
</tbody>
</table>
+160
View File
@@ -0,0 +1,160 @@
<?php
#################################################################################
# T4 hero auction tab (37_auction.tpl) - Phase 6 #
# POST actions: #
# t4action=bid (aucid, maxbid) #
# t4action=sell (rowid, qty, price, duration) #
# bid_max is only ever shown for the viewer's OWN bids (getMyBids). #
#################################################################################
$t4Auction = new HeroAuction();
$t4Msg = '';
if (isset($_POST['t4action'])) {
if ($_POST['t4action'] === 'bid' && isset($_POST['aucid'], $_POST['maxbid'])) {
$t4Result = $t4Auction->placeBid($session->uid, (int) $_POST['aucid'], (int) $_POST['maxbid']);
if ($t4Result === HeroAuction::BID_OK) {
$t4Msg = HERO_AUC_BID_OK;
} elseif ($t4Result === HeroAuction::BID_OUTBID) {
$t4Msg = HERO_AUC_BID_OUTBID;
} elseif ($t4Result === HeroAuction::BID_NO_SILVER) {
$t4Msg = HERO_AUC_BID_NOSILVER;
} else {
$t4Msg = HERO_AUC_BID_FAIL;
}
} elseif ($_POST['t4action'] === 'sell'
&& isset($_POST['rowid'], $_POST['qty'], $_POST['price'], $_POST['duration'])) {
$t4Result = $t4Auction->createAuction(
$session->uid, (int) $_POST['rowid'],
(int) $_POST['qty'], (int) $_POST['price'], (int) $_POST['duration']
);
$t4Msg = ($t4Result > 0) ? HERO_AUC_SELL_OK : HERO_AUC_SELL_FAIL;
}
}
$t4Open = $t4Auction->getOpenAuctions();
$t4MyBids = $t4Auction->getMyBids($session->uid);
$t4MySales = $t4Auction->getMySales($session->uid);
$t4Now = time();
// Unequipped items sellable from the inventory.
$t4Sellable = array();
foreach ($t4HeroItems->getInventory($session->uid) as $t4Row) {
if ($t4Row['equipped'] == 0 && !$t4Row['orphan']) {
$t4Sellable[] = $t4Row;
}
}
?>
<?php if ($t4Msg !== '') { ?>
<p class="message" style="font-weight:bold;"><?php echo $t4Msg; ?></p>
<?php } ?>
<table id="distribution" cellpadding="1" cellspacing="1">
<thead>
<tr><th colspan="5"><?php echo HERO_AUC_OPEN; ?></th></tr>
<tr>
<td><b><?php echo HERO_AUC_ITEM; ?></b></td>
<td><b><?php echo HERO_QUANTITY; ?></b></td>
<td><b><?php echo HERO_AUC_PRICE; ?></b></td>
<td><b><?php echo HERO_AUC_TIME_LEFT; ?></b></td>
<td><b><?php echo HERO_AUC_BID; ?></b></td>
</tr>
</thead>
<tbody>
<?php if (count($t4Open)) { ?>
<?php foreach ($t4Open as $t4A) { ?>
<tr>
<td><span class="heroT4Item item<?php echo (int) $t4A['itemid']; ?>"></span> <?php echo $t4A['name']; ?>
<?php if ((int) $t4A['seller'] === 0) { ?><small>(<?php echo HERO_AUC_SELLER_NPC; ?>)</small><?php } ?>
</td>
<td style="text-align:center;"><?php echo (int) $t4A['quantity']; ?></td>
<td style="text-align:right;"><?php echo number_format((int) $t4A['silver_current']); ?></td>
<td><span id="timer<?php echo ++$session->timer; ?>"><?php echo $generator->getTimeFormat(max(0, $t4A['time_end'] - $t4Now)); ?></span></td>
<td style="width:170px;text-align:center;">
<?php if ((int) $t4A['seller'] !== $session->uid) { ?>
<form action="" method="POST" style="margin:0;">
<input type="hidden" name="t4action" value="bid">
<input type="hidden" name="aucid" value="<?php echo (int) $t4A['id']; ?>">
<input type="text" name="maxbid" size="6" style="text-align:right;"
value="<?php echo (int) $t4A['silver_current'] + ((int) $t4A['bidder'] > 0 ? 1 : 0); ?>">
<input type="submit" value="<?php echo HERO_AUC_BID; ?>">
</form>
<?php } else { ?>-<?php } ?>
</td>
</tr>
<?php } ?>
<?php } else { ?>
<tr><td colspan="5"><?php echo HERO_AUC_NONE; ?></td></tr>
<?php } ?>
</tbody>
</table>
<?php if (count($t4MyBids)) { ?>
<table id="distribution" cellpadding="1" cellspacing="1" style="margin-top:10px;">
<thead>
<tr><th colspan="4"><?php echo HERO_AUC_MY_BIDS; ?></th></tr>
</thead>
<tbody>
<?php foreach ($t4MyBids as $t4A) { ?>
<tr>
<td><?php echo $t4A['name']; ?> (<?php echo (int) $t4A['quantity']; ?>x)</td>
<td style="text-align:right;"><?php echo HERO_AUC_PRICE; ?>: <?php echo number_format((int) $t4A['silver_current']); ?></td>
<td style="text-align:right;"><?php echo HERO_AUC_YOUR_MAX; ?>: <?php echo number_format((int) $t4A['bid_max']); ?></td>
<td><span id="timer<?php echo ++$session->timer; ?>"><?php echo $generator->getTimeFormat(max(0, $t4A['time_end'] - $t4Now)); ?></span></td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } ?>
<?php if (count($t4MySales)) { ?>
<table id="distribution" cellpadding="1" cellspacing="1" style="margin-top:10px;">
<thead>
<tr><th colspan="3"><?php echo HERO_AUC_MY_SALES; ?></th></tr>
</thead>
<tbody>
<?php foreach ($t4MySales as $t4A) { ?>
<tr>
<td><?php echo $t4A['name']; ?> (<?php echo (int) $t4A['quantity']; ?>x)</td>
<td style="text-align:right;"><?php echo HERO_AUC_PRICE; ?>: <?php echo number_format((int) $t4A['silver_current']); ?></td>
<td><span id="timer<?php echo ++$session->timer; ?>"><?php echo $generator->getTimeFormat(max(0, $t4A['time_end'] - $t4Now)); ?></span></td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } ?>
<?php if (count($t4Sellable)) { ?>
<table id="distribution" cellpadding="1" cellspacing="1" style="margin-top:10px;">
<thead>
<tr><th colspan="5"><?php echo HERO_AUC_SELL; ?></th></tr>
</thead>
<tbody>
<tr>
<form action="" method="POST" style="margin:0;">
<input type="hidden" name="t4action" value="sell">
<td>
<select name="rowid">
<?php foreach ($t4Sellable as $t4Row) { ?>
<option value="<?php echo (int) $t4Row['id']; ?>">
<?php echo $t4Row['name']; ?><?php if ((int) $t4Row['quantity'] > 1) echo ' (' . (int) $t4Row['quantity'] . 'x)'; ?>
</option>
<?php } ?>
</select>
</td>
<td><?php echo HERO_QUANTITY; ?>: <input type="text" name="qty" value="1" size="3" style="text-align:center;"></td>
<td><?php echo HERO_AUC_START_PRICE; ?>: <input type="text" name="price" value="10" size="5" style="text-align:right;"></td>
<td><?php echo HERO_AUC_DURATION; ?>:
<select name="duration">
<option value="14400">4h</option>
<option value="28800">8h</option>
<option value="86400">24h</option>
</select>
</td>
<td style="text-align:center;"><input type="submit" value="<?php echo HERO_AUC_LIST; ?>"></td>
</form>
</tr>
</tbody>
</table>
<?php } ?>
+112
View File
@@ -0,0 +1,112 @@
<?php
#################################################################################
# T4 hero inventory & equipment tab (37_items.tpl) - Phase 6 #
# Inline POST handling (same convention as 37_hero.tpl's rename form): #
# t4action=equip|unequip|useitem, rowid, qty #
# Law tablets target the CURRENT village ($village->wid). #
#################################################################################
$t4Msg = '';
if (isset($_POST['t4action'], $_POST['rowid'])) {
$t4RowId = (int) $_POST['rowid'];
switch ($_POST['t4action']) {
case 'equip':
$t4Msg = $t4HeroItems->equipItem($session->uid, $t4RowId)
? HERO_EQUIP_OK : HERO_EQUIP_FAIL;
break;
case 'unequip':
$t4Msg = $t4HeroItems->unequipItem($session->uid, $t4RowId)
? HERO_UNEQUIP_OK : HERO_ITEM_USE_FAIL;
break;
case 'useitem':
$t4Qty = max(1, (int) ($_POST['qty'] ?? 1));
$t4Result = $t4HeroItems->useItem($session->uid, $t4RowId, $t4Qty, $village->wid);
if ($t4Result === HeroItems::USE_OK) {
$t4Msg = HERO_ITEM_USED_OK;
} elseif ($t4Result === HeroItems::USE_DEFERRED) {
$t4Msg = HERO_ITEM_USE_BATTLE;
} else {
$t4Msg = HERO_ITEM_USE_FAIL;
}
break;
}
}
$t4Inventory = $t4HeroItems->getInventory($session->uid);
$t4Equipped = $t4HeroItems->getEquipped($session->uid);
?>
<?php if ($t4Msg !== '') { ?>
<p class="message" style="font-weight:bold;"><?php echo $t4Msg; ?></p>
<?php } ?>
<table id="distribution" cellpadding="1" cellspacing="1">
<thead>
<tr><th colspan="4"><?php echo HERO_ITEMS_EQUIPPED; ?></th></tr>
</thead>
<tbody>
<?php for ($t4Slot = 1; $t4Slot <= 6; $t4Slot++) { ?>
<tr>
<td style="width:110px;"><b><?php echo constant('HERO_SLOT_' . $t4Slot); ?></b></td>
<?php if (isset($t4Equipped[$t4Slot])) { $t4Row = $t4Equipped[$t4Slot]; ?>
<td><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;">
<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>
</td>
<?php } else { ?>
<td colspan="3" style="color:#888;">-</td>
<?php } ?>
</tr>
<?php } ?>
</tbody>
</table>
<table id="distribution" cellpadding="1" cellspacing="1" style="margin-top:10px;">
<thead>
<tr><th colspan="4"><?php echo HERO_ITEMS_BAG; ?></th></tr>
</thead>
<tbody>
<?php
$t4HasUnequipped = false;
foreach ($t4Inventory as $t4Row) {
if ($t4Row['equipped'] == 1 || $t4Row['orphan']) {
continue;
}
$t4HasUnequipped = true;
$t4IsBag = ((int) $t4Row['def']['slot'] === HSLOT_BAG);
?>
<tr>
<td><span class="heroT4Item item<?php echo (int) $t4Row['itemid']; ?>"></span> <?php echo $t4Row['name']; ?></td>
<td style="width:80px;text-align:center;">
<?php echo $t4IsBag ? (int) $t4Row['quantity'] . 'x' : 'T' . (int) $t4Row['def']['tier']; ?>
</td>
<td style="width:110px;text-align:center;"><?php echo constant('HERO_SLOT_' . (int) $t4Row['def']['slot']); ?></td>
<td style="width:170px;text-align:center;">
<form action="" method="POST" style="margin:0;">
<input type="hidden" name="rowid" value="<?php echo (int) $t4Row['id']; ?>">
<?php if ($t4IsBag) { ?>
<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 { ?>
<input type="hidden" name="t4action" value="equip">
<input type="submit" value="<?php echo HERO_EQUIP; ?>">
<?php } ?>
</form>
</td>
</tr>
<?php } ?>
<?php if (!$t4HasUnequipped) { ?>
<tr><td colspan="4"><?php echo HERO_ITEMS_EMPTY; ?></td></tr>
<?php } ?>
</tbody>
</table>
+28
View File
@@ -0,0 +1,28 @@
<?php
#################################################################################
# T4 hero mansion tab navigation (37_t4nav.tpl) - Phase 6 #
# Included by 37.tpl with $t4tab set to 'hero'|'items'|'adventures'|'auction'. #
# Silver balance shown on the right; reuses existing anchor styling. #
#################################################################################
$t4HeroItems = new HeroItems();
$t4Silver = $t4HeroItems->getSilver($session->uid);
$t4Tabs = [
'hero' => ['label' => HERO_T4_TAB_HERO, 'url' => 'build.php?id=' . $id],
'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'],
];
?>
<link rel="stylesheet" href="css/hero_items.css" type="text/css">
<div class="heroT4Nav" style="margin:6px 0 10px 0;">
<?php foreach ($t4Tabs as $key => $tab) { ?>
<?php if ($key === $t4tab) { ?>
<span style="font-weight:bold;margin-right:14px;"><?php echo $tab['label']; ?></span>
<?php } else { ?>
<a href="<?php echo $tab['url']; ?>" style="margin-right:14px;"><?php echo $tab['label']; ?></a>
<?php } ?>
<?php } ?>
<span style="float:right;"><b><?php echo HERO_SILVER; ?>:</b> <?php echo number_format($t4Silver); ?></span>
<div style="clear:both;"></div>
</div>
+120
View File
@@ -0,0 +1,120 @@
<?php
#################################################################################
# Hero adventure report (26.tpl) - T4 hero port, Phase 1 #
# Handles ntype 26. The reward payload is stored in ndata.data as a #
# query-string, e.g.: #
# difficulty=1&exp=22&silver=80&wood=300&clay=0&iron=450&crop=120&hp=18 #
# &item=HERO_ITEM_OINTMENT&itemqty=2 #
# Any missing key simply renders as 0 / hidden, so partial payloads are safe. #
# #
# Lang constants (HERO_ADV_*, HERO_ITEM_*) arrive in the Phase 6 lang pass; #
# the tz() helper below falls back to a readable literal until then so this #
# template never emits "Undefined constant" before the rest of the port lands. #
#################################################################################
// Tiny local fallback so this file is self-contained in Phase 1.
if (!function_exists('tz_hero_adv')) {
function tz_hero_adv($const, $fallback) {
return defined($const) ? constant($const) : $fallback;
}
}
parse_str((string)($message->readingNotice['data'] ?? ''), $adv);
$difficulty = (int)($adv['difficulty'] ?? 0);
$died = (int)($adv['died'] ?? 0);
$exp = (int)($adv['exp'] ?? 0);
$silver = (int)($adv['silver'] ?? 0);
$hp = (int)($adv['hp'] ?? 0);
$wood = (int)($adv['wood'] ?? 0);
$clay = (int)($adv['clay'] ?? 0);
$iron = (int)($adv['iron'] ?? 0);
$crop = (int)($adv['crop'] ?? 0);
$itemId = (int)($adv['itemid'] ?? 0);
$itemQty = (int)($adv['itemqty'] ?? 0);
// Resolve the item name from the catalog (heroItemName falls back safely).
// berichte.php runs from the web root, so the relative path is stable.
if ($itemId > 0 && !function_exists('heroItemName')) {
include_once 'GameEngine/Data/hero_items.php';
}
$diffLabel = $difficulty === 1
? tz_hero_adv('HERO_ADV_HARD', 'a hard adventure')
: tz_hero_adv('HERO_ADV_NORMAL', 'a normal adventure');
?>
<table cellpadding="1" cellspacing="1" id="report_surround">
<thead>
<tr>
<th><?php echo defined('SUBJECT') ? SUBJECT : 'Subject'; ?>:</th>
<th><?php echo tz_loc_topic($message->readingNotice['topic']); ?></th>
</tr>
<tr>
<?php $date = $generator->procMtime($message->readingNotice['time']); ?>
<td class="sent"><?php echo defined('TZ_SENT') ? TZ_SENT : 'Sent'; ?></td>
<td><?php echo (defined('ON') ? ON : 'on') . ' '; ?><span><?php echo $date[0] . " " . $date[1]; ?></span></td>
</tr>
</thead>
<tbody>
<tr><td colspan="2" class="empty"></td></tr>
<tr><td colspan="2" class="report_content">
<?php if ($died) { ?>
<p><?php echo tz_hero_adv('HERO_ADV_DIED', 'Your hero fell on'); ?> <b><?php echo $diffLabel; ?></b>.</p>
<p><?php echo tz_hero_adv('HERO_ADV_DIED_INFO', 'All loot was lost. The hero can be revived at the Hero\'s Mansion.'); ?></p>
<?php } else { ?>
<p><?php echo tz_hero_adv('HERO_ADV_RETURNED', 'Your hero has returned from'); ?> <b><?php echo $diffLabel; ?></b>.</p>
<table class="adventureReward" cellpadding="1" cellspacing="1" style="width:100%;margin-top:8px;">
<tr>
<th style="text-align:left;"><?php echo tz_hero_adv('HERO_ADV_REWARD', 'Reward'); ?></th>
<th style="text-align:right;"><?php echo tz_hero_adv('HERO_ADV_AMOUNT', 'Amount'); ?></th>
</tr>
<tr>
<td><?php echo tz_hero_adv('HERO_EXPERIENCE', 'Experience'); ?></td>
<td style="text-align:right;">+<?php echo $exp; ?></td>
</tr>
<?php if ($silver > 0) { ?>
<tr>
<td><?php echo tz_hero_adv('HERO_SILVER', 'Silver'); ?></td>
<td style="text-align:right;">+<?php echo number_format($silver); ?></td>
</tr>
<?php } ?>
<?php if ($wood || $clay || $iron || $crop) { ?>
<tr>
<td><?php echo tz_hero_adv('RESOURCES', 'Resources'); ?></td>
<td style="text-align:right;">
<span class="res wood"><?php echo number_format($wood); ?></span>
<span class="res clay"><?php echo number_format($clay); ?></span>
<span class="res iron"><?php echo number_format($iron); ?></span>
<span class="res crop"><?php echo number_format($crop); ?></span>
</td>
</tr>
<?php } ?>
<?php if ($itemId > 0 && $itemQty > 0) { ?>
<tr>
<td><?php echo tz_hero_adv('HERO_ADV_ITEM_FOUND', 'Item found'); ?></td>
<td style="text-align:right;"><?php echo $itemQty; ?>&times; <?php echo heroItemName($itemId); ?></td>
</tr>
<?php } ?>
<?php if ($hp > 0) { ?>
<tr>
<td><?php echo tz_hero_adv('HERO_ADV_HP_LOST', 'Health lost'); ?></td>
<td style="text-align:right;color:#a00;">-<?php echo $hp; ?>%</td>
</tr>
<?php } ?>
</table>
<?php } // end survived branch ?>
</td></tr>
</tbody>
</table>
+95
View File
@@ -0,0 +1,95 @@
<?php
#################################################################################
# Hero auction report (27.tpl) - T4 hero port, Phase 4 #
# Handles ntype 27. Payload (ndata.data, query-string): #
# role=winner&itemid=71&qty=1&price=120&refund=30 #
# role=seller&itemid=71&qty=1&price=120&fee=12 #
# role=seller&expired=1&itemid=71&qty=1 #
# Same defensive-lang conventions as 26.tpl. #
#################################################################################
if (!function_exists('tz_hero_auc')) {
function tz_hero_auc($const, $fallback) {
return defined($const) ? constant($const) : $fallback;
}
}
parse_str((string)($message->readingNotice['data'] ?? ''), $auc);
$role = (string)($auc['role'] ?? '');
$expired = (int)($auc['expired'] ?? 0);
$itemId = (int)($auc['itemid'] ?? 0);
$qty = (int)($auc['qty'] ?? 0);
$price = (int)($auc['price'] ?? 0);
$refund = (int)($auc['refund'] ?? 0);
$fee = (int)($auc['fee'] ?? 0);
if ($itemId > 0 && !function_exists('heroItemName')) {
include_once 'GameEngine/Data/hero_items.php';
}
$itemLabel = $qty . '&times; ' . ($itemId > 0 ? heroItemName($itemId) : '?');
?>
<table cellpadding="1" cellspacing="1" id="report_surround">
<thead>
<tr>
<th><?php echo defined('SUBJECT') ? SUBJECT : 'Subject'; ?>:</th>
<th><?php echo tz_loc_topic($message->readingNotice['topic']); ?></th>
</tr>
<tr>
<?php $date = $generator->procMtime($message->readingNotice['time']); ?>
<td class="sent"><?php echo defined('TZ_SENT') ? TZ_SENT : 'Sent'; ?></td>
<td><?php echo (defined('ON') ? ON : 'on') . ' '; ?><span><?php echo $date[0] . " " . $date[1]; ?></span></td>
</tr>
</thead>
<tbody>
<tr><td colspan="2" class="empty"></td></tr>
<tr><td colspan="2" class="report_content">
<?php if ($role === 'winner') { ?>
<p><?php echo tz_hero_auc('HERO_AUC_WON', 'You won the auction for'); ?> <b><?php echo $itemLabel; ?></b>.</p>
<table class="auctionResult" cellpadding="1" cellspacing="1" style="width:100%;margin-top:8px;">
<tr>
<td><?php echo tz_hero_auc('HERO_AUC_PRICE', 'Final price'); ?></td>
<td style="text-align:right;"><?php echo number_format($price); ?> <?php echo tz_hero_auc('HERO_SILVER', 'silver'); ?></td>
</tr>
<?php if ($refund > 0) { ?>
<tr>
<td><?php echo tz_hero_auc('HERO_AUC_REFUND', 'Refunded from your maximum bid'); ?></td>
<td style="text-align:right;">+<?php echo number_format($refund); ?></td>
</tr>
<?php } ?>
</table>
<?php } elseif ($role === 'seller' && !$expired) { ?>
<p><?php echo tz_hero_auc('HERO_AUC_SOLD', 'Your auction sold:'); ?> <b><?php echo $itemLabel; ?></b>.</p>
<table class="auctionResult" cellpadding="1" cellspacing="1" style="width:100%;margin-top:8px;">
<tr>
<td><?php echo tz_hero_auc('HERO_AUC_PRICE', 'Final price'); ?></td>
<td style="text-align:right;"><?php echo number_format($price); ?> <?php echo tz_hero_auc('HERO_SILVER', 'silver'); ?></td>
</tr>
<tr>
<td><?php echo tz_hero_auc('HERO_AUC_FEE', 'Auction fee'); ?></td>
<td style="text-align:right;color:#a00;">-<?php echo number_format($fee); ?></td>
</tr>
<tr>
<td><b><?php echo tz_hero_auc('HERO_AUC_PAYOUT', 'Payout'); ?></b></td>
<td style="text-align:right;"><b>+<?php echo number_format($price - $fee); ?></b></td>
</tr>
</table>
<?php } else { ?>
<p><?php echo tz_hero_auc('HERO_AUC_EXPIRED', 'Your auction ended without bids. The item was returned to your inventory:'); ?> <b><?php echo $itemLabel; ?></b>.</p>
<?php } ?>
</td></tr>
</tbody>
</table>
+42
View File
@@ -112,6 +112,12 @@ $movement3_0 = $database->getMovement(3, $village->wid, 0);
$movement5_0 = $database->getMovement(5, $village->wid, 0);
$movement8_1 = $database->getMovement(8, $village->wid, 1);
$movement9_0 = $database->getMovement(9, $village->wid, 0);
// T4 hero port: hero adventure movements are only relevant for the hero's
// home village and only when the feature is on.
$movement20_0 = (defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4)
? $database->getMovement(20, $village->wid, 0) : [];
$movement21_1 = (defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4)
? $database->getMovement(21, $village->wid, 1) : [];
/**
* ---------------------------------------------------------
@@ -285,6 +291,42 @@ renderMovementRow(
$session
);
/**
* =========================================================
* HERO ADVENTURE (T4 hero port): outbound + returning
* =========================================================
*/
$advOutArrival = array();
foreach ($movement20_0 as $receive) {
$advOutArrival[] = $receive['endtime'];
}
renderMovementRow(
'att2',
'a2',
defined('HERO_ADV_MOV_OUT') ? HERO_ADV_MOV_OUT : 'Hero on an adventure',
defined('HERO_ADV_MOV_SHORT') ? HERO_ADV_MOV_SHORT : 'Adventure',
count($movement20_0),
!empty($advOutArrival) ? min($advOutArrival) : 0,
$generator,
$session
);
$advBackArrival = array();
foreach ($movement21_1 as $receive) {
$advBackArrival[] = $receive['endtime'];
}
renderMovementRow(
'def1',
'd1',
defined('HERO_ADV_MOV_BACK') ? HERO_ADV_MOV_BACK : 'Hero returning from an adventure',
defined('HERO_ADV_MOV_SHORT') ? HERO_ADV_MOV_SHORT : 'Adventure',
count($movement21_1),
!empty($advBackArrival) ? min($advBackArrival) : 0,
$generator,
$session
);
/**
* =========================================================
* FOUNDING NEW VILLAGE
+15 -9
View File
@@ -3,15 +3,21 @@
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Project: TravianZ ##
## Filename activate.php ##
## Developed by: Dixie ##
## Developed by: Dzoki ##
## License: TravianZ Project ##
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
## URLs: http://travian.shadowss.ro ##
## Source code: https://github.com/Shadowss/TravianZ ##
## ##
## Filename : activate.php ##
## Type : In Game Activate File ##
## --------------------------------------------------------------------------- ##
## Developed by : Dzoki & Advocaite & Donnchadh ##
## Refactored by : Shadow ##
## Redesign by : Shadow ##
## --------------------------------------------------------------------------- ##
## Contact : cata7007@gmail.com ##
## Project : TravianZ ##
## URLs: : https://travianz.org ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
use App\Utils\AccessLogger;
+16 -7
View File
@@ -1,14 +1,23 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename ajax.php ##
## Developed by: Dzoki ##
## License: TravianZ Project ##
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
## URLs: http://travian.shadowss.ro ##
## Source code: https://github.com/Shadowss/TravianZ ##
## ##
## Filename : ajax.php ##
## Type : In Game Ajax File ##
## --------------------------------------------------------------------------- ##
## Developed by : Dzoki & Advocaite & Donnchadh ##
## Refactored by : Shadow ##
## Redesign by : Shadow ##
## --------------------------------------------------------------------------- ##
## Contact : cata7007@gmail.com ##
## Project : TravianZ ##
## URLs: : https://travianz.org ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
// even with autoloader created, we can't use it here yet, as it's not been created
+16 -8
View File
@@ -1,15 +1,23 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Project: TravianZ ##
## Filename anleitung.php ##
## Developed by: Dzoki ##
## License: TravianZ Project ##
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
## URLs: http://travian.shadowss.ro ##
## Source code: https://github.com/Shadowss/TravianZ ##
## ##
## Filename : anleitung.php ##
## Type : In Game Part of Main Page ##
## --------------------------------------------------------------------------- ##
## Developed by : Dzoki & Advocaite & Donnchadh ##
## Refactored by : Shadow ##
## Redesign by : Shadow ##
## --------------------------------------------------------------------------- ##
## Contact : cata7007@gmail.com ##
## Project : TravianZ ##
## URLs: : https://travianz.org ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
use App\Utils\AccessLogger;
+1 -1
View File
@@ -166,7 +166,7 @@ $worldLabel = worldSizeLabel();
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title><?php echo SERVER_NAME ?> - Mass Message</title>
<title><?php echo SERVER_NAME ?> - Build Croppers</title>
<link rel="shortcut icon" href="favicon.ico"/>
<meta http-equiv="cache-control" content="max-age=0" />
<meta http-equiv="pragma" content="no-cache" />
+116
View File
@@ -0,0 +1,116 @@
/* AUTO-GENERATED from GameEngine/Data/hero_items.php - regenerate via var/tools/generate_hero_sprites.php
Placeholder art: replace img/hero/items.png with real 48x48 sprites on the SAME grid
(16 columns, catalog order) and this CSS keeps working unchanged. */
.heroT4Item{display:inline-block;width:48px;height:48px;vertical-align:middle;
background-image:url('../img/hero/items.png');background-repeat:no-repeat;}
.heroT4Item.item1{background-position:-0px -0px;} /* Helmet of Awareness */
.heroT4Item.item2{background-position:-48px -0px;} /* Helmet of Enlightenment */
.heroT4Item.item3{background-position:-96px -0px;} /* Helmet of Wisdom */
.heroT4Item.item4{background-position:-144px -0px;} /* Helmet of Regeneration */
.heroT4Item.item5{background-position:-192px -0px;} /* Helmet of Health */
.heroT4Item.item6{background-position:-240px -0px;} /* Helmet of Healing */
.heroT4Item.item7{background-position:-288px -0px;} /* Helmet of the Gladiator */
.heroT4Item.item8{background-position:-336px -0px;} /* Helmet of the Tribune */
.heroT4Item.item9{background-position:-384px -0px;} /* Helmet of the Consul */
.heroT4Item.item10{background-position:-432px -0px;} /* Helmet of the Horseman */
.heroT4Item.item11{background-position:-480px -0px;} /* Helmet of the Cavalry */
.heroT4Item.item12{background-position:-528px -0px;} /* Helmet of the Heavy Cavalry */
.heroT4Item.item13{background-position:-576px -0px;} /* Helmet of the Mercenary */
.heroT4Item.item14{background-position:-624px -0px;} /* Helmet of the Warrior */
.heroT4Item.item15{background-position:-672px -0px;} /* Helmet of the Ruler */
.heroT4Item.item20{background-position:-720px -0px;} /* Light Breastplate of Regeneration */
.heroT4Item.item21{background-position:-0px -48px;} /* Breastplate of Regeneration */
.heroT4Item.item22{background-position:-48px -48px;} /* Heavy Breastplate of Regeneration */
.heroT4Item.item23{background-position:-96px -48px;} /* Light Armor */
.heroT4Item.item24{background-position:-144px -48px;} /* Armor */
.heroT4Item.item25{background-position:-192px -48px;} /* Heavy Armor */
.heroT4Item.item26{background-position:-240px -48px;} /* Light Cuirass */
.heroT4Item.item27{background-position:-288px -48px;} /* Cuirass */
.heroT4Item.item28{background-position:-336px -48px;} /* Heavy Cuirass */
.heroT4Item.item29{background-position:-384px -48px;} /* Light Segmented Armor */
.heroT4Item.item30{background-position:-432px -48px;} /* Segmented Armor */
.heroT4Item.item31{background-position:-480px -48px;} /* Heavy Segmented Armor */
.heroT4Item.item40{background-position:-528px -48px;} /* Small Shield */
.heroT4Item.item41{background-position:-576px -48px;} /* Shield */
.heroT4Item.item42{background-position:-624px -48px;} /* Great Shield */
.heroT4Item.item43{background-position:-672px -48px;} /* Small Hunting Horn */
.heroT4Item.item44{background-position:-720px -48px;} /* Hunting Horn */
.heroT4Item.item45{background-position:-0px -96px;} /* Great Hunting Horn */
.heroT4Item.item46{background-position:-48px -96px;} /* Thief's Satchel */
.heroT4Item.item47{background-position:-96px -96px;} /* Thief's Bag */
.heroT4Item.item48{background-position:-144px -96px;} /* Thief's Sack */
.heroT4Item.item49{background-position:-192px -96px;} /* Small Map */
.heroT4Item.item50{background-position:-240px -96px;} /* Map */
.heroT4Item.item51{background-position:-288px -96px;} /* Great Map */
.heroT4Item.item52{background-position:-336px -96px;} /* Small Pennant */
.heroT4Item.item53{background-position:-384px -96px;} /* Pennant */
.heroT4Item.item54{background-position:-432px -96px;} /* Great Pennant */
.heroT4Item.item55{background-position:-480px -96px;} /* Small Standard */
.heroT4Item.item56{background-position:-528px -96px;} /* Standard */
.heroT4Item.item57{background-position:-576px -96px;} /* Great Standard */
.heroT4Item.item60{background-position:-624px -96px;} /* Boots of Regeneration */
.heroT4Item.item61{background-position:-672px -96px;} /* Boots of Recovery */
.heroT4Item.item62{background-position:-720px -96px;} /* Boots of Healing */
.heroT4Item.item63{background-position:-0px -144px;} /* Boots of the Mercenary */
.heroT4Item.item64{background-position:-48px -144px;} /* Boots of the Warrior */
.heroT4Item.item65{background-position:-96px -144px;} /* Boots of the Ruler */
.heroT4Item.item66{background-position:-144px -144px;} /* Small Spurs */
.heroT4Item.item67{background-position:-192px -144px;} /* Spurs */
.heroT4Item.item68{background-position:-240px -144px;} /* Great Spurs */
.heroT4Item.item70{background-position:-288px -144px;} /* Riding Horse */
.heroT4Item.item71{background-position:-336px -144px;} /* Thoroughbred */
.heroT4Item.item72{background-position:-384px -144px;} /* Warhorse */
.heroT4Item.item101{background-position:-432px -144px;} /* Legionnaire's Short Sword */
.heroT4Item.item102{background-position:-480px -144px;} /* Legionnaire's Sword */
.heroT4Item.item103{background-position:-528px -144px;} /* Legionnaire's Long Sword */
.heroT4Item.item104{background-position:-576px -144px;} /* Praetorian's Short Sword */
.heroT4Item.item105{background-position:-624px -144px;} /* Praetorian's Sword */
.heroT4Item.item106{background-position:-672px -144px;} /* Praetorian's Long Sword */
.heroT4Item.item107{background-position:-720px -144px;} /* Imperian's Short Sword */
.heroT4Item.item108{background-position:-0px -192px;} /* Imperian's Sword */
.heroT4Item.item109{background-position:-48px -192px;} /* Imperian's Long Sword */
.heroT4Item.item110{background-position:-96px -192px;} /* Imperatoris' Short Saber */
.heroT4Item.item111{background-position:-144px -192px;} /* Imperatoris' Saber */
.heroT4Item.item112{background-position:-192px -192px;} /* Imperatoris' Long Saber */
.heroT4Item.item113{background-position:-240px -192px;} /* Caesaris' Light Lance */
.heroT4Item.item114{background-position:-288px -192px;} /* Caesaris' Lance */
.heroT4Item.item115{background-position:-336px -192px;} /* Caesaris' Heavy Lance */
.heroT4Item.item116{background-position:-384px -192px;} /* Clubswinger's Cudgel */
.heroT4Item.item117{background-position:-432px -192px;} /* Clubswinger's Club */
.heroT4Item.item118{background-position:-480px -192px;} /* Clubswinger's Mace */
.heroT4Item.item119{background-position:-528px -192px;} /* Spearman's Pitchfork */
.heroT4Item.item120{background-position:-576px -192px;} /* Spearman's Pike */
.heroT4Item.item121{background-position:-624px -192px;} /* Spearman's Spear */
.heroT4Item.item122{background-position:-672px -192px;} /* Axeman's Hatchet */
.heroT4Item.item123{background-position:-720px -192px;} /* Axeman's Axe */
.heroT4Item.item124{background-position:-0px -240px;} /* Axeman's Battle Axe */
.heroT4Item.item125{background-position:-48px -240px;} /* Paladin's Small Hammer */
.heroT4Item.item126{background-position:-96px -240px;} /* Paladin's Hammer */
.heroT4Item.item127{background-position:-144px -240px;} /* Paladin's Sledgehammer */
.heroT4Item.item128{background-position:-192px -240px;} /* Teutonic Knight's Short Sword */
.heroT4Item.item129{background-position:-240px -240px;} /* Teutonic Knight's Sword */
.heroT4Item.item130{background-position:-288px -240px;} /* Teutonic Knight's Long Sword */
.heroT4Item.item131{background-position:-336px -240px;} /* Phalanx's Pitchfork */
.heroT4Item.item132{background-position:-384px -240px;} /* Phalanx's Spear */
.heroT4Item.item133{background-position:-432px -240px;} /* Phalanx's Lance */
.heroT4Item.item134{background-position:-480px -240px;} /* Swordsman's Short Sword */
.heroT4Item.item135{background-position:-528px -240px;} /* Swordsman's Sword */
.heroT4Item.item136{background-position:-576px -240px;} /* Swordsman's Long Sword */
.heroT4Item.item137{background-position:-624px -240px;} /* Thunder's Short Bow */
.heroT4Item.item138{background-position:-672px -240px;} /* Thunder's Bow */
.heroT4Item.item139{background-position:-720px -240px;} /* Thunder's Long Bow */
.heroT4Item.item140{background-position:-0px -288px;} /* Druidrider's Baton */
.heroT4Item.item141{background-position:-48px -288px;} /* Druidrider's Staff */
.heroT4Item.item142{background-position:-96px -288px;} /* Druidrider's Great Staff */
.heroT4Item.item143{background-position:-144px -288px;} /* Haeduan's Light Lance */
.heroT4Item.item144{background-position:-192px -288px;} /* Haeduan's Lance */
.heroT4Item.item145{background-position:-240px -288px;} /* Haeduan's Heavy Lance */
.heroT4Item.item200{background-position:-288px -288px;} /* Ointment */
.heroT4Item.item201{background-position:-336px -288px;} /* Scroll */
.heroT4Item.item202{background-position:-384px -288px;} /* Bucket of Water */
.heroT4Item.item203{background-position:-432px -288px;} /* Law Tablet */
.heroT4Item.item204{background-position:-480px -288px;} /* Book of Wisdom */
.heroT4Item.item205{background-position:-528px -288px;} /* Artwork */
.heroT4Item.item206{background-position:-576px -288px;} /* Small Bandage */
.heroT4Item.item207{background-position:-624px -288px;} /* Bandage */
.heroT4Item.item208{background-position:-672px -288px;} /* Cage */
Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

+199
View File
@@ -0,0 +1,199 @@
{
"helmet 1.gif": [
1,
2,
3
],
"helmet 2.gif": [
4,
5,
6
],
"helmet 3.gif": [
7,
8,
9
],
"helmet 4.gif": [
10,
11,
12
],
"helmet 5.gif": [
13,
14,
15
],
"armour 1.gif": [
20,
21,
22
],
"armour 2.gif": [
23,
24,
25
],
"armour 3.gif": [
26,
27,
28
],
"armour 4.gif": [
29,
30,
31
],
"scout.gif": [
40,
41,
42
],
"horn.gif": [
43,
44,
45
],
"bag.gif": [
46,
47,
48
],
"map.gif": [
49,
50,
51
],
"item64.gif": [
52,
53,
54
],
"item67.gif": [
55,
56,
57
],
"shoes 1.gif": [
60,
61,
62
],
"shoes 2.gif": [
63,
64,
65
],
"shoes 3.gif": [
66,
67,
68
],
"horse.gif": [
70,
71,
72
],
"sabie 1 roman legionar.gif": [
101,
102,
103
],
"sabie 2 roman praetorian.gif": [
104,
105,
106
],
"sabie 3 roman imperian.gif": [
107,
108,
109
],
"sabie 4 romani imperatoris.gif": [
110,
111,
112
],
"sabie 4 romani caesaris.gif": [
113,
114,
115
],
"sabie 1 teutoni clubswinger.gif": [
116,
117,
118
],
"sabie 2 teutoni spearman.gif": [
119,
120,
121
],
"sabie 2 teutoni axeman.gif": [
122,
123,
124
],
"Paladin teutons.gif": [
125,
126,
127
],
"Teutonic Knight teutons.gif": [
128,
129,
130
],
"Phalanx gauls.gif": [
131,
132,
133
],
"Swordsman gauls.gif": [
134,
135,
136
],
"Theutates Thunder gauls.gif": [
137,
138,
139
],
"Druidrider gauls.gif": [
140,
141,
142
],
"Haeduan teutons.gif": [
143,
144,
145
],
"ointment.gif": [
200
],
"scroll.gif": [
201
],
"water.gif": [
202
],
"tablet.gif": [
203
],
"book.gif": [
204
],
"artwork.gif": [
205
],
"bandage25.gif": [
206
],
"bandage33.gif": [
207
],
"cage.gif": [
208
]
}
+1
View File
@@ -335,6 +335,7 @@ define("NEW_FUNCTIONS_MEDAL_10YEAR", %NEW_FUNCTIONS_MEDAL_10YEAR%);
define("NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM", %NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM%);
define("NEW_FUNCTIONS_MILESTONES", %NEW_FUNCTIONS_MILESTONES%);
define("NEW_FUNCTIONS_MEDAL_RESET", %NEW_FUNCTIONS_MEDAL_RESET%);
define("NEW_FUNCTIONS_HERO_T4", %NEW_FUNCTIONS_HERO_T4%);
//////////////////////////////////////////
// **** DO NOT EDIT SETTINGS **** //
+1
View File
@@ -171,6 +171,7 @@ class Process {
$findReplace["%NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM%"] = $_POST['new_functions_special_medals_system'];
$findReplace["%NEW_FUNCTIONS_MILESTONES%"] = $_POST['new_functions_milestones'];
$findReplace["%NEW_FUNCTIONS_MEDAL_RESET%"] = $_POST['new_functions_medal_reset'];
$findReplace["%NEW_FUNCTIONS_HERO_T4%"] = $_POST['new_functions_hero_t4'];
fwrite($gameConfig, str_replace(array_keys($findReplace), array_values($findReplace), $text));
+2 -1
View File
@@ -207,7 +207,8 @@ $mechs = [
'new_functions_medal_10year' => 'Medal 10y',
'new_functions_special_medals_system' => 'Special Medals System',
'new_functions_medal_reset' => 'Medal Reset',
'new_functions_milestones' => 'Server Milestones'
'new_functions_milestones' => 'Server Milestones',
'new_functions_hero_t4' => 'T4 Hero (items, adventures, auction)'
];
foreach($mechs as $k => $l){
+72
View File
@@ -951,6 +951,8 @@ CREATE TABLE IF NOT EXISTS `%PREFIX%hero` (
`level` tinyint(3) NULL,
`points` int(3) NULL,
`experience` int(11) NULL,
`silver` int(11) NOT NULL DEFAULT 0,
`production` tinyint(1) NOT NULL DEFAULT 0,
`dead` tinyint(1) NULL,
`health` float(12,9) NULL,
`attack` tinyint(3) NULL,
@@ -976,6 +978,76 @@ CREATE TABLE IF NOT EXISTS `%PREFIX%hero` (
-- --------------------------------------------------------
--
-- Table structure for table `%prefix%hero_items` (T4 hero port)
--
CREATE TABLE IF NOT EXISTS `%PREFIX%hero_items` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) NOT NULL,
`heroid` int(11) NOT NULL DEFAULT 0,
`itemid` smallint(5) NOT NULL,
`slot` tinyint(2) NOT NULL DEFAULT 0,
`stat_value` int(11) NOT NULL DEFAULT 0,
`quantity` int(11) NOT NULL DEFAULT 1,
`equipped` tinyint(1) NOT NULL DEFAULT 0,
`tstamp` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `uid_equipped` (`uid`,`equipped`),
KEY `uid_slot` (`uid`,`slot`),
KEY `itemid` (`itemid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- --------------------------------------------------------
--
-- Table structure for table `%prefix%hero_adventure` (T4 hero port)
--
CREATE TABLE IF NOT EXISTS `%PREFIX%hero_adventure` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) NOT NULL,
`wref` int(11) NOT NULL DEFAULT 0,
`difficulty` tinyint(1) NOT NULL DEFAULT 0,
`duration` int(11) NOT NULL DEFAULT 0,
`created` int(11) NOT NULL DEFAULT 0,
`expire` int(11) NOT NULL DEFAULT 0,
`status` tinyint(1) NOT NULL DEFAULT 0,
`moveid` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `uid_status` (`uid`,`status`),
KEY `expire` (`expire`),
KEY `moveid` (`moveid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- --------------------------------------------------------
--
-- Table structure for table `%prefix%auction` (T4 hero port)
--
CREATE TABLE IF NOT EXISTS `%PREFIX%auction` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`seller` int(11) NOT NULL DEFAULT 0,
`itemid` smallint(5) NOT NULL,
`slot` tinyint(2) NOT NULL DEFAULT 0,
`stat_value` int(11) NOT NULL DEFAULT 0,
`quantity` int(11) NOT NULL DEFAULT 1,
`silver_start` int(11) NOT NULL DEFAULT 0,
`silver_current` int(11) NOT NULL DEFAULT 0,
`bidder` int(11) NOT NULL DEFAULT 0,
`bid_max` int(11) NOT NULL DEFAULT 0,
`created` int(11) NOT NULL DEFAULT 0,
`time_end` int(11) NOT NULL DEFAULT 0,
`status` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `status_end` (`status`,`time_end`),
KEY `bidder` (`bidder`),
KEY `seller` (`seller`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- --------------------------------------------------------
--
+27
View File
@@ -0,0 +1,27 @@
<?php
#################################################################################
# Regenerates css/hero_items.css from GameEngine/Data/hero_items.php so the #
# sprite grid can never drift from the catalog. Run from the repo root: #
# php var/tools/generate_hero_sprites.php #
# Grid contract: img/hero/items.png is a 16-column sheet of 32x32 cells in #
# CATALOG ORDER. Replace the PNG with real art on the same grid and rerun #
# nothing - only rerun this script when the CATALOG changes. #
#################################################################################
define('TB_PREFIX', 'x');
include __DIR__ . '/../../GameEngine/Data/hero_items.php';
$cell = 48; $cols = 16; $idx = 0;
$css = "/* AUTO-GENERATED from GameEngine/Data/hero_items.php - regenerate via var/tools/generate_hero_sprites.php\n"
. " Placeholder art: replace img/hero/items.png with real 48x48 sprites on the SAME grid\n"
. " (16 columns, catalog order) and this CSS keeps working unchanged. */\n"
. ".heroT4Item{display:inline-block;width:48px;height:48px;vertical-align:middle;\n"
. " background-image:url('../img/hero/items.png');background-repeat:no-repeat;}\n";
foreach ($heroItemCatalog as $iid => $def) {
$x = ($idx % $cols) * $cell;
$y = intdiv($idx, $cols) * $cell;
$css .= ".heroT4Item.item{$iid}{background-position:-{$x}px -{$y}px;} /* {$def['name']} */\n";
$idx++;
}
file_put_contents(__DIR__ . '/../../css/hero_items.css', $css);
echo "css/hero_items.css written: $idx items\n";