<?php

#################################################################################
##              -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =-                 ##
## --------------------------------------------------------------------------- ##
##  Filename       : Battle.php                      	                       ##
##  Type           : Battle System Backend                                     ##
## --------------------------------------------------------------------------- ##
##  Developed by   : Dzoki & Dixie  			                               ##
##  Refactored by  : Shadow & Ferywir									       ##
##  Thanks to      : ronix, InCube, Akakori, Elmar & Kirilloid                 ##
## --------------------------------------------------------------------------- ##
##  Contact        : cata7007@gmail.com                                        ##
##  Project        : TravianZ                                                  ##
##  URLs:          : https://travianz.org                                      ##
##  GitHub         : https://github.com/Shadowss/TravianZ                      ##
## --------------------------------------------------------------------------- ##
##  License        : TravianZ Project                                          ##
##  Copyright      : TravianZ (c) 2010-2026. All rights reserved.              ##
## --------------------------------------------------------------------------- ##
#################################################################################


class Battle {

	/*****************************************
	Function sigma
	*****************************************/
    
	private function sigma($x) {
    return ($x > 1 ? 2 - pow($x, -1.5) : pow($x, 1.5)) / 2;
	}
		
	/*****************************************
	Function to proc Simulation
	*****************************************/
        
	public function procSim($post) {
    global $form;

    /******************************************************************
     * BASIC VALIDATION
     ******************************************************************/
    if (
        empty($post['a1_v']) ||
        !(
            isset($post['a2_v1']) ||
            isset($post['a2_v2']) ||
            isset($post['a2_v3']) ||
            isset($post['a2_v4']) ||
            isset($post['a2_v5']) ||
            isset($post['a2_v6']) ||
            isset($post['a2_v7']) ||
            isset($post['a2_v8']) ||
            isset($post['a2_v9'])
        )
    ) {
        return;
    }

    // [Phase 3] a1_v intra in aritmetica de sloturi ((trib-1)*10+1) din simulate();
    // in afara 1..9 producea chei u<negative>/inexistente. Invalid => iesire,
    // exact ca celelalte validari de mai sus. (Schimbare de comportament DOAR
    // pentru input invalid.)
    if ((int)$post['a1_v'] < 1 || (int)$post['a1_v'] > 9) {
        return;
    }

    $_POST['mytribe'] = $post['a1_v'];

    /******************************************************************
     * TARGET BUILD
     ******************************************************************/
    $target = [];
    for ($i = 1; $i <= 9; $i++) {
        if (!empty($post['a2_v'.$i])) {
            $target[] = $i;
        }
    }
    $_POST['target'] = $target;
    if (empty($target)) {
        return;
    }

    /******************************************************************
     * HERO BONUS LIMITS (OFF / DEF)
     ******************************************************************/
    $post['h_off_bonus'] = isset($post['h_off_bonus'])
        ? min(20, (int)$post['h_off_bonus'])
        : 0;
    $post['h_def_bonus'] = isset($post['h_def_bonus'])
        ? min(20, (int)$post['h_def_bonus'])
        : 0;

    /******************************************************************
     * UNIT SUM CHECK
     ******************************************************************/
    $sum = 0;
    for ($i = 1; $i <= 10; $i++) {
        $sum += isset($post['a1_'.$i]) ? (int)$post['a1_'.$i] : 0;
    }
    if ($sum <= 0) {
        return;
    }

    /******************************************************************
     * PALACE LIMIT
     ******************************************************************/
    $post['palast'] = isset($post['palast'])
        ? min(20, (int)$post['palast'])
        : 0;

    /******************************************************************
     * WALL LEVELS (OPTIMIZED LOOP)
     ******************************************************************/
    $post['walllevel'] = 0;
    for ($i = 1; $i <= 9; $i++) {
        if (!isset($post['wall'.$i])) {
            continue;
        }
        $wall = (int)$post['wall'.$i];
        if ($wall > 20) {
            $wall = 20;
        } elseif ($wall < 0) {
            $wall = 0;
        }
        $post['wall'.$i] = $wall;
        $post['walllevel'] = $wall;
    }

    /******************************************************************
     * SIMULATION CALL
     ******************************************************************/
    $post['tribe'] = $target[0];
    $_POST['result'] = $this->simulate($post);
	$newWallLevel = $_POST['result'][7]?? 0;
	$oldWallLevel = $_POST['result'][8]?? 0;

    /******************************************************************
     * WALL CHANGE RE-SIMULATION
     ******************************************************************/
    if ($newWallLevel != $oldWallLevel) {
        $post['walllevel'] = $newWallLevel;
        $_POST['result'] = $this->simulate($post);
		
        // restore original expected output
		
        $_POST['result'][7] = $newWallLevel;
        $_POST['result'][8] = $oldWallLevel;
        $post['walllevel'] = $oldWallLevel;
    }
    $form->valuearray = $post;
}
		
	/*****************************************
	Function to battle Hero
	*****************************************/
		
	private function getBattleHero($uid) {

    global $database;
    $heroarray = $database->getHero($uid);
    if (empty($heroarray) || empty($heroarray[0])) {
        // ob/db sunt MULTIPLICATORI: 0 ar anula toata puterea armatei ($ap *= ob).
        // Un erou lipsa/invalid trebuie sa fie neutru, nu catastrofal.
        return [
            'heroid' => 0,
            'unit'   => '',
            'atk'    => 0,
            'di'     => 0,
            'dc'     => 0,
            'ob'     => 1,
            'db'     => 1,
            'health' => 0
        ];
    }

    $hero = $heroarray[0];
    $heroUnit = $hero['unit'];
    if (!isset($GLOBALS['h'.$heroUnit])) {

        // ob/db sunt MULTIPLICATORI: 0 ar anula toata puterea armatei ($ap *= ob).
        // Un erou lipsa/invalid trebuie sa fie neutru, nu catastrofal.
        return [
            'heroid' => 0,
            'unit'   => '',
            'atk'    => 0,
            'di'     => 0,
            'dc'     => 0,
            'ob'     => 1,
            'db'     => 1,
            'health' => 0
        ];
    }

    $herodata = $GLOBALS['h'.$heroUnit];
    $attack = (int)$hero['attack'];
    $defence = (int)$hero['defence'];
    $attackBonus = (int)$hero['attackbonus'];
    $defenceBonus = (int)$hero['defencebonus'];
    $h_atk = $herodata['atk']
        + ($attack * $herodata['atkp']);
    $h_di = $herodata['di']
        + 5 * floor(($defence * $herodata['dip']) / 5);
    $h_dc = $herodata['dc']
        + 5 * floor(($defence * $herodata['dcp']) / 5);
    $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,
        'atk'    => $h_atk,
        'di'     => $h_di,
        '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
    ];
}

	/*****************************************
	Function to battle Hero Sim
	*****************************************/

	private function getBattleHeroSim($attbonus) {
		
    $h_ob = 1 + (0.010 * (float)$attbonus);
    return [
        'unit' => 16,
        'atk'  => 0,
        'ob'   => $h_ob
    ];
}

	/*****************************************
	Function to Simulation
	*****************************************/

	private function simulate($post) {

    /******************************************************************
     * INPUT PARSING (Phase 3)
     * Toate cheile din $post se citesc O SINGURA DATA aici, cu ?? si
     * cast (int) + clamp de interval. Pentru valori VALIDE rezultatul
     * este identic cu originalul (verificat cu harness-ul de
     * echivalenta); clamp-urile schimba comportamentul doar pe input
     * invalid - fiecare e semnalat individual mai jos.
     ******************************************************************/
    $attTribe = (int)$post['a1_v'];   // validat 1..9 in procSim()

    // [Phase 3] era (int)$post['h_off'], fara ?? (notice pe cheie lipsa)
    // si fara podea; forta negativa de erou n-are sens
    $hero_strenght = max(0, (int)($post['h_off'] ?? 0));

    // [Phase 3] procSim() taia doar plafonul (min 20); adaugata si podeaua 0
    $offhero  = max(0, min(20, (int)($post['h_off_bonus'] ?? 0)));
    $deffhero = max(0, min(20, (int)($post['h_def_bonus'] ?? 0)));
    $palast   = max(0, min(20, (int)($post['palast'] ?? 0)));

    // procSim() garanteaza deja 0..20 pe fluxul normal; clamp-ul e doar plasa
    $walllevel = max(0, min(20, (int)($post['walllevel'] ?? 0)));
    $wall      = $walllevel;

    $deftribe = (int)($post['tribe'] ?? 0);   // setat de procSim() = $target[0]

    // [Phase 3] era !empty($post['kata']) ? (int)$post['kata'] : 0, fara plafon;
    // e nivelul cladirii-tinta pentru catapulte (WW poate ajunge la 100)
    $kata = max(0, min(100, (int)($post['kata'] ?? 0)));

    // [Phase 3] era $post['stonemason'] pasat brut; nivelul valid e 0..20
    // (indexeaza $bid34; peste 20 dadea undefined index + durability nula)
    $stonemason = max(0, min(20, (int)($post['stonemason'] ?? 0)));

    // [Phase 3] erau pasate brute ca attpop/defpop; negative n-au sens in morala
    $ew1 = max(0, (int)($post['ew1'] ?? 0));
    $ew2 = max(0, (int)($post['ew2'] ?? 0));

    // [Phase 3] era $post['ktyp'] + 3 brut; doar 0 (=> tip 3, atac normal)
    // sau 1 (=> tip 4, raid) sunt valori valide
    $ktyp = ((int)($post['ktyp'] ?? 0) === 1) ? 1 : 0;

    /******************************************************************
     * ATTACKER INIT (KEEP LEGACY STRUCTURE)
     ******************************************************************/
    $attacker = [];

    for ($i = 1; $i <= 90; $i++) {
        $attacker['u'.$i] = 0;
    }

    $start = ($attTribe - 1) * 10 + 1;

    /******************************************************************
     * ATTACKER UNITS + ATTACK BONUSES
     * [Phase 3] ${'att_ab'.$index} (variabile dinamice) -> array $att_ab,
     * conform conventiei proiectului; valorile raman identice
     ******************************************************************/
    $att_ab = array_fill(1, 8, 0);

    for ($i = $start, $index = 1; $i <= $start + 9; $i++, $index++) {

        $attacker['u'.$i] = !empty($post['a1_'.$index])
            ? (int)$post['a1_'.$index]
            : 0;

        if ($index <= 8 && !empty($post['f1_'.$index])) {
            $att_ab[$index] = (int)$post['f1_'.$index];
        }
    }

    /******************************************************************
     * DEFENDER INIT
     ******************************************************************/
    $defender = [];
    $def_ab   = [];
    $defscout = 0;

    for ($i = 1; $i <= 90; $i++) {
        $units = (int)($post['a2_'.$i] ?? 0);
        $ab    = (int)($post['f2_'.$i] ?? 0);

        $defender['u'.$i] = $units;
        $def_ab[$i]       = $units > 0 ? $ab : 0;

        if ($units > 0 && in_array($i, [4, 14, 23, 44, 52, 64, 74, 82])) {
            $defscout += $units;
        }
    }

    /******************************************************************
     * SCOUT CHECK 
     ******************************************************************/
    $scout = 1;

    for ($i = $start; $i <= $start + 9; $i++) {

        if (in_array($i, [4, 14, 23, 44, 52, 64, 74, 82])) {
            continue;
        }

        if ($attacker['u'.$i] > 0) {
            $scout = 0;
            break;
        }
    }

    /******************************************************************
     * WALL / PALAST RULES 
     ******************************************************************/
    if ($scout == 1 && $defscout == 0) {
        $walllevel = 0;
        $wall = 0;
        $palast = 0;
    }

    if ($scout == 1) {
        $palast = 0;
    }

    /******************************************************************
     * FINAL CALL
     * [Phase 3] cele doua apeluri aproape identice au fost unificate;
     * difereau doar prin tip (1 vs ktyp+3) si prin zero-urile de erou
     * de pe ramura de spionaj
     ******************************************************************/
    return $this->calculateBattle(
        $attacker,
        $defender,
        $wall,
        $attTribe,
        $deftribe,
        $palast,
        $ew1,
        $ew2,
        $scout ? 1 : $ktyp + 3,
        $def_ab,
        $att_ab[1], $att_ab[2], $att_ab[3], $att_ab[4],
        $att_ab[5], $att_ab[6], $att_ab[7], $att_ab[8],
        $kata,
        $stonemason,
        $walllevel,
        $scout ? 0 : $offhero,
        $scout ? 0 : $hero_strenght,
        $scout ? 0 : $deffhero,
        0, 0, 0, 0, 0
    );
}
		
	/*****************************************
	Function for Type Level
	*****************************************/

	public function getTypeLevel($tid, $vid) {

    global $village, $database;
    $resourcearray = $database->getResourceLevel($vid);
    if (empty($resourcearray)) {
        return 0;
    }
    $keyholder = [];

    /******************************************************************
     * FIND BUILDINGS OF REQUESTED TYPE
     ******************************************************************/
    foreach ($resourcearray as $key => $value) {

        // The building TYPE is stored in the 'f<n>t' columns of fdata (e.g. the
        // Brewery on slot 20 is 'f20t' == 35); the matching level lives in 'f<n>'.
        // Match on the 't' SUFFIX, not the first character - a previous rewrite
        // tested $key[0] === 't', which never matched 'f<n>t' and made this
        // return 0 for every building (issue #294: Brewery bonus never applied).
        // Mirrors Building::getTypeLevel()/Automation::getTypeLevel().
        if (
            isset($value)
            && $value == $tid
            && strpos($key, 't') !== false
        ) {

            $keyholder[] = (int)preg_replace('/[^0-9]/', '', $key);
        }
    }
    $element = count($keyholder);
    if ($element === 0) {
        return 0;
    }
    if ($element === 1) {
        $field = 'f'.$keyholder[0];
        return isset($resourcearray[$field])
            ? (int)$resourcearray[$field]
            : 0;
    }

    /******************************************************************
     * FIND HIGHEST LEVEL
     ******************************************************************/
    $targetKey = $keyholder[0];
    $targetLevel = isset($resourcearray['f'.$targetKey])
        ? (int)$resourcearray['f'.$targetKey]
        : 0;

    // preserve original behavior separation for resource fields
    if ($tid <= 4) {
        foreach ($keyholder as $fieldId) {
            $fieldLevel = isset($resourcearray['f'.$fieldId])
                ? (int)$resourcearray['f'.$fieldId]
                : 0;

            // preserve original "last max wins" behavior
            if ($fieldLevel >= $targetLevel) {
                $targetLevel = $fieldLevel;
                $targetKey = $fieldId;
            }
        }
    } else {
        foreach ($keyholder as $fieldId) {
            $fieldLevel = isset($resourcearray['f'.$fieldId])
                ? (int)$resourcearray['f'.$fieldId]
                : 0;
            if ($fieldLevel > $targetLevel) {
                $targetLevel = $fieldLevel;
                $targetKey = $fieldId;
            }
        }
    }
    return $targetLevel;
}
	
	/*****************************************
	Function to process Calculate Battle
	(Phase 2: orchestrator - fiecare sectiune
	a devenit un helper privat, mai jos)

	$previousHeroOutcome: result of an earlier
	pass over the SAME battle (rams changed the
	wall level, so it is replayed). When set,
	the hero damage is not applied a second
	time - the first verdict is reused instead
	(issue #372).
	*****************************************/

	function calculateBattle(
		$Attacker, $Defender,
		$def_wall, $att_tribe, $def_tribe,
		$residence, $attpop, $defpop,
		$type,
		$def_ab,
		$att_ab1, $att_ab2, $att_ab3, $att_ab4, $att_ab5, $att_ab6, $att_ab7, $att_ab8,
		$tblevel, $stonemason, $walllevel,
		$offhero, $hero_strenght, $deffhero,
		$AttackerID, $DefenderID,
		$AttackerWref, $DefenderWref,
		$conqureby,
		$defReinforcements = null,
		$previousHeroOutcome = null) {

    global $database, $unitsbytype;

    /******************************************************************
     * UNIT GROUP DEFINITIONS
     ******************************************************************/
    $calvaryLookup  = array_flip($unitsbytype['cavalry']);
    $catapultLookup = array_flip($unitsbytype['catapult']);
    $ramsLookup     = array_flip($unitsbytype['ram']);

    /******************************************************************
     * BASE VARIABLES
     ******************************************************************/
    $result = [];
    $units  = [];

    $attAb = [
        1 => $att_ab1, 2 => $att_ab2, 3 => $att_ab3, 4 => $att_ab4,
        5 => $att_ab5, 6 => $att_ab6, 7 => $att_ab7, 8 => $att_ab8
    ];

    /******************************************************************
     * ARTIFACTS (GLOBAL EFFECTS)
     ******************************************************************/
    $attacker_artefact = $database->getArtifactsValueInfluence($AttackerID, $AttackerWref, 3, 1, false);
    $defender_artefact = $database->getArtifactsValueInfluence($DefenderID, $DefenderWref, 3, 1, false);
    $strongerbuildings = $database->getArtifactsValueInfluence($DefenderID, $DefenderWref, 1, 1, false);

    $isWWVillage = $database->getVillageField($DefenderWref, 'natar');

    /******************************************************************
     * HERO LOADING (ATTACKER / DEFENDER)
     ******************************************************************/
    $atkhero = null;
    $defenderhero = null;

    if (!empty($Attacker['uhero'])) {
        $atkhero = $this->getBattleHero($AttackerID);
    }

    if (!empty($Defender['hero'])) {
        $defenderhero = $this->getBattleHero($DefenderID);
    }

    /******************************************************************
     * DEFENDER FORCES (BASE + REINFORCEMENTS) - citeste DB, nu scrie
     ******************************************************************/
    $defForces = $this->computeDefenderForces(
        $Defender, $def_ab, $type,
        $defender_artefact, $defenderhero,
        $DefenderWref, $defReinforcements,
        $DefenderID
    );

    $dp       = $defForces['dp'];
    $cdp      = $defForces['cdp'];
    $involve  = $defForces['involve'];
    $detected = $defForces['detected'];

    if ($defForces['def_hero_unit'] !== null) {
        $units['Def_unit']['hero'] = $defForces['def_hero_unit'];
    }

    $DefendersAll = $defForces['DefendersAll'];

    /******************************************************************
     * ATTACKER FORCES (AP/CAP/CATP/RAM + HERO OFFENSE)
     ******************************************************************/
    $start = ($att_tribe - 1) * 10 + 1;
    $end   = $att_tribe * 10;

    $attForces = $this->computeAttackerForces(
        $Attacker, $att_tribe, $type, $attAb,
        $attacker_artefact, $atkhero,
        $offhero, $hero_strenght, $deffhero,
        $DefenderID,
        $calvaryLookup, $catapultLookup, $ramsLookup,
        $dp, $cdp,
        $AttackerID
    );

    $ap   = $attForces['ap'];
    $cap  = $attForces['cap'];
    $catp = $attForces['catp'];
    $ram  = $attForces['ram'];

    // bonusurile de erou din simulator (deffhero) multiplica apararea,
    // de aceea dp/cdp trec prin computeAttackerForces() si se intorc aici
    $dp  = $attForces['dp'];
    $cdp = $attForces['cdp'];

    $involve += $attForces['involve'];

    if (!empty($attForces['att_units'])) {
        $units['Att_unit'] = $attForces['att_units'];
    }

    /******************************************************************
     * WALL + RESIDENCE
     ******************************************************************/
    $walled = $this->applyWallAndResidence($dp, $cdp, $def_wall, $def_tribe, $residence, $type);

    $dp  = $walled['dp'];
    $cdp = $walled['cdp'];

    /******************************************************************
     * ATTACK / DEFENSE TOTAL
     ******************************************************************/
    $bonus = $this->computeBreweryAttackBonus($AttackerID, $att_tribe);

    $points = $this->computeTotalPoints($ap, $cap, $dp, $cdp, $bonus);

    $rap = $points['rap'];
    $rdp = $points['rdp'];

    $result['Attack_points'] = $rap;
    $result['Defend_points'] = $rdp;
    // BUG FIXED (defense points): per-owner dp+cdp breakdown, for splitting
    // Defend_points proportionally in AutomationBattleResolution.php instead of
    // crediting the whole battle's defense points to the village owner alone.
    $result['DefensePointShares'] = $defForces['perOwnerDp'];

    $winner = ($rap > $rdp);

    $safeRap = max(1, (float)$rap);
    $safeRdp = max(1, (float)$rdp);

    /******************************************************************
     * MORALE + M FACTOR
     ******************************************************************/
    $moralbonus = $this->computeMorale($attpop, $defpop, $isWWVillage, $rap, $rdp);
    $Mfactor    = $this->computeMfactor($involve, $type);

    /******************************************************************
     * LOSSES (tip 1/3/4 - izoleaza singurul rand() din fisier)
     ******************************************************************/
    $losses = $this->computeLossRatios(
        $type, $winner, $att_tribe, $detected, $Attacker,
        $rap, $rdp, $safeRap, $safeRdp,
        $moralbonus, $Mfactor,
        $ram, $catp
    );

    foreach ($losses['result'] as $key => $value) {
        $result[$key] = $value;
    }

    $winner = $losses['winner'];
    $ram    = $losses['ram'];
    $catp   = $losses['catp'];

    /******************************************************************
     * CATAPULTS DAMAGE
     ******************************************************************/
    $catapultResult = $this->applyCatapultDamage(
        $catp, $tblevel, $att_ab8, $stonemason,
        $safeRap, $safeRdp, $attpop, $defpop, $strongerbuildings
    );

    foreach ($catapultResult as $key => $value) {
        $result[$key] = $value;
    }

    /******************************************************************
     * RAMS DAMAGE
     ******************************************************************/
    $ramResult = $this->applyRamDamage(
        $ram, $walllevel, $att_ab7, $stonemason,
        $safeRap, $safeRdp, $strongerbuildings
    );

    foreach ($ramResult as $key => $value) {
        $result[$key] = $value;
    }

    /******************************************************************
     * FINAL MORALE FACTOR
     ******************************************************************/
    $result[6] = pow(
        $safeRap / max($safeRdp * $moralbonus, 1),
        $Mfactor
    );

    $result['moralBonus'] = $moralbonus;

    /******************************************************************
     * CASUALTIES
     ******************************************************************/
    // NOTA: pentru tipurile fara ramura de pierderi (ex. 2/5) originalul
    // citea $result[1] nedefinit (notice per iteratie, valoare 0);
    // isset-ul de aici pastreaza valoarea (0) fara notice-uri
    $result['casualties_attacker'] = $this->computeCasualties(
        isset($result[1]) ? $result[1] : 0,
        isset($units['Att_unit']) ? $units['Att_unit'] : [],
        $start,
        $end
    );

    /******************************************************************
     * HERO DAMAGE - per-hero carry over on a recalculation
     *
     * applyHeroBattleDamage() WRITES to the hero table. When rams change
     * the wall level, the caller replays the whole battle
     * (AutomationBattleResolution::applyRamDamage()), passing the FIRST
     * pass's result back in as $previousHeroOutcome.
     *
     * issue #372 (original bug, both halves already fixed once here):
     * (a) on a naive second pass the "WHERE dead = 0" lookup no longer finds
     * a hero who just died on the first pass, so his casualty silently
     * disappeared (dead in the hero table, reported alive, t11 never
     * decremented, returnunitsComplete() put him back in the village);
     * (b) a hero who survived the first pass got his health damage
     * subtracted a SECOND time on the naive second pass.
     *
     * BUG GASIT IN AUDIT (fixed here): the previous fix for (a)/(b) skipped
     * the ENTIRE hero-damage block whenever a recalculation happened,
     * freezing the hero's fate as of the FIRST pass. But the first pass
     * runs with the OLD (pre-ram) wall level, which still counts its full
     * defense bonus - so a defender hero judged "alive" on that pass could
     * in reality belong to an army that the recalculated, post-ram-damage
     * pass now shows as 100% wiped out (applyOwnDefenceCasualties()
     * downstream uses the RECALCULATED result[2] for the actual troop
     * kills). The hero's own verdict never got re-checked against that same
     * recalculated ratio, so he could survive on record despite his whole
     * army dying with the wall.
     *
     * Fix: track each hero's PREVIOUS verdict individually (1 = already
     * dead -> short-circuit, exactly as before; 0 = survived pass 1 -> must
     * be re-run against the current ratio; null = first pass, unchanged)
     * instead of skipping the block wholesale. See applyHeroBattleDamage()
     * for how each case is handled without double-charging health.
     ******************************************************************/
    $prevAtkVerdict = ($previousHeroOutcome !== null && array_key_exists('deadheroatt', $previousHeroOutcome))
        ? $previousHeroOutcome['deadheroatt'] : null;

    $prevDefVerdict = ($previousHeroOutcome !== null && array_key_exists('deadherodef', $previousHeroOutcome))
        ? $previousHeroOutcome['deadherodef'] : null;

    if ($previousHeroOutcome !== null && isset($previousHeroOutcome['casualties_attacker'][11])) {
        // A first-pass death must never be lost, even before the attacker
        // block below gets a chance to re-derive it.
        $result['casualties_attacker'][11] = $previousHeroOutcome['casualties_attacker'][11];
    }

    /******************************************************************
     * HERO DAMAGE (ATTACKER)
     ******************************************************************/
    if ($prevAtkVerdict !== 1 && !empty($units['Att_unit']['hero']) && !empty($atkhero['heroid'])) {

        /**
         * A fost armata NIMICITA?
         *
         * Ne uitam la pierderile REALE, nu la $result[1]. Acela e un raport de
         * PUTERE, si pe ramura in care atacatorul e considerat castigator poate
         * iesi 0.9997 chiar daca toate unitatile au murit - pierderile se
         * rotunjesc la intreg, raportul nu.
         *
         * Din cauza asta o verificare de tipul "$result[1] >= 1" nu se
         * declansa niciodata in luptele reale.
         */
        $sentTotal = 0;
        $deadTotal = 0;

        for ($ci = $start; $ci <= $end; $ci++) {
            // ATENTIE la indexare: unitatile trimise sunt cheiate dupa ID-ul
            // real (ex. 61..70 la egipteni), dar computeCasualties() intoarce
            // pierderile cheiate 1..10. Fara conversie, cele doua sume nu se
            // refera la aceleasi unitati.
            $slot = $ci - $start + 1;

            $sentTotal += isset($units['Att_unit'][$ci]) ? (int) $units['Att_unit'][$ci] : 0;
            $deadTotal += isset($result['casualties_attacker'][$slot])
                ? (int) $result['casualties_attacker'][$slot] : 0;
        }

        $armyWipedOut = ($sentTotal > 0 && $deadTotal >= $sentTotal);

        $dead = $this->applyHeroBattleDamage(
            $atkhero['heroid'],
            $result[1],
            // T4 hero port (Phase 5): doar eroul ATACATOR beneficiaza de
            // reducerea de daune din armuri (comportament original pastrat)
            $atkhero['uid'] ?? 0,
            $armyWipedOut,
            $prevAtkVerdict
        );

        if ($dead !== null) {
            $result['deadheroatt'] = $dead;
        }

        if ($dead === 1) {
            $result['casualties_attacker'][11] = 1;
        }
    } elseif ($prevAtkVerdict !== null) {
        $result['deadheroatt'] = $prevAtkVerdict;
    }

    /******************************************************************
     * HERO DAMAGE (DEFENDER)
     ******************************************************************/
    if ($prevDefVerdict !== 1 && !empty($units['Def_unit']['hero']) && !empty($defenderhero['heroid'])) {

        $dead = $this->applyHeroBattleDamage($defenderhero['heroid'], $result[2], null, false, $prevDefVerdict);

        if ($dead !== null) {
            $result['deadherodef'] = $dead;
        }
    } elseif ($prevDefVerdict !== null) {
        $result['deadherodef'] = $prevDefVerdict;
    }

    /******************************************************************
     * HERO DAMAGE (DEFENDER + REINFORCEMENTS)
     ******************************************************************/
    if (!empty($DefendersAll)) {

        $prevRefVerdicts = ($previousHeroOutcome !== null && isset($previousHeroOutcome['deadheroref']))
            ? $previousHeroOutcome['deadheroref'] : [];

        $battleHeroesCache = [];
        $villageOwnerCache = [];

        foreach ($DefendersAll as $defenders) {

            if (empty($defenders['hero'])) {
                continue;
            }

            $prevRefVerdict = isset($prevRefVerdicts[$defenders['id']]) ? $prevRefVerdicts[$defenders['id']] : null;

            if ($prevRefVerdict === 1) {
                $result['deadheroref'][$defenders['id']] = 1;
                continue;
            }

            $fromVillage = (int)$defenders['from'];

            if (!isset($villageOwnerCache[$fromVillage])) {
                $villageOwnerCache[$fromVillage] = (int)$database->getVillageField($fromVillage, "owner");
            }

            $owner = $villageOwnerCache[$fromVillage];

            if (!isset($battleHeroesCache[$owner])) {
                $battleHeroesCache[$owner] = $this->getBattleHero($owner);
            }

            $heroarraydefender = $battleHeroesCache[$owner];

            if (empty($heroarraydefender['heroid'])) {
                continue;
            }

            $dead = $this->applyHeroBattleDamage($heroarraydefender['heroid'], $result[2], null, false, $prevRefVerdict);

            if ($dead !== null) {
                $result['deadheroref'][$defenders['id']] = $dead;
            }
        }
    } elseif ($previousHeroOutcome !== null && isset($previousHeroOutcome['deadheroref'])) {
        $result['deadheroref'] = $previousHeroOutcome['deadheroref'];
    }

    /******************************************************************
     * BOUNTY CALCULATION
     ******************************************************************/
    $result['bounty'] = $this->computeBounty(
        $Attacker,
        $result['casualties_attacker'],
        $att_tribe,
        $start,
        $end
    );

    return $result;
}

	/*****************************************
	Phase 2 helper: apararea de baza +
	intaririle (citeste DB, nu scrie nimic)
	*****************************************/

	private function computeDefenderForces(
    $Defender, $def_ab, $type,
    $defender_artefact, $defenderhero,
    $DefenderWref, $defReinforcements,
    $DefenderID = 0
) {

    global $database;

    $dp = 0;
    $cdp = 0;
    $involve = 0;
    $detected = false;
    $defHeroUnit = null;
    // BUG FIXED (defense points): tracks how much of the total dp+cdp pool each
    // owner contributed (village owner's own troops + each reinforcement sender's
    // troops), keyed by owner id. Purely additive - only consumed by the new
    // 'perOwnerDp' return key below, doesn't change $dp/$cdp or anything else.
    $perOwnerDp = [];
    $DefenderID = (int) $DefenderID;

    /******************************************************************
     * DEFENDER BASE FORCES
     ******************************************************************/
    if ($type == 1) {

        $datadefScout = $this->getDataDefScout($Defender, $def_ab, $defender_artefact);

        $dp      += $datadefScout['dp'];
        $cdp     += $datadefScout['cdp'];
        $involve += $datadefScout['involve'];

        if (!$detected && $datadefScout['detect']) {
            $detected = $datadefScout['detect'];
        }

        if ($DefenderID > 0) {
            $perOwnerDp[$DefenderID] = ($perOwnerDp[$DefenderID] ?? 0) + $datadefScout['dp'] + $datadefScout['cdp'];
        }

    } else {

        $datadef = $this->getDataDef($Defender, $def_ab);

        $own_dp  = $datadef['dp'];
        $own_cdp = $datadef['cdp'];

        $involve += $datadef['involve'];

        if (isset($Defender['hero']) && $Defender['hero'] != 0) {

            $defHeroUnit = $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'];
        }

        $dp += $own_dp;
        $cdp += $own_cdp;

        if ($DefenderID > 0) {
            $perOwnerDp[$DefenderID] = ($perOwnerDp[$DefenderID] ?? 0) + $own_dp + $own_cdp;
        }
    }

    /******************************************************************
     * REINFORCEMENTS
     ******************************************************************/
    $DefendersAll = ($defReinforcements === null)
        ? $database->getEnforceVillage($DefenderWref, 0)
        : $defReinforcements;

    if (!empty($DefendersAll) && $DefenderWref > 0) {

        $ownerCache = [];
        $userCache  = [];
        $abCache    = [];
        $heroCache  = [];

        foreach ($DefendersAll as $defenders) {

            $fromvillage = (int)$defenders['from'];

            if ($fromvillage <= 0) {
                continue;
            }

            if (!isset($ownerCache[$fromvillage])) {
                $ownerCache[$fromvillage] = (int)$database->getVillageField($fromvillage, "owner");
            }

            $owner = $ownerCache[$fromvillage];

            if (!isset($userCache[$owner])) {
                $userCache[$owner] = $database->getUserArray($owner, 1);
            }

            $enforcetribe = (int)$userCache[$owner]["tribe"];
            $ud = ($enforcetribe - 1) * 10;

            if (!isset($abCache[$fromvillage])) {
                $abCache[$fromvillage] = $database->getABTech($fromvillage);
            }

            $armory = $abCache[$fromvillage];

            for ($i = 1; $i <= 8; $i++) {
                $def_ab[$ud + $i] = isset($armory['a'.$i]) ? $armory['a'.$i] : 0;
            }

            if ($type == 1) {

                $datadefScout = $this->getDataDefScout($defenders, $def_ab, $defender_artefact);

                $dp  += $datadefScout['dp'];
                $cdp += $datadefScout['cdp'];

                $involve += $datadefScout['involve'];

                if (!$detected && $datadefScout['detect']) {
                    $detected = $datadefScout['detect'];
                }

                if ($owner > 0) {
                    $perOwnerDp[$owner] = ($perOwnerDp[$owner] ?? 0) + $datadefScout['dp'] + $datadefScout['cdp'];
                }

            } else {

                $datadef = $this->getDataDef($defenders, $def_ab);

                $reinf_dp  = $datadef['dp'];
                $reinf_cdp = $datadef['cdp'];

                $involve += $datadef['involve'];

                if (!empty($defenders['hero'])) {

                    if (!isset($heroCache[$owner])) {
                        $heroCache[$owner] = $this->getBattleHero($owner);
                    }

                    $defhero = $heroCache[$owner];

                    $reinf_dp  += $defhero['di'];
                    $reinf_cdp += $defhero['dc'];

                    $reinf_dp  *= $defhero['db'];
                    $reinf_cdp *= $defhero['db'];
                }

                $dp  += $reinf_dp;
                $cdp += $reinf_cdp;

                if ($owner > 0) {
                    $perOwnerDp[$owner] = ($perOwnerDp[$owner] ?? 0) + $reinf_dp + $reinf_cdp;
                }
            }
        }
    }

    return [
        'dp'            => $dp,
        'cdp'           => $cdp,
        'involve'       => $involve,
        'detected'      => $detected,
        'def_hero_unit' => $defHeroUnit,
        'DefendersAll'  => $DefendersAll,
        // BUG FIXED (defense points): owner id -> dp+cdp contributed, so callers
        // can split battle-outcome defense points proportionally per defender
        // instead of awarding the whole battle's points to the village owner.
        'perOwnerDp'    => $perOwnerDp
    ];
}

	/*****************************************
	Phase 2 helper: fortele atacatorului
	(ap/cap/catp/ram + ofensiva eroului)
	*****************************************/

	private function computeAttackerForces(
    $Attacker, $att_tribe, $type, $attAb,
    $attacker_artefact, $atkhero,
    $offhero, $hero_strenght, $deffhero,
    $DefenderID,
    $calvaryLookup, $catapultLookup, $ramsLookup,
    $dp, $cdp,
    $AttackerID = 0
) {

    global $unitsbytype;

    $start = ($att_tribe - 1) * 10 + 1;
    $end   = $att_tribe * 10;

    $ap = 0;
    $cap = 0;
    $catp = 0;
    $ram = 0;
    $involve = 0;
    $attUnits = [];

    if ($type == 1) {

        // era: $abcount = ($att_tribe == 3) ? 3 : 4; $scoutAB = ${'att_ab'.$abcount};
        // (galii au cercetasul pe slotul 3, restul triburilor pe slotul 4)
        $scoutAB = ($att_tribe == 3) ? $attAb[3] : $attAb[4];

        for ($i = $start; $i <= $end; $i++) {

            $unitAmount = (int)$Attacker['u'.$i];

            if ($unitAmount <= 0) {
                continue;
            }

            global ${'u'.$i};

            if (in_array($i, $unitsbytype['scout'])) {

                if ($scoutAB > 0) {

                    $unitAttack = round(
                        35 + (
                            35 + (300 * ${'u'.$i}['pop'] / 7)
                        ) * (pow(1.007, $scoutAB) - 1),
                        4
                    );

                    $ap += $unitAttack * $unitAmount;

                } else {
                    $ap += 35 * $unitAmount;
                }
            }

            $involve += $unitAmount;
            $attUnits[$i] = $unitAmount;
        }

        // NOTA: artefactul ofensiv al atacatorului se aplica DOAR pe ramura
        // de spionaj (tip 1), nu si la atacurile normale - comportament
        // original pastrat 1:1; de verificat separat daca e intentionat
        $ap *= $attacker_artefact;

    } else {

        $abcount = 1;

        for ($i = $start; $i <= $end; $i++) {

            $unitAmount = (int)$Attacker['u'.$i];

            if ($unitAmount <= 0) {
                $abcount++;
                continue;
            }

            global ${'u'.$i};

            $unitData = ${'u'.$i};
            $unitAttack = $unitData['atk'];

            if ($abcount <= 8 && $attAb[$abcount] > 0) {

                $unitAttack = round(
                    $unitAttack + (
                        $unitAttack + (300 * $unitData['pop'] / 7)
                    ) * (pow(1.007, $attAb[$abcount]) - 1),
                    4
                );
            }

            $totalAttack = $unitAttack * $unitAmount;

            if (isset($calvaryLookup[$i])) {
                $cap += $totalAttack;
            } else {
                $ap += $totalAttack;
            }

            if (isset($catapultLookup[$i])) {
                $catp += $unitAmount;
            }

            if (isset($ramsLookup[$i])) {
                $ram += $unitAmount;
            }

            $involve += $unitAmount;
            $attUnits[$i] = $unitAmount;

            $abcount++;
        }

        /******************************************************************
         * HERO OFFENSE
         ******************************************************************/
        if (!empty($Attacker['uhero']) && !empty($atkhero)) {

            $attUnits['hero'] = $Attacker['uhero'];

            $heroOb = (!empty($atkhero['ob']) && $atkhero['ob'] > 0) ? $atkhero['ob'] : 1;
            $ap  *= $heroOb;
            $cap *= $heroOb;

            // 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) {

            $simHero = $this->getBattleHeroSim($offhero);

            $ap  *= $simHero['ob'];
            $cap *= $simHero['ob'];

            $ap += $hero_strenght;

            /**
             * SIMULATOR: aplicam si efectele echipamentului eroului.
             *
             * Ramura reala de mai sus foloseste eroul din baza de date; aici
             * eroul e sintetic, construit din procentele introduse manual, deci
             * itemele lipseau complet si simularea dadea alt rezultat decat
             * lupta adevarata.
             *
             * Le luam de la jucatorul care ruleaza simularea: forta din arme si
             * scuturi (fight_strength), bonusul pe tipul de unitate al armei si
             * cornul de vanatoare contra Natarilor.
             */
            if (class_exists('HeroBattleBonus') && HeroBattleBonus::enabled()) {
                global $session;

                $simUid = isset($session->uid) ? (int) $session->uid : 0;

                if ($simUid > 0) {
                    $ap += (int) HeroBattleBonus::statBonus($simUid);

                    list($simAp, $simCap) = HeroBattleBonus::unitOffense($simUid, $Attacker, $calvaryLookup);
                    $ap  += $simAp;
                    $cap += $simCap;

                    $simNatar = HeroBattleBonus::natarMultiplier($simUid, $DefenderID);

                    if ($simNatar > 1.0) {
                        $ap  *= $simNatar;
                        $cap *= $simNatar;
                    }
                }
            }
        }

        if ($deffhero > 0) {

            $dfdhero = $this->getBattleHeroSim($deffhero);

            $dp  *= $dfdhero['ob'];
            $cdp *= $dfdhero['ob'];

            // Aceleasi iteme conteaza si cand simulezi apararea.
            if (class_exists('HeroBattleBonus') && HeroBattleBonus::enabled()) {
                global $session;

                $simUid = isset($session->uid) ? (int) $session->uid : 0;

                if ($simUid > 0) {
                    $simDef = (int) HeroBattleBonus::unitDefense($simUid, $Attacker);
                    $dp  += $simDef;
                    $cdp += $simDef;
                }
            }
        }
    }

    // Bonusul de alianta "Metallurgy": forta trupelor creste peste
    // upgrade-urile de fierarie, care sunt deja aplicate mai sus pe fiecare
    // unitate. Se inmulteste cu ele, nu se aduna - la fel ca in T4.
    //
    // Se aplica pe toate cele patru totaluri, ca sa acopere si atacul, si
    // apararea: aceeasi functie calculeaza ambele parti, in functie de rolul
    // jucatorului in lupta.
    if (class_exists('AllianceBonus') && AllianceBonus::enabled()) {

        // BUG REPARAT: aici se folosea (int) $Attacker, dar $Attacker e ARRAYUL
        // de unitati, nu un id de sat. In PHP, (int) pe un array da 1, deci se
        // citea proprietarul satului 1 - un jucator la intamplare sau nimeni.
        // Bonusul de Metalurgie nu ajungea niciodata la cine trebuie.
        //
        // Atacatorul vine acum ca parametru ($AttackerID). Daca lipseste (de
        // exemplu in simulator), cadem pe eroul atacator, care poarta si el uid-ul.
        $metalOwner = (int) $AttackerID;

        if ($metalOwner <= 0 && !empty($atkhero['uid'])) {
            $metalOwner = (int) $atkhero['uid'];
        }

        // SIMULATOR: nu exista nici sat atacator, nici erou din baza de date,
        // asa ca luam bonusurile aliantei jucatorului care ruleaza simularea.
        // Intr-o lupta reala $AttackerID e mereu setat, deci ramura asta nu se
        // atinge acolo.
        if ($metalOwner <= 0 && ($offhero > 0 || $deffhero > 0 || $hero_strenght > 0)) {
            global $session;

            if (isset($session->uid)) {
                $metalOwner = (int) $session->uid;
            }
        }

        if ($metalOwner > 0) {
            $metalMult = AllianceBonus::multiplier($metalOwner, AllianceBonus::METALLURGY);

            if ($metalMult > 1.0) {
                $ap  *= $metalMult;
                $cap *= $metalMult;
                $dp  *= $metalMult;
                $cdp *= $metalMult;
            }
        }
    }

    return [
        'ap'        => $ap,
        'cap'       => $cap,
        'catp'      => $catp,
        'ram'       => $ram,
        'involve'   => $involve,
        'att_units' => $attUnits,
        'dp'        => $dp,
        'cdp'       => $cdp
    ];
}

	/*****************************************
	Phase 2 helper: zid + resedinta (pur)
	*****************************************/

	private function applyWallAndResidence($dp, $cdp, $def_wall, $def_tribe, $residence, $type) {

    $residenceBonus = (2 * ($residence * $residence)) + 10;

    if ($def_wall > 0) {

        // Factori de zid per trib: 1 City Wall, 2 Earth Wall, 3 Palisade,
        // 6 Makeshift Wall, 7 Stone Wall, 8 Defensive Wall, 9 Barricade
        $wallFactors = array(1 => 1.030, 2 => 1.020, 3 => 1.025, 6 => 1.015, 7 => 1.030, 8 => 1.028, 9 => 1.022);
        $factor = isset($wallFactors[$def_tribe]) ? $wallFactors[$def_tribe] : 1.025;

        $wallMultiplier = round(pow($factor, $def_wall), 3);

        if ($dp > 0 || $cdp > 0) {

            if ($type == 1) {

                $dp *= $wallMultiplier;
                $dp += 10;

            } else {

                $dp  *= $wallMultiplier;
                $cdp *= $wallMultiplier;

                $resBonus = $residenceBonus * $wallMultiplier;

                $dp  += $resBonus;
                $cdp += $resBonus;
            }

        } else {

            $baseWall = 10 * $wallMultiplier * $def_wall;

            $dp  = $baseWall;
            $cdp = $baseWall;

            if ($type != 1) {

                $resBonus = $residenceBonus * $wallMultiplier;

                $dp  += $resBonus;
                $cdp += $resBonus;

            } else {

                $dp += 10;
                $cdp = 0;
            }
        }

    } elseif ($type != 1) {

        $dp  += $residenceBonus;
        $cdp += $residenceBonus;
    }

    return ['dp' => $dp, 'cdp' => $cdp];
}

	/*****************************************
	Phase 2 helper: bonusul de atac Brewery
	(citeste DB; extras ca sa ramana
	computeTotalPoints() pur - vezi tabelul
	de mapare din livrare)
	*****************************************/

	private function computeBreweryAttackBonus($AttackerID, $att_tribe) {

    global $bid35, $database;

    $bonus = 0;

    // Brewery (35) Mead-Festival attack bonus: Teuton-only, capital-only but
    // empire-wide, and active ONLY while a festival is running (72h). It must be
    // read from the attacker's CAPITAL - $AttackerWref is the launching village,
    // which usually has no Brewery - and gated on the festival being active,
    // otherwise the bonus is permanent and never reacts to the festival being
    // started/expired (issue #294). This mirrors the catapult-randomization gate
    // in Units.php and the chief-penalty gate in Automation.php. The simulator
    // passes AttackerID = 0, so it keeps a 0 bonus exactly as before.
    if ($AttackerID != 0 && $att_tribe == 2) {

        $attackerCapital = $database->getVillage($AttackerID, 3);

        if ($attackerCapital && (int)$attackerCapital['festival'] > time()) {

            $typeLevel = $this->getTypeLevel(35, $attackerCapital['wref']);

            $bonus = isset($bid35[$typeLevel])
                ? $bid35[$typeLevel]['attri']
                : 0;
        }
    }

    return $bonus;
}

	/*****************************************
	Phase 2 helper: rap/rdp (pur)
	*****************************************/

	private function computeTotalPoints($ap, $cap, $dp, $cdp, $bonus) {

    $rap = round(
        ($ap + $cap) + (
            (($ap + $cap) / 100) * $bonus
        )
    );

    if ($rap == 0) {

        $rdp = round($dp + $cdp);

    } else {

        $rdp = round(
            (round($cap / $rap, 4) * $cdp) +
            (round($ap / $rap, 4) * $dp)
        );
    }

    return ['rap' => $rap, 'rdp' => $rdp];
}

	/*****************************************
	Phase 2 helper: morala (pur)
	*****************************************/

	private function computeMorale($attpop, $defpop, $isWWVillage, $rap, $rdp) {

    if ($attpop > $defpop && !$isWWVillage) {

        $moralbonus = 1 / round(
            max(
                0.667,
                pow(
                    $defpop / $attpop,
                    0.2 * min(1, $rap / max($rdp, 1))
                )
            ),
            3
        );

    } else {

        $moralbonus = 1.0;
    }

    return $moralbonus;
}

	/*****************************************
	Phase 2 helper: factorul M (pur)
	*****************************************/

	private function computeMfactor($involve, $type) {

    if ($involve >= 1000 && $type != 1) {
        $Mfactor = 2 * round((1.8592 - pow($involve, 0.015)), 4);
    } else {
        $Mfactor = 1.5;
    }

    if ($Mfactor < 1.2578) {
        $Mfactor = 1.2578;
    } elseif ($Mfactor > 1.5) {
        $Mfactor = 1.5;
    }

    return $Mfactor;
}

	/*****************************************
	Phase 2 helper: ratele de pierderi pentru
	tipurile 1/3/4 - izoleaza singurul rand()
	din fisier (hero_fealthy); pentru alte
	tipuri nu seteaza cheile 1/2, exact ca
	originalul
	*****************************************/

	private function computeLossRatios(
    $type, $winner, $att_tribe, $detected, $Attacker,
    $rap, $rdp, $safeRap, $safeRdp,
    $moralbonus, $Mfactor,
    $ram, $catp
) {

    $result = [];

    if ($type == 1) {

        $holder = pow((($rdp * $moralbonus) / $safeRap), $Mfactor);

        if ($holder > 1 || $rdp > $rap) {
            $holder = 1;
        }

        $result[1] = ($att_tribe == 5 || !$detected) ? 0 : $holder;
        $result[2] = 0;

    } elseif ($type == 4) {

        $holder = ($winner)
            ? pow((($rdp * $moralbonus) / $safeRap), $Mfactor)
            : pow(($safeRap / max($safeRdp * $moralbonus, 1)), $Mfactor);

        $holder = $holder / (1 + $holder);

        $result[1] = $winner ? $holder : 1 - $holder;
        $result[2] = $winner ? 1 - $holder : $holder;

        // NOTA: $result[1] e o fractie 0..1, deci "/ 100" face scaderea
        // aproape mereu 0 (berbecii/catapultele trag cu efectivul de
        // dinainte de pierderi) - comportament original pastrat 1:1
        $ram  -= round($ram * $result[1] / 100);
        $catp -= round($catp * $result[1] / 100);

    } elseif ($type == 3) {

        $result[1] = ($winner)
            ? pow((($rdp * $moralbonus) / $safeRap), $Mfactor)
            : 1;

        if ($result[1] > 1) {

            $result[1] = 1;
            $winner = false;
            $result['Winner'] = "defender";
        }

        $result[2] = (!$winner)
            ? pow(($safeRap / max($safeRdp * $moralbonus, 1)), $Mfactor)
            : 1;

        if ($result[1] == 1) {
            $result[2] = pow(($safeRap / max($safeRdp * $moralbonus, 1)), $Mfactor);
        }

        if ($result[2] > 1) {

            $result[2] = 1;
            $winner = true;
            $result['Winner'] = "attacker";
        }

        $ku = ($att_tribe - 1) * 10 + 9;

        $kings = (int)$Attacker['u'.$ku];

        // NOTA: cast-ul (int) trunchiaza fractia la 0, deci $aviables ==
        // $kings pentru orice pierdere partiala (doar pierderea totala,
        // result[1] == 1, ii scade) - comportament original pastrat 1:1
        $aviables = $kings - round($kings * (int)$result[1]);

        if ($aviables > 0) {

            switch ($aviables) {
                case 1: $fealthy = rand(20, 30); break;
                case 2: $fealthy = rand(40, 60); break;
                case 3: $fealthy = rand(60, 80); break;
                case 4: $fealthy = rand(80, 100); break;
                default: $fealthy = 100; break;
            }

            $result['hero_fealthy'] = $fealthy;
        }

        // NOTA: acelasi "/ 100" pe fractie ca la tipul 4 - pastrat 1:1
        $ram -= ($winner)
            ? round($ram * $result[1] / 100)
            : round($ram * $result[2] / 100);

        $catp -= ($winner)
            ? round($catp * $result[1] / 100)
            : round($catp * $result[2] / 100);
    }

    return [
        'result' => $result,
        'winner' => $winner,
        'ram'    => $ram,
        'catp'   => $catp
    ];
}

	/*****************************************
	Phase 2 helper: daunele catapultelor
	asupra cladirii-tinta (determinist)
	*****************************************/

	private function applyCatapultDamage(
    $catp, $tblevel, $att_ab8, $stonemason,
    $safeRap, $safeRdp, $attpop, $defpop, $strongerbuildings
) {

    global $bid34;

    if ($catp <= 0 || $tblevel == 0) {
        return [];
    }

    $upgrades = round(200 * pow(1.0205, $att_ab8)) / 200;

    $durability = ($stonemason > 0)
        ? $bid34[$stonemason]['attri'] / 100
        : 1;

    $attackDefenseRatio = $safeRap / $safeRdp;

    $catpMoraleBonus = min(
        max(pow(($attpop / max($defpop, 1)), 0.3), 1),
        3
    );

    $catapultsDamage = $this->calculateCatapultsDamage(
        $catp,
        $upgrades,
        $durability,
        $attackDefenseRatio,
        $strongerbuildings,
        $catpMoraleBonus
    );

    return [
        3 => $this->calculateNewBuildingLevel($tblevel, $catapultsDamage),
        4 => $tblevel,
        'catapults' => [
            'upgrades' => $upgrades,
            'durability' => $durability,
            'attackDefenseRatio' => $attackDefenseRatio,
            'strongerBuildings' => $strongerbuildings,
            'moraleBonus' => $catpMoraleBonus
        ]
    ];
}

	/*****************************************
	Phase 2 helper: daunele berbecilor
	asupra zidului (determinist)
	*****************************************/

	private function applyRamDamage(
    $ram, $walllevel, $att_ab7, $stonemason,
    $safeRap, $safeRdp, $strongerbuildings
) {

    global $bid34;

    if ($ram <= 0 || $walllevel == 0) {
        return [];
    }

    $upgrades = round(200 * pow(1.0205, $att_ab7)) / 200;

    $durability = ($stonemason > 0)
        ? $bid34[$stonemason]['attri'] / 100
        : 1;

    $attackDefenseRatio = $safeRap / $safeRdp;

    $ramsDamage = $this->calculateCatapultsDamage(
        $ram,
        $upgrades,
        $durability,
        $attackDefenseRatio,
        $strongerbuildings,
        1
    );

    return [
        7 => $this->calculateNewBuildingLevel($walllevel, $ramsDamage),
        8 => $walllevel,
        'rams' => [
            'upgrades' => $upgrades,
            'durability' => $durability,
            'attackDefenseRatio' => $attackDefenseRatio,
            'strongerBuildings' => $strongerbuildings,
            'moraleBonus' => 1
        ]
    ];
}

	/*****************************************
	Phase 2 helper: pierderile atacatorului
	pe sloturile 1..10 (pur)
	*****************************************/

	private function computeCasualties($lossRatio, $attUnits, $start, $end) {

    $casualties = [];

    for ($i = $start; $i <= $end; $i++) {

        $y = $i - $start + 1;

        $casualties[$y] = round(
            $lossRatio * (isset($attUnits[$i]) ? $attUnits[$i] : 0)
        );
    }

    return $casualties;
}

	/*****************************************
	Phase 2 helper: daunele de sanatate ale
	unui erou dupa lupta - unifica cele 3
	blocuri DB duplicate (atacator, aparator,
	intariri); SINGURUL helper care scrie in
	DB. Intoarce: null = eroul nu exista /
	era deja mort (nicio scriere), 1 = a
	murit acum, 0 = a supravietuit (health
	scazut cu daunele)

	$previousVerdict (recalculare dupa daune de berbec, issue #372 +
	BUG GASIT in audit): 1 = eroul era deja mort pe primul pass -> iesim
	imediat, fara query, fara a doua scriere (asa se evita dubla scadere
	de health din bug-ul original #372). 0 = eroul supravietuise primul
	pass (si isi luase deja daunele acelui pass) - reverificam impotriva
	raportului RECALCULAT (zid daramat de berbec = aparare mai mica =
	$lossRatio real, mai mare) si il omoram daca acum trece pragul, dar
	NU ii mai scadem health inca o data daca tot supravietuieste (health-ul
	lui reflecta deja daunele din primul pass). null = pass normal / prim
	pass, comportament neschimbat.
	*****************************************/

	private function applyHeroBattleDamage($hero_id, $lossRatio, $reduceForUid = null, $armyWipedOut = false, $previousVerdict = null) {

    global $database;

    $hero_id = (int)$hero_id;

    if ($previousVerdict === 1) {
        return 1;
    }

    $_result = mysqli_query(
        $database->dblink,
        "SELECT heroid, health
         FROM " . TB_PREFIX . "hero
         WHERE dead = 0
         AND heroid = " . $hero_id . "
         LIMIT 1"
    );

    $fdb = mysqli_fetch_assoc($_result);

    if (empty($fdb)) {
        return null;
    }

    $hero_health = (int)$fdb['health'];
    $damage_health = round(100 * $lossRatio);

    // T4 hero port (Phase 5): armors reduce the health damage the
    // hero takes in one battle (flat, floored at 0). No-op when off.
    // Se aplica doar cand apelantul trimite un uid (eroul atacator).
    if ($reduceForUid !== null) {
        $damage_health = HeroBattleBonus::reduceDamage($reduceForUid, $damage_health);
    }

    /**
     * ARMATA NIMICITA -> EROUL MOARE.
     *
     * BUG REPARAT: armurile scad daunele cu o valoare fixa
     * (HeroBattleBonus::reduceDamage), asa ca la o infrangere totala eroul
     * putea cobori sub prag si supravietuia - se intorcea singur in sat, desi
     * toata armata murise.
     *
     * Reducerea din armuri ramane valabila pentru lupte partiale, dar cand
     * pierzi TOATE unitatile trimise eroul cade odata cu ele, indiferent de
     * echipament. Decizia vine din pierderile reale, calculate de apelant.
     */
    if ($armyWipedOut || $hero_health <= $damage_health || $damage_health > 90) {

        mysqli_query(
            $database->dblink,
            "UPDATE " . TB_PREFIX . "hero
             SET dead = 1, health = 0
             WHERE heroid = " . $hero_id . "
             LIMIT 1"
        );

        /**
         * BUG REPARAT: se scria doar in tabela "hero". Coloana units.hero
         * ramanea 1, deci eroul aparea in continuare in sat si in punctul de
         * adunare, desi Conacul il arata mort.
         *
         * Il scoatem din toate satele proprietarului si din intaririle lui.
         */
        $ownerRow = mysqli_fetch_assoc(mysqli_query(
            $database->dblink,
            "SELECT uid FROM " . TB_PREFIX . "hero WHERE heroid = " . $hero_id . " LIMIT 1"
        ));

        if ($ownerRow && (int) $ownerRow['uid'] > 0) {
            $heroOwner = (int) $ownerRow['uid'];

            mysqli_query($database->dblink,
                "UPDATE " . TB_PREFIX . "units u
                   JOIN " . TB_PREFIX . "vdata v ON v.wref = u.vref
                    SET u.hero = 0
                  WHERE v.owner = " . $heroOwner . " AND u.hero > 0");

            mysqli_query($database->dblink,
                "UPDATE " . TB_PREFIX . "enforcement e
                   JOIN " . TB_PREFIX . "vdata v ON v.wref = e.`from`
                    SET e.hero = 0
                  WHERE v.owner = " . $heroOwner . " AND e.hero > 0");
        }

        return 1;
    }

    /**
     * BUG GASIT in audit: eroul supravietuise deja primul pass (pre-berbec)
     * si isi luase daunele ACELUIA. Raportul recalculat aici confirma ca tot
     * supravietuieste, deci nu se mai scrie nimic - altfel s-ar scadea health
     * de doua ori pentru aceeasi lupta (exact dubla-scadere din issue #372
     * pe care fix-ul original o repara pentru alte ramuri).
     */
    if ($previousVerdict === 0) {
        return 0;
    }

    mysqli_query(
        $database->dblink,
        "UPDATE " . TB_PREFIX . "hero
         SET health = health - " . (int)$damage_health . "
         WHERE heroid = " . $hero_id . "
         LIMIT 1"
    );

    return 0;
}

	/*****************************************
	Phase 2 helper: prada maxima carata de
	supravietuitori (pur)
	*****************************************/

	private function computeBounty($Attacker, $casualties, $att_tribe, $start, $end) {

    $max_bounty = 0;

    for ($i = $start; $i <= $end; $i++) {

        global ${'u'.$i};

        $y = $i - (($att_tribe - 1) * 10);

        $aliveUnits =
            (int)$Attacker['u'.$i]
            - (int)$casualties[$y];

        $max_bounty += $aliveUnits * (int)${'u'.$i}['cap'];
    }

    return $max_bounty;
}

	/*****************************************
	Function to process Def Scout
	*****************************************/
	
	public function getDataDefScout($defenders, $def_ab, $defender_artefact) {

    $invol = 0;
    $dp = 0;
    $cdp = 0;
    $detected = false;

    /******************************************************************
     * SCOUT UNITS ONLY 
     ******************************************************************/
	 
    global $unitsbytype;
    $scoutUnits = $unitsbytype['scout'];

    foreach ($scoutUnits as $y) {
        $unitAmount = isset($defenders['u'.$y])
            ? (int)$defenders['u'.$y]
            : 0;
        if ($unitAmount <= 0) {
            continue;
        }
        global ${'u'.$y};
        $unitData = ${'u'.$y};
        $abLevel = isset($def_ab[$y])
            ? (int)$def_ab[$y]
            : 0;

        if ($abLevel > 0) {
            $unitDefense = round(
                20 + (
                    20 + (300 * $unitData['pop'] / 7)
                ) * (pow(1.007, $abLevel) - 1),
                4
            );
            $dp += $unitDefense * $unitAmount * $defender_artefact;
        } else {
            $dp += $unitAmount * 20 * $defender_artefact;
        }
        $detected = true;
        $invol += $unitAmount;
    }
    return [
        'dp'      => $dp,
        'cdp'     => $cdp,
        'detect'  => $detected,
        'involve' => $invol
    ];
}

	/*****************************************
	Function to process Deffence
	*****************************************/

	public function getDataDef($defenders, $def_ab) {

    $dp = 0;
    $cdp = 0;
    $invol = 0;

    for ($y = 1; $y <= 90; $y++) {
        $unitAmount = isset($defenders['u'.$y])
            ? (int)$defenders['u'.$y]
            : 0;
        if ($unitAmount <= 0) {
            continue;
        }
        global ${'u'.$y};
        $unitData = ${'u'.$y};
        $abLevel = isset($def_ab[$y])
            ? (int)$def_ab[$y]
            : 0;
        if ($abLevel > 0) {

            // IMPORTANT:
            // kept original formula structure / values
            // only reduced duplicate operations

            $powValue = pow(1.007, $abLevel) - 1;
            $unitDI = round(
                $unitData['di'] + (
                    $unitData['di'] + (300 * $unitData['pop'] / 7)
                ) * $powValue,
                4
            );
            $unitDC = round(
                $unitData['dc'] + (
                    $unitData['dc'] + (300 * $unitData['pop'] / 7)
                ) * $powValue,
                4
            );
            $dp += $unitDI * $unitAmount;
            $cdp += $unitDC * $unitAmount;
        } else {
            $dp += $unitAmount * $unitData['di'];
            $cdp += $unitAmount * $unitData['dc'];
        }
        $invol += $unitAmount;
    }
    return [
        'dp'      => $dp,
        'cdp'     => $cdp,
        'involve' => $invol
    ];
}
 
 	/********************************************************************
	Function to calculates the new building level, after damaging it
	********************************************************************/

	public function calculateNewBuildingLevel($oldLevel, $damage) {

    $oldLevel = (int)$oldLevel;
    $damage = (float)$damage - 0.5;
    if ($damage < 0 || $oldLevel <= 0) {
        return $oldLevel;
    }
    while ($oldLevel > 0 && $damage >= $oldLevel) {
        $damage -= $oldLevel;
        $oldLevel--;
    }
    return $oldLevel;
	}
 
  	/****************************************************
	Function to calculates the damage done by catapults
	****************************************************/

	public function calculateCatapultsDamage(
    $catapultsQuantity,
    $catapultsUpgrade,
    $durability,
    $ADRatio,
    $strongerBuildings,
    $moraleBonus
) {

    $divider = $durability * $strongerBuildings;
    if ($divider <= 0) {
        $divider = 1;
    }
    $catapultsEfficiency = floor($catapultsQuantity / $divider);
    $sigma = $this->sigma($ADRatio);
    return (
        4
        * $sigma
        * $catapultsEfficiency
        * $catapultsUpgrade
        / $moraleBonus
    );
}
};

$battle = new Battle;