mirror of
https://github.com/Shadowss/TravianZ.git
synced 2026-07-10 14:46:09 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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){
|
||||
|
||||
Reference in New Issue
Block a user