<?php

#################################################################################
##              -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =-                 ##
## --------------------------------------------------------------------------- ##
##  Project:       TravianZ                                                    ##
##  Filename:      AutomationBattleResolution.php                              ##
##  Split&Refactor Shadow													   ##
##  Purpose:       sendunitsComplete + all battle resolution helper methods    ##
##                                                                             ##
##  Phase S2: Trait extracted from GameEngine/Automation.php                   ##
##            (Automation class).                                              ##
##  Methods were moved IDENTICALLY, with no logic changes.                     ##
##                                                                             ##
##  License:       TravianZ Project                                            ##
##  Copyright:     TravianZ (c) 2010-2026. All rights reserved.                ##
##  URLs:          https://travianz.org                                        ##
##                 https://github.com/Shadowss/TravianZ                        ##
#################################################################################

trait AutomationBattleResolution {


    private function resolveCatapultsDestruction(&$bdo, &$battlepart, &$info_cat, &$data, $catapultTarget, $twoRowsCatapultSetup, $isSecondRow, $catp_pic, $can_destroy, $isoasis, &$village_destroyed, $tribe) {
        global $battle, $database, $bid34;

        if(isset($catapultTarget))
        {
            //Currently targeted building/field level
            $tblevel = (int) $bdo['f'.$catapultTarget];
            //Currently targetet building/field GID (ID of the building/field type - woodcutter, cropland, embassy...)
            $tbgid = (int) $bdo['f'.$catapultTarget.'t'];
            //Currently targeted building/field ID in the database (fdata, the fID field, e.g. f1, f2, f3...)
            $tbid = (int) $catapultTarget;          
            
            //If we're targeting the WW
            if($catapultTarget == 40){
                $battlepart['catapults']['strongerBuildings'] = 1;
                $battlepart['catapults']['moraleBonus'] = 1;
            }
            
            $catapultsDamage = $battle->calculateCatapultsDamage($data['t8'],
                                                                 $battlepart['catapults']['upgrades'],                                                                											   
            											         $battlepart['catapults']['durability'],
                                                                 $battlepart['catapults']['attackDefenseRatio'],                                                               
            											         $battlepart['catapults']['strongerBuildings'],
                                                                 $battlepart['catapults']['moraleBonus']);
            
            $newLevel = $battle->calculateNewBuildingLevel($tblevel, $catapultsDamage / ($twoRowsCatapultSetup ? 2 : 1));

            //fix: Only modify build queue if actual damage was dealt
			if ($newLevel < $tblevel) {
				$database->modifyBData($data['to'], $tbid, [$newLevel, $tblevel], $tribe);
			}
            
            // building/field destroyed
            if ($newLevel == 0){       
                // prepare data to be updated
                $fieldsToSet = ["f".$tbid];
                $fieldValuesToSet = [0];
                
                // update $bdo, so we don't have to reselect later
                $bdo['f'.$catapultTarget] = 0;
                
                if ($tbid >= 19 && $tbid != 99) {
                    $fieldsToSet[] = "f".$tbid."t";
                    $fieldValuesToSet[] = 0;
                    $bdo['f'.$catapultTarget."t"] = 0;
                }
                
                // update all that needs updating
                $database->setVillageLevel($data['to'], $fieldsToSet, $fieldValuesToSet);
                
                // BUG FIXED: $tbgid is 0 when the catapult targets an already-empty field
                // slot (nothing ever built there) - $newLevel==0 still runs this whole
                // "destroyed" branch even though tblevel was already 0 (see the guard right
                // above only skips the build-queue update, not this block). There is no
                // "bid0" data array (building type IDs start at 1), so $GLOBALS["bid0"]
                // doesn't exist -> "Undefined global variable $bid0". Null-coalescing keeps
                // the exact same downstream result ($buildarray stays effectively empty,
                // so the isset() check below is still false) without the warning.
                $buildarray = $GLOBALS["bid".$tbgid] ?? null;

                if ( is_array($buildarray) && isset( $buildarray[$newLevel] ) ) {
                    // (great) warehouse level was changed
                    if ($tbgid == 10 || $tbgid == 38) {
                        $database->setMaxStoreForVillage($data['to'], $buildarray[$newLevel]['attri']);
                    }

                    // (great) granary level was changed
                    if ($tbgid == 11 || $tbgid == 39) {
                        $database->setMaxCropForVillage($data['to'], $buildarray[$newLevel]['attri']);
                    }
                }
                
                // oasis cannot be destroyed
                $pop = $this->recountPop($data['to'], false);
                if ($isoasis == 0 && $pop == 0 && $can_destroy == 1) $village_destroyed = 1;
                
                if ($isSecondRow) {
                    if ($tbid > 0) {
                        $info_cat .= "<tbody class=\"goods\"><tr><th>".INFORMATION."</th><td colspan=\"11\">
					<img class=\"unit u".$catp_pic."\" src=\"img/x.gif\" alt=\"".rc_tok('RC_CATAPULT')."\" title=\"".rc_tok('RC_CATAPULT')."\" /> ".rc_bld($tbgid, $can_destroy)." <b>".rc_tok('RC_DESTROYED')."</b>.";
                    }
                    
                    // embassy level was changed
                    if ($tbgid == 18){
                        $info_cat .= $database->checkEmbassiesAfterBattle($data['to'], $bdo['f'.$catapultTarget], false);
                    }
                    
                    $info_cat .= "</td></tr></tbody>";
                } else {
                    $info_cat = "".$catp_pic.", ".rc_bld($tbgid, $can_destroy)." <b>".rc_tok('RC_DESTROYED')."</b>.";
                    
                    // embassy level was changed
                    if ($tbgid == 18){
                        $info_cat .= $database->checkEmbassiesAfterBattle($data['to'], $bdo['f'.$catapultTarget], false);
                    }
                }
            }
            // building/field not damaged
            elseif($newLevel == $tblevel){
                if($isSecondRow) {
                    if ($tbid > 0) {
                        $info_cat .= "<tbody class=\"goods\"><tr><th>".INFORMATION."</th><td colspan=\"11\">
					<img class=\"unit u".$catp_pic."\" src=\"img/x.gif\" alt=\"".rc_tok('RC_CATAPULT')."\" title=\"".rc_tok('RC_CATAPULT')."\" /> ".rc_bld($tbgid, $can_destroy)." ".rc_tok('RC_NOT_DAMAGED')."</td></tr></tbody>";
                    }
                } else {
                    $info_cat = "".$catp_pic.",".rc_bld($tbgid, $can_destroy)." ".rc_tok('RC_NOT_DAMAGED');
                }
            }
            // building/field was damaged, let's calculate the actual damage
            else
            { 
                // update $bdo, so we don't have to reselect later
                $bdo['f'.$catapultTarget] = $newLevel;
                
                // building was damaged to a lower level
                $info_cata = " ".rc_tok('RC_DAMAGED_FROM_TO', $tblevel, $newLevel);
                
                $buildarray = $GLOBALS["bid".$tbgid];
                
                // (great) warehouse level was changed
                if ($tbgid == 10 || $tbgid == 38) {
                    $database->setMaxStoreForVillage($data['to'], $buildarray[$newLevel]['attri']);
                }
                
                // (great) granary level was changed
                if ($tbgid == 11 || $tbgid == 39) {
                    $database->setMaxCropForVillage($data['to'], $buildarray[$newLevel]['attri']);
                }
                
                $fieldsToSet = ["f".$tbid];
                $fieldValuesToSet = [$newLevel];
                
                $database->setVillageLevel($data['to'], $fieldsToSet, $fieldValuesToSet);
                
                // recalculate population and check if the village shouldn't be destroyed at this point
                $pop = $this->recountPop($data['to'], false);
                if ($isoasis == 0) {
                    if($pop == 0 && $can_destroy == 1) $village_destroyed = 1;
                }
                
                if ($isSecondRow) {
                    $info_cat .= "<tbody class=\"goods\"><tr><th>".INFORMATION."</th><td colspan=\"11\">
					<img class=\"unit u".$catp_pic."\" src=\"img/x.gif\" alt=\"".rc_tok('RC_CATAPULT')."\" title=\"".rc_tok('RC_CATAPULT')."\" /> ".rc_bld($tbgid, $can_destroy).$info_cata;
                    
                    // embassy level was changed
                    if ($tbgid == 18) {
                        $info_cat .= $database->checkEmbassiesAfterBattle($data['to'], $bdo['f'.$catapultTarget], false);
                    }
                    
                    $info_cat .= "</td></tr></tbody>";
                } else {
                    $info_cat = "" . $catp_pic . "," . rc_bld($tbgid, $can_destroy).$info_cata;
                    
                    // embassy level was changed
                    if ($tbgid == 18) {
                        $info_cat .= $database->checkEmbassiesAfterBattle($data['to'], $bdo['f'.$catapultTarget], false);
                    }
                }
            }
        }else{
            if(!isset($info_cat) || empty($info_cat) || $info_cat == ","){
                $info_cat = "".$catp_pic.", ".rc_tok('RC_NO_BUILDINGS');
            }else if(strpos($info_cat, rc_tok('RC_NO_BUILDINGS')) === false){
                $info_cat .= "<tbody class=\"goods\"><tr><th>".INFORMATION."</th><td colspan=\"11\">
                          <img class=\"unit u".$catp_pic."\" src=\"img/x.gif\" alt=\"".rc_tok('RC_CATAPULT')."\" title=\"".rc_tok('RC_CATAPULT')."\" /> ".rc_tok('RC_NO_BUILDINGS').".</td></tr></tbody>";
            }       
        }
    }

    private function applyCatapults($data, $battlepart, $catp_pic, $can_destroy, $isoasis, $targettribe, $info_cat)
    {
        global $database;

        // village_destroyed stays unset in the pop<=0 path of the original;
        // downstream treats unset as 0, so initialising to 0 here is equivalent.
        $village_destroyed = 0;

                    			$pop = $this->recountPop($data['to']);
                    			
                    			// village has been destroyed
                    			if ($pop <= 0) {
                    				if ($can_destroy == 1) $info_cat = "".$catp_pic.", ".rc_tok('RC_VILLAGE_ALREADY_DESTROYED');
                    				else $info_cat = "".$catp_pic.", ".rc_tok('RC_VILLAGE_CANT_DESTROY');
                    			}
                    			else
                    			{
                    				// village stands, let's do the damage
                    				/**
                    				 * FIRST CATAPULTS ROW
                    				 */
                    				
                    				$basearray = $data['to'];
                    				$bdo = $database->getResourceLevel($basearray, false);
                    				$catapultTarget = $data['ctar1'];
                    				$catapultTarget2 = (isset($data['ctar2']) ? $data['ctar2'] : 0);
                    				
                    				$catapults1TargetRandom = ($catapultTarget == 0);
                    				$catapults2WillNotShoot = ($catapultTarget2 == 0);
                    				$catapults2TargetRandom = ($catapults2WillNotShoot || $catapultTarget2 == 99);
                    				
                    				// we're manually targetting 1st and/or 2nd row of catapults
                    				if (!$catapults1TargetRandom)
                    				{
                    					$_catapultsTarget1Levels = [];
                    					$__catapultsTarget1AltTargets = [];
                    					
                    					// calculate targets for 1st rows of catapults
                    					$j = 0;
                    					for ($i = 1; $i <= 41; $i++)
                    					{
                    						if ($i == 41) $i = 99;
                    						
                    						// 1st row of catapults pre-selected target calculations, if needed
                    						if (!$catapults1TargetRandom && $bdo['f'.$i.'t'] == $catapultTarget && $bdo['f'.$i] > 0 && $i != 40)
                    						{
                    							$j++;
                    							$_catapultsTarget1Levels[$j]=$bdo['f'.$i];
                    							$__catapultsTarget1AltTargets[$j]=$i;
                    						}
                    					}
                    					
                    					// if we couldn't find a suitable target for 1st row of catapults,
                    					// select a random target instead
                    					if (!$catapults1TargetRandom) {
                    						if ( count( $_catapultsTarget1Levels ) > 0 ) {
                    							if ( max( $_catapultsTarget1Levels ) <= 0 ) {
                    								$catapultTarget = 0;
                    							} else {
                    								$catapultTarget = $__catapultsTarget1AltTargets[rand( 1, $j )];
                    							}
                    						} else {
                    							$catapultTarget = 0;
                    							$catapults1TargetRandom = true;
                    						}
                    					}
                    				}
                    				
                    				// 1st row of catapults set to target randomly
                    				if ($catapults1TargetRandom)
                    				{
                    					$list = [];
                    					for ($i = 1; $i <= 41; $i++)
                    					{
                    						if ($i == 41) $i = 99;
                    						if ($bdo['f'.$i] > 0 && $i != 40) $list[] = $i;
                    					}
                    					$catapultTarget = $list[rand(0, count($list) - 1)];
                    				}
                    				
                    				/**
                    				 * resolve 1st row of catapults
                    				 */
                    				$village_destroyed = 0;
                    				$this->resolveCatapultsDestruction($bdo, $battlepart, $info_cat, $data, $catapultTarget, !$catapults2WillNotShoot, false, $catp_pic, $can_destroy, $isoasis, $village_destroyed, $targettribe);
                    				
                    				/**
                    				 * SECOND CATAPULTS ROW
                    				 */
                    				
                    				// we're manually targetting 2nd row of catapults
                    				if (!$catapults2TargetRandom)
                    				{
                    					$_catapultsTarget2Levels = [];
                    					$__catapultsTarget2AltTargets = [];
                    					
                    					// calculate targets for 2nd rows of catapults
                    					$j = 0;
                    					for ($i = 1; $i <= 41; $i++)
                    					{
                    						if ($i == 41) $i = 99;
                    						
                    						// 2nd row of catapults pre-selected target calculations, if needed
                    						if (!$catapults2TargetRandom && !$catapults2WillNotShoot && $bdo['f'.$i.'t'] == $catapultTarget2 && $bdo['f'.$i] > 0 && $i != 40)
                    						{
                    							$j++;
                    							$_catapultsTarget2Levels[$j] = $bdo['f'.$i];
                    							$__catapultsTarget2AltTargets[$j] = $i;
                    						}
                    					}
                    					
                    					// if we couldn't find a suitable target for 2nd row of catapults,
                    					// select a random target instead
                    					if (!$catapults2TargetRandom) {
                    						if (count($_catapultsTarget2Levels) > 0 ) {
                    							if (max($_catapultsTarget2Levels) <= 0 ) {
                    								$catapultTarget2 = 99;
                    							}
                    							else $catapultTarget2 = $__catapultsTarget2AltTargets[rand( 1, $j )];
                    						} else {
                    							$catapultTarget2 = 99;
                    							$catapults2TargetRandom = true;
                    						}
                    					}
                    				}
                    				
                    				// 2nd row of catapults set to target randomly
                    				if ($catapults2TargetRandom && !$catapults2WillNotShoot)
                    				{
                    					$list = [];
                    					for ($i = 1; $i <= 41; $i++)
                    					{
                    						if ($i == 41) $i = 99;
                    						if ($bdo['f'.$i] > 0 && $i != 40) $list[] = $i;
                    					}
                    					// FIX (PHP log): daca $list e gol (niciun target alternativ
                    					// cu nivel>0), rand(0, count($list)-1) devine rand(0,-1).
                    					// PHP NU arunca eroare aici (verificat pe 8.3) - intoarce
                    					// pseudo-random 0 sau -1, ambele chei inexistente in $list,
                    					// deci "Undefined array key -1"/0 si $catapultTarget2=null.
                    					// Pastram fallback-ul 99 folosit deja mai sus (linia ~301)
                    					// pentru cazul "niciun target gasit".
                    					if (!empty($list)) {
                    						$catapultTarget2 = $list[ rand(0, count($list) - 1) ];
                    					} else {
                    						$catapultTarget2 = 99;
                    					}
                    				}
                    				
                    				/**
                    				 * resolve 2nd row of catapults
                    				 */
                    				if (!$catapults2WillNotShoot) {
                    					$this->resolveCatapultsDestruction($bdo, $battlepart, $info_cat, $data, $catapultTarget2, true, true, $catp_pic, $can_destroy, $isoasis, $village_destroyed, $targettribe);
                    				}
                    				
                    				// clear resource levels cache, since we might have destroyed buildings/fields by now
                    				call_user_func(get_class($database).'::clearResourseLevelsCache');
                    			}

        return ['battlepart' => $battlepart, 'info_cat' => $info_cat, 'village_destroyed' => $village_destroyed];
    }

    private function claimMovementRecord($moveid) {
        global $database;

        $moveid = (int)$moveid;
        if ($moveid <= 0) {
            return false;
        }

        $q = "UPDATE ".TB_PREFIX."movement SET proc = 1 WHERE moveid = $moveid AND proc = 0";
        mysqli_query($database->dblink, $q);

        return (mysqli_affected_rows($database->dblink) === 1);
    }
	
	/**
	* Claim atomic pentru un retur de trupe.
	*
	* Un attack ref poate avea legitim un singur movement de retur
	* (sort_type = 4).
	*
	* Daca exista deja un alt return pentru acelasi ref care a fost
	* procesat, acest movement este considerat duplicat si este marcat
	* processed fara a mai credita trupele.
	*/
	
	private function claimReturnMovementRecord($moveid, $ref) {
    global $database;

    $moveid = (int)$moveid;
    $ref    = (int)$ref;

    if ($moveid <= 0 || $ref <= 0) {
        return false;
    }

    /*
     * Lock pe ATTACK REF, nu pe moveid.
     *
     * Astfel doua movement-uri duplicate cu moveid diferit
     * nu pot trece simultan de verificarea de unicitate.
     */
    $lockResult = mysqli_query(
        $database->dblink,
        "SELECT GET_LOCK('return_attack_ref_$ref', 10) AS locked"
    );

    if (!$lockResult) {
        return false;
    }

    $lockRow = mysqli_fetch_assoc($lockResult);

    if (
        !isset($lockRow['locked']) ||
        (int)$lockRow['locked'] !== 1
    ) {
        return false;
    }

    try {
        /*
         * Verificam daca acest movement mai este pending.
         */
        $q = "
            SELECT moveid
            FROM " . TB_PREFIX . "movement
            WHERE moveid = $moveid
              AND sort_type = 4
              AND proc = 0
            LIMIT 1
        ";

        $result = mysqli_query(
            $database->dblink,
            $q
        );

        if (!$result || mysqli_num_rows($result) === 0) {
            return false;
        }

        /*
         * Verificam daca un alt return pentru acelasi attack
         * a fost deja procesat.
         */
        $q = "
            SELECT moveid
            FROM " . TB_PREFIX . "movement
            WHERE ref = $ref
              AND sort_type = 4
              AND proc = 1
              AND moveid <> $moveid
            LIMIT 1
        ";

        $result = mysqli_query(
            $database->dblink,
            $q
        );

        if ($result && mysqli_num_rows($result) > 0) {
            /*
             * Este un duplicate.
             *
             * Il consumam fara sa adaugam trupele.
             */
            mysqli_query(
                $database->dblink,
                "UPDATE " . TB_PREFIX . "movement
                 SET proc = 1
                 WHERE moveid = $moveid
                   AND proc = 0"
            );

            return false;
        }

        /*
         * Claim atomic pentru movement-ul legitim.
         */
        $result = mysqli_query(
            $database->dblink,
            "UPDATE " . TB_PREFIX . "movement
             SET proc = 1
             WHERE moveid = $moveid
               AND proc = 0"
        );

        return $result && mysqli_affected_rows($database->dblink) === 1;

    } finally {
        mysqli_query(
            $database->dblink,
            "SELECT RELEASE_LOCK('return_attack_ref_$ref')"
        );
    }
	}

    /**
     * Handle hero evasion: if the defender has evasion active and can afford it,
     * send all defender units back to base and charge 2 gold + 1 evasion charge.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $data         Current movement row (needs 'to').
     * @param int   $DefenderID   Defender's user ID.
     * @param array $DefenderUnit Unit counts for the defending village.
     * @param int   $targettribe  Defender's tribe (1=Roman, 2=Teuton, 3=Gaul…).
     * @param int   $evasion      Whether evasion is enabled (1) or not (0).
     * @param int   $maxevasion   Remaining evasion charges.
     * @param int   $gold         Defender's current gold.
     * @param bool  $cannotsend   True when troops are already returning and can't be re-sent.
     * @param int   $attackType   Type of incoming attack (must be > 2 to trigger evasion).
     */
    private function handleEvasion(
        array $data,
        int   $DefenderID,
        array $DefenderUnit,
        int   $targettribe,
        int   $evasion,
        int   $maxevasion,
        int   $gold,
        bool  $cannotsend,
        int   $attackType
    ): void {
        global $database;

        if (!($evasion == 1 && $maxevasion > 0 && $gold > 1 && !$cannotsend && $attackType > 2)) {
            return;
        }

        $playerunit  = ($targettribe - 1) * 10;
        $totaltroops = 0;
        $evasionUnitModifications_units   = [];
        $evasionUnitModifications_amounts = [];
        $evasionUnitModifications_modes   = [];

        for ($i = 1; $i <= 10; $i++) {
            $playerunit += $i;
            $data['u' . $i] = $DefenderUnit['u' . $playerunit];
            $evasionUnitModifications_units[]   = $playerunit;
            $evasionUnitModifications_amounts[] = $DefenderUnit['u' . $playerunit];
            $evasionUnitModifications_modes[]   = 0;
            $playerunit -= $i;
            $totaltroops += $data['u' . $i];
        }

        $data['u11'] = $DefenderUnit['hero'];
        $totaltroops += $data['u11'];

        if ($totaltroops > 0) {
            $evasionUnitModifications_units[]   = 'hero';
            $evasionUnitModifications_amounts[] = $DefenderUnit['hero'];
            $evasionUnitModifications_modes[]   = 0;

            $attackid = $database->addAttack($data['to'], $data['u1'], $data['u2'], $data['u3'], $data['u4'], $data['u5'], $data['u6'], $data['u7'], $data['u8'], $data['u9'], $data['u10'], $data['u11'], 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
            $database->addMovement(4, 0, $data['to'], $attackid, microtime(true), microtime(true) + (180 / EVASION_SPEED));
            $database->updateUserField($DefenderID, ["gold", "maxevasion"], [$gold - 2, $maxevasion - 1], 1);
        }

        $database->modifyUnit($data['to'], $evasionUnitModifications_units, $evasionUnitModifications_amounts, $evasionUnitModifications_modes);
    }

    /**
     * Process senator/chief attacks: reduce loyalty and, if it hits 0, conquer the village.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $data              Current movement row.
     * @param int   $type              Attack type (must be 3 for chiefing).
     * @param int   $dead9             Chiefs killed in battle.
     * @param int   $traped9           Chiefs caught in traps.
     * @param array $from              Attacker's village info row.
     * @param array $to                Defender's village info row.
     * @param array $toF               Defender's village field row (has loyalty).
     * @param int   $owntribe          Attacker's tribe.
     * @param int   $targettribe       Defender's tribe.
     * @param array $varray            All villages of the defender.
     * @param array $varray1           All villages of the attacker.
     * @param array $battlepart        Battle result.
     * @param int   $isoasis           0 = village, non-zero = oasis.
     * @param int   $village_destroyed Whether the village has already been destroyed.
     * @param int   $chief_pic         Chief unit sprite ID for report strings.
     * @return array{info_chief:string, chiefing_village:int, village_destroyed:int}
     */
    private function handleConquest(
        array $data,
        int   $type,
        int   $dead9,
        int   $traped9,
        int   $targettribe,
        array $from,
        array $to,
        array $toF,
        int   $owntribe,
        array $varray,
        array $varray1,
        array $battlepart,
        int   $isoasis,
        int   $village_destroyed,
        int   $chief_pic
    ): array {
        global $database, $units;

        $info_chief      = ',';
        $chiefing_village = 0;

        if (!(($data['t9'] - $dead9 - $traped9) > 0 && $isoasis == 0)) {
            return ['info_chief' => $info_chief, 'chiefing_village' => $chiefing_village, 'village_destroyed' => $village_destroyed];
        }

        if ($type != 3) {
            $info_chief = $chief_pic . ',' . rc_tok('RC_NO_REDUCE_CP_RAID');
            return ['info_chief' => $info_chief, 'chiefing_village' => $chiefing_village, 'village_destroyed' => $village_destroyed];
        }

        $palacelevel = $database->getResourceLevel($from['wref']);
        $plevel      = 0;

        for ($i = 1; $i <= 40; $i++) {
            if ($palacelevel['f' . $i . 't'] == 26) $plevel = $i;
            elseif ($palacelevel['f' . $i . 't'] == 25) $plevel = $i;
            elseif ($palacelevel['f' . $i . 't'] == 44) $plevel = $i; // Command Center (Huni)
        }

        // Command Center: sloturi de expansiune ca la Palat (10 -> 1, 15 -> 2, 20 -> 3)
        if ($palacelevel['f' . $plevel . 't'] == 44) {
            if ($palacelevel['f' . $plevel] < 10)       $canconquer = 0;
            elseif ($palacelevel['f' . $plevel] < 15)   $canconquer = 1;
            elseif ($palacelevel['f' . $plevel] < 20)   $canconquer = 2;
            else                                         $canconquer = 3;
        } elseif ($palacelevel['f' . $plevel . 't'] == 26) {
            if ($palacelevel['f' . $plevel] < 10)       $canconquer = 0;
            elseif ($palacelevel['f' . $plevel] < 15)   $canconquer = 1;
            elseif ($palacelevel['f' . $plevel] < 20)   $canconquer = 2;
            else                                         $canconquer = 3;
        } elseif ($palacelevel['f' . $plevel . 't'] == 25) {
            if ($palacelevel['f' . $plevel] < 10)       $canconquer = 0;
            elseif ($palacelevel['f' . $plevel] < 20)   $canconquer = 1;
            else                                         $canconquer = 2;
        } else {
            $canconquer = 0;
        }

        $expArray = $database->getVillageFields($from['wref'], 'exp1, exp2, exp3');
        $villexp  = ($expArray['exp1'] == 0) ? 0 : (($expArray['exp2'] == 0) ? 1 : (($expArray['exp3'] == 0) ? 2 : 3));

        // HOTFIX "$cp0 undefined" (acelasi tipar ca FIX U1 din Units.php): tabelele $cp0..$cpN
        // vin din Data/cp.php, incarcat pana acum doar de Session.php - deci pe traseul de CRON
        // (fara Session) verificarea de CP la cucerire era OCOLITA silentios. Data/cp.php e
        // acum inclus si in Automation.php; gardul ?? PHP_INT_MAX ramane fail-closed daca
        // tabelul lipseste totusi (fara date de CP nu se poate cuceri), identic cu Settlers().
        $mode     = CP;
        $cp_mode  = $GLOBALS['cp' . $mode] ?? [];
        $need_cps = $cp_mode[count($varray1) + 1] ?? PHP_INT_MAX;
        $user_cps = $database->getUserArray($from['owner'], 1)['cp'];

        if ($user_cps < $need_cps) {
            $info_chief = $chief_pic . ',' . rc_tok('RC_NOT_ENOUGH_CP');
            return ['info_chief' => $info_chief, 'chiefing_village' => $chiefing_village, 'village_destroyed' => $village_destroyed];
        }

        if (count($varray) <= 1 || $to['capital'] == 1 || $villexp >= $canconquer) {
            $info_chief = $chief_pic . ',' . rc_tok('RC_CANT_TAKEOVER');
            return ['info_chief' => $info_chief, 'chiefing_village' => $chiefing_village, 'village_destroyed' => $village_destroyed];
        }

        if ($to['owner'] == 3 && $to['name'] == 'WW Buildingplan') {
            $info_chief = $chief_pic . ',' . rc_tok('RC_CANT_TAKEOVER');
            return ['info_chief' => $info_chief, 'chiefing_village' => $chiefing_village, 'village_destroyed' => $village_destroyed];
        }

        if ($database->getFieldLevelInVillage($data['to'], '25, 26, 44')) {
            $info_chief = $chief_pic . ',' . rc_tok('RC_RESIDENCE_NOT_DESTROYED');
            return ['info_chief' => $info_chief, 'chiefing_village' => $chiefing_village, 'village_destroyed' => $village_destroyed];
        }

        // --- Reduce loyalty ---
        $time               = time();
        $reducedLoyaltyTotal = 0;

        for ($i = 0; $i < ($data['t9'] - $dead9 - $traped9); $i++) {
            $reducedLoyalty = ($owntribe == 1) ? rand(20, 30) : rand(20, 25);

            if ($from['celebration'] > $time && $from['type'] == 2) $reducedLoyalty += 5;
            if ($to['celebration']   > $time && $to['type']   == 2) $reducedLoyalty -= 5;

            $reducedLoyalty /= $battlepart['moralBonus'];

            // Bug fix: Brewery (35) is capital-only but empire-wide — its effect
            // must be checked on the attacker's CAPITAL, not on $data['from'] (the
            // launching village, which may not be the capital at all), and only
            // while a Mead-Festival is actually active there, not just because
            // the Brewery has been built (it has no permanent effect).
            if ($owntribe == 2) {
                $attackerCapital = $database->getVillage($from['owner'], 3);
                if ($attackerCapital && (int)$attackerCapital['festival'] > $time && $this->getTypeLevel(35, $attackerCapital['wref']) > 0) {
                    $reducedLoyalty /= 2;
                }
            }

            $reducedLoyaltyTotal += $reducedLoyalty;
        }

        if (($toF['loyalty'] - $reducedLoyaltyTotal) > 0) {
            $info_chief = $chief_pic . ',' . rc_tok('RC_LOYALTY_LOWERED', floor($toF['loyalty']), floor($toF['loyalty'] - $reducedLoyaltyTotal));
            $database->setVillageField($data['to'], 'loyalty', ($toF['loyalty'] - $reducedLoyaltyTotal));
            return ['info_chief' => $info_chief, 'chiefing_village' => $chiefing_village, 'village_destroyed' => $village_destroyed];
        }

        if ($village_destroyed) {
            return ['info_chief' => $info_chief, 'chiefing_village' => $chiefing_village, 'village_destroyed' => $village_destroyed];
        }

        // --- Village conquered ---
        $villname = addslashes($database->getVillageField($data['to'], 'name'));
        $artifact = reset($database->getOwnArtefactInfo($data['to']));

        $info_chief = $chief_pic . ',' . rc_tok('RC_INHABITANTS_JOIN', $villname);

        if ($artifact['vref'] == $data['to']) {
            $database->claimArtefact($data['to'], $data['to'], $database->getVillageField($data['from'], 'owner'));
        }

        $database->setVillageFields($data['to'], ['loyalty', 'owner'], [0, $database->getVillageField($data['from'], 'owner')]);
		
		// Milestones: first WW village ever conquered, and — separately —
        // first village ever conquered FROM ANOTHER PLAYER (not from
        // Natars). $to is this function's own parameter (not re-fetched),
        // so $to['natar']/$to['owner'] still reflect the village's state
        // from BEFORE this conquest, which is exactly what we need here.
        // natar==1 marks one of the 13 pre-built WW conquest targets (see
        // Artifacts::createWWVillages()) — Natars' capital and artifact/
        // plan villages are natar=0, so this check cannot misfire on those.
        if (defined('NEW_FUNCTIONS_MILESTONES') && NEW_FUNCTIONS_MILESTONES) {
            $newOwner = $database->getVillageField($data['from'], 'owner');
            if ((int)($to['natar'] ?? 0) === 1) {
                $database->recordMilestoneIfFirst('first_ww', $newOwner, $data['to']);
            } elseif ((int)($to['owner'] ?? 0) !== 3) {
                $database->recordMilestoneIfFirst('first_pvp_conquest', $newOwner, $data['to']);
            }
        }

        $database->query("DELETE FROM " . TB_PREFIX . "abdata WHERE vref = " . (int)$data['to']);
        $database->addABTech($data['to']);

        $database->query("DELETE FROM " . TB_PREFIX . "tdata WHERE vref = " . (int)$data['to']);
        $database->addTech($data['to']);

        $database->query("DELETE FROM " . TB_PREFIX . "enforcement WHERE `from` = " . (int)$data['to']);
        $database->query("DELETE FROM " . TB_PREFIX . "route where wid = " . (int)$data['to'] . " OR `from` = " . (int)$data['to']);

        $units2reset = [];
        for ($u = 1; $u <= 90; $u++) $units2reset[] = 'u' . $u . ' = 0';
        // cucerirea goleste spitalul si coada de vindecare
        $database->clearHospital($data['to']);
        $units2reset[] = 'u99 = 0';
        $units2reset[] = 'u99o = 0';
        $units2reset[] = 'hero = 0';
        $database->query("UPDATE " . TB_PREFIX . "units SET " . implode(',', $units2reset) . " WHERE vref = " . (int)$data['to']);

        $newLevels_fieldNames  = [];
        $newLevels_fieldValues = [];

        $AttackerID = $database->getVillageField($data['from'], 'owner');
        $DefenderID = $database->getVillageField($data['to'],  'owner');
        $poparray   = $database->getVSumField([$AttackerID, $DefenderID], 'pop');
        $pop1 = $poparray[0]['Total'];
        $pop2 = $poparray[1]['Total'];

        if ($pop1 > $pop2 && $targettribe != 5) {
            $buildlevel = $database->getResourceLevel($data['to']);
            for ($i = 1; $i <= 39; $i++) {
                if ($buildlevel['f' . $i] != 0) {
                    if ($buildlevel['f' . $i . 't'] != 35 && $buildlevel['f' . $i . 't'] != 36 && $buildlevel['f' . $i . 't'] != 41) {
                        // Main Building (gid 15) nu poate cobori sub nivel 1: satul trebuie
                        // sa ramana locuibil dupa cucerire (altfel pop 0 + nicio cladire).
                        $minLevel = ($buildlevel['f' . $i . 't'] == 15) ? 1 : 0;
                        $leveldown = $buildlevel['f' . $i] - 1;
                        if ($leveldown < $minLevel) $leveldown = $minLevel;
                        $newLevels_fieldNames[]  = 'f' . $i;
                        $newLevels_fieldValues[] = $leveldown;
                        // sterge tipul cladirii DOAR daca a ajuns efectiv la 0
                        // (bug vechi: "!$leveldown > 0" era mereu adevarat pt leveldown==0,
                        //  deci stergea tipul si cand cladirea cobora la nivel 1)
                        if ($leveldown <= 0) {
                            $newLevels_fieldNames[]  = 'f' . $i . 't';
                            $newLevels_fieldValues[] = 0;
                        }
                    } else {
                        $newLevels_fieldNames[]  = 'f' . $i;
                        $newLevels_fieldValues[] = 0;
                        $newLevels_fieldNames[]  = 'f' . $i . 't';
                        $newLevels_fieldValues[] = 0;
                    }
                }
            }
            if ($buildlevel['f99'] != 0) {
                $newLevels_fieldNames[]  = 'f99';
                $newLevels_fieldValues[] = $buildlevel['f99'] - 1;
            }
        }

        // destroy wall
        $newLevels_fieldNames[]  = 'f40';
        $newLevels_fieldValues[] = 0;
        $newLevels_fieldNames[]  = 'f40t';
        $newLevels_fieldValues[] = 0;

        $database->clearExpansionSlot($data['to'], 1);

        $expArray2 = $database->getVillageFields($data['from'], 'exp1, exp2, exp3');
        if ($expArray2['exp1'] == 0)      { $exp = 'exp1'; }
        elseif ($expArray2['exp2'] == 0)  { $exp = 'exp2'; }
        else                              { $exp = 'exp3'; }
        $database->setVillageField($data['from'], $exp, $data['to']);

        $database->deleteTradeRoutesByVillage($data['to']);
        $database->setVillageLevel($data['to'], $newLevels_fieldNames, $newLevels_fieldValues);

        $units->returnTroops($data['to'], 1);

        $chiefing_village = 1;
        $database->reassignHero($data['to']);
        $this->recountPop($data['to'], false);

        return ['info_chief' => $info_chief, 'chiefing_village' => $chiefing_village, 'village_destroyed' => $village_destroyed];
    }

    /**
     * Fetch all attacks (sort_type 3, not reinforcement) that have arrived by
     * $time, joined with their attack rows, ordered by arrival.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param int $time Current timestamp.
     * @return array Rows of pending/completed attacks.
     */
    private function fetchCompletedAttacks($time) {
        global $database;

        $time = (int) $time;
        $q = "
            SELECT
                `from`, `to`, endtime, ref, ctar1, ctar2, spy, moveid, attack_type,
                t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, (SELECT oasistype FROM ".TB_PREFIX."wdata WHERE id = `to`) as oasistype
            FROM
                ".TB_PREFIX."movement,
                ".TB_PREFIX."attacks
            WHERE
                ".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 != 2
                AND
                endtime < $time
            ORDER BY endtime ASC";
        return $database->query_return($q);
    }

    /**
     * Batch-preload the village / unit / tech data used by sendunitsComplete()
     * so the per-attack loop hits the in-request cache instead of querying row
     * by row. Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $dataarray Completed attacks (each with 'from' and 'to').
     */
    private function preloadBattleData(array $dataarray) {
        global $database;

        $vilIDs = [];
        foreach ($dataarray as $data) {
            $vilIDs[$data['from']] = true;
            $vilIDs[$data['to']] = true;
        }
        $vilIDs = array_keys($vilIDs);

        $database->getProfileVillages($vilIDs, 5);
        $database->getUnit($vilIDs);
        $database->getEnforceVillage($vilIDs, 0);
        $database->getMovement(34, $vilIDs, 1);
        $database->getABTech($vilIDs);
        $database->getMInfo($vilIDs);
    }

    private function buildScoutReport($data, $spy_pic, $isoasis, $targettribe, $crannySpy, $totwood, $totclay, $totiron, $totcrop) {
        global $database;
        $info_spy = "";
                        if ($data['spy'] == 1){
                            $info_spy = "".$spy_pic.",<div class=\"res\"><img class=\"r1\" src=\"img/x.gif\" alt=\"".LUMBER."\" title=\"".LUMBER."\" />".round($totwood)." |
				 <img class=\"r2\" src=\"img/x.gif\" alt=\"".CLAY."\" title=\"".CLAY."\" />".round($totclay)." |
				 <img class=\"r3\" src=\"img/x.gif\" alt=\"".IRON."\" title=\"".IRON."\" />".round($totiron)." |
				 <img class=\"r4\" src=\"img/x.gif\" alt=\"".CROP."\" title=\"".CROP."\" />".round($totcrop)."</div>
				 <div class=\"carry\"><img class=\"car\" src=\"img/x.gif\" alt=\"".rc_tok('CARRY')."\" title=\"".rc_tok('CARRY')."\" />".rc_tok('RC_TOTAL_RESOURCES')." ".round($totwood+$totclay+$totiron+$totcrop)."</div>
	";
                        }else if($data['spy'] == 2){
                            if ($isoasis == 0){
                                $walllevel = $database->getFieldLevelInVillage($data['to'], '31, 32, 33, 42, 43, 47, 50');
                                $residencelevel = $database->getFieldLevelInVillage($data['to'], 25);
                                $palacelevel = $database->getFieldLevelInVillage($data['to'], 26);
                                $residenceimg = "<img src=\"".GP_LOCATE."img/g/g25.gif\" height=\"20\" width=\"15\" alt=\"".RESIDENCE."\" title=\"".RESIDENCE."\" />";
                                $palaceimg = "<img src=\"".GP_LOCATE."img/g/g26.gif\" height=\"20\" width=\"15\" alt=\"".PALACE."\" title=\"".PALACE."\" />";
                                $crannyimg = "<img src=\"".GP_LOCATE."img/g/g23.gif\" height=\"20\" width=\"15\" alt=\"".CRANNY."\" title=\"".CRANNY."\" />";
                                $wallgids = array(1 => 31, 2 => 32, 3 => 33, 6 => 43, 7 => 42, 8 => 47, 9 => 50);
                                $wallgid = isset($wallgids[$targettribe]) ? $wallgids[$targettribe] : 31;
                                $wallimg = "<img src=\"".GP_LOCATE."img/g/g".$wallgid."Icon.gif\" height=\"20\" width=\"15\" alt=\"".rc_tok('RC_WALL')."\" title=\"".rc_tok('RC_WALL')."\" />";
                                $info_spy = "".$spy_pic.",";
                                if($residencelevel > 0) $info_spy .= $residenceimg." ".rc_tok('RC_RESIDENCE_LEVEL')."<b>".$residencelevel."</b><br />";
                                elseif($palacelevel > 0) $info_spy .= $palaceimg." ".rc_tok('RC_PALACE_LEVEL')." <b>".$palacelevel."</b><br />";

                                if($walllevel > 0) $info_spy .= $wallimg." ".rc_tok('RC_WALL_LEVEL')." <b>".$walllevel."</b><br />";
                                $info_spy .= $crannyimg." ".rc_tok('RC_CRANNY_CAPACITY')." <b>".$crannySpy."</b>";
                            }
                            else $info_spy = "".$spy_pic.", ".rc_tok('RC_NO_INFO');
                        }
        return $info_spy;
    }

    /**
     * Release prisoners trapped in the target village during a conquest attack,
     * send them home (with 25% casualties), repair destroyed traps, and return
     * the HTML fragment for the battle-report trap line.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array  $data            Current attack row.
     * @param array  $from            Attacker village info (getMInfo).
     * @param array  $to              Defender village info (getMInfo).
     * @param int    $ownally         Attacker's alliance id.
     * @param int    $type            Attack type (3 = normal with rams/catas).
     * @param int    $totalsend_att   Total attacking troops sent.
     * @param int    $totaldead_att   Total attacking troops killed.
     * @param int    $totaltraped_att Total attacking troops trapped.
     * @return string HTML fragment for the trap report line; empty if no prisoners freed.
     */
    private function handlePrisoners($data, $from, $to, $ownally, $type, $totalsend_att, $totaldead_att, $totaltraped_att) {
        global $database, $units, $bid19, $u99;

        if ($type != 3 || $totalsend_att - ($totaldead_att + $totaltraped_att) <= 0) {
            return '';
        }

        $prisoners = $database->getPrisoners([$to['wref']], 0, false)[$to['wref'].'0'];
        if (count($prisoners) == 0) {
            return '';
        }

        $anothertroops = $mytroops = $ownDeads = $anotherDeads = 0;
        $prisoners2delete = $movementType = $movementFrom = $movementTo = $movementRef = $movementTime = $movementEndtime = [];
        $utime = microtime(true);

        foreach ($prisoners as $prisoner) {
            $p_owner = $database->getVillageField($prisoner['from'], "owner");

            if ($prisoner['from'] == $from['wref']) {
                for ($i = 1; $i <= 11; $i++) {
                    $deadPrisoners      = round($prisoner['t'.$i] / 4);
                    $mytroops          += $prisoner['t'.$i];
                    $ownDeads          += $deadPrisoners;
                    $prisoner['t'.$i] -= $deadPrisoners;
                }
                $database->modifyAttack2(
                    $data['ref'],
                    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
                    [$prisoner['t1'], $prisoner['t2'], $prisoner['t3'], $prisoner['t4'], $prisoner['t5'],
                     $prisoner['t6'], $prisoner['t7'], $prisoner['t8'], $prisoner['t9'], $prisoner['t10'], $prisoner['t11']]
                );
                $prisoners2delete[] = $prisoner['id'];
            } else {
                $p_alliance   = $database->getUserField($p_owner, "alliance", 0);
                $friendarray  = $database->getAllianceAlly($p_alliance, 1);
                $neutralarray = $database->getAllianceAlly($p_alliance, 2);
                $friend  = ($friendarray[0]['alli1']  > 0 && $friendarray[0]['alli2']  > 0 && $p_alliance > 0)
                         && ($friendarray[0]['alli1']  == $ownally || $friendarray[0]['alli2']  == $ownally)
                         && ($ownally != $p_alliance && $ownally && $p_alliance);
                $neutral = ($neutralarray[0]['alli1'] > 0 && $neutralarray[0]['alli2'] > 0 && $p_alliance > 0)
                         && ($neutralarray[0]['alli1'] == $ownally || $neutralarray[0]['alli2'] == $ownally)
                         && ($ownally != $p_alliance && $ownally && $p_alliance);

                if ($p_alliance == $ownally || $friend || $neutral) {
                    $p_tribe = $database->getUserField($p_owner, "tribe", 0);

                    for ($i = 1; $i <= 11; $i++) {
                        $deadPrisoners      = round($prisoner['t'.$i] / 4);
                        if ($p_owner == $from['owner']) {
                            $mytroops  += $prisoner['t'.$i];
                            $ownDeads  += $deadPrisoners;
                        } else {
                            $anothertroops += $prisoner['t'.$i];
                            $anotherDeads  += $deadPrisoners;
                        }
                        $prisoner['t'.$i] -= $deadPrisoners;
                    }

                    $troopsTime  = $units->getWalkingTroopsTime($prisoner['from'], $prisoner['wref'], $p_owner, $p_tribe, $prisoner, 1, 't');
                    $p_time      = $database->getArtifactsValueInfluence($p_owner, $prisoner['from'], 2, $troopsTime);
                    $p_reference = $database->addAttack(
                        $prisoner['from'],
                        $prisoner['t1'], $prisoner['t2'], $prisoner['t3'], $prisoner['t4'], $prisoner['t5'],
                        $prisoner['t6'], $prisoner['t7'], $prisoner['t8'], $prisoner['t9'], $prisoner['t10'], $prisoner['t11'],
                        3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
                    );
                    $movementType[]     = 4;
                    $movementFrom[]     = $prisoner['wref'];
                    $movementTo[]       = $prisoner['from'];
                    $movementRef[]      = $p_reference;
                    $movementTime[]     = $utime;
                    $movementEndtime[]  = $p_time + $utime;
                    $prisoners2delete[] = $prisoner['id'];
                }
            }
        }

        if (count($movementType)) {
            $database->addMovement($movementType, $movementFrom, $movementTo, $movementRef, $movementTime, $movementEndtime);
        }
        $database->deletePrisoners($prisoners2delete);

        $ownDeadsText     = ($ownAlive     = $mytroops     - $ownDeads)     > 0 ? " ".rc_tok('RC_OF_WHICH_SAVED',$ownAlive)     : "";
        $anotherDeadsText = ($anotherAlive = $anothertroops - $anotherDeads) > 0 ? " ".rc_tok('RC_OF_WHICH_SAVED',$anotherAlive) : "";

        $database->addGeneralAttack($ownDeads + $anotherDeads);

        $newtraps = round(($mytroops + $anothertroops) / 3);
        $database->modifyUnit($data['to'], ['99', '99o'], [$mytroops + $anothertroops, $mytroops + $anothertroops], [0, 0]);
        if ($newtraps > 0) {
            $repairDuration = $database->getArtifactsValueInfluence(
                $to['owner'], $to['wref'], 5,
                round(($bid19[max($this->getTypeLevel(36, $to['wref']), 1)]['attri'] / 100) * $u99['time'] / SPEED)
            );
            $database->trainUnit($to['wref'], 99, $newtraps, $u99['pop'], $repairDuration, 0);
        }

        $trapper_pic = "<img src=\"".GP_LOCATE."img/u/98.gif\" alt=\"".rc_tok('RC_TRAP')."\" title=\"".rc_tok('RC_TRAP')."\" />";
        $p_username  = $database->getUserField($from['owner'], "username", 0);

        if ($mytroops > 0 && $anothertroops > 0) {
            return $trapper_pic." <b>".$p_username."</b> ".rc_tok('RC_FREED_FROM_HIS_TROOPS', $mytroops).$ownDeadsText." ".rc_tok('RC_AND_FRIENDLY_TROOPS', $anothertroops).$anotherDeadsText.".";
        } elseif ($mytroops > 0) {
            return $trapper_pic." <b>".$p_username."</b> ".rc_tok('RC_FREED_FROM_HIS_TROOPS', $mytroops).$ownDeadsText.".";
        } elseif ($anothertroops > 0) {
            return $trapper_pic." <b>".$p_username."</b> ".rc_tok('RC_FREED_FRIENDLY_TROOPS', $anothertroops).$anotherDeadsText.".";
        }
        return '';
    }

    /**
     * Assemble the battle-report data string ($data2) and the all-attacker-dead
     * fallback ($data_fail) from pre-computed battle results.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param bool   $scout           True when this is a scouting mission.
     * @param array  $from            Attacker village info (getMInfo).
     * @param array  $to              Defender village info (getMInfo).
     * @param int    $owntribe        Attacker tribe id.
     * @param int    $targettribe     Defender tribe id.
     * @param string $unitssend_att   CSV of units sent by attacker.
     * @param string $unitsdead_att   CSV of attacker units killed.
     * @param array  $steal           [wood, clay, iron, crop] looted amounts.
     * @param array  $battlepart      calculateBattle() result.
     * @param array  $unitssend_def   CSV of defender units sent (indexed 0–5).
     * @param array  $unitsdead_def   CSV of defender units dead (indexed 0–5).
     * @param array  $unitssend_deff  Masked defender units for data_fail (indexed 0–5).
     * @param array  $unitsdead_deff  Masked dead units for data_fail (indexed 0–5).
     * @param int    $rom,$ger,$gal,$nat,$natar  Tribe presence flags.
     * @param string $DefenderHeroesTot   CSV of defender heroes totals by tribe.
     * @param string $DefenderHeroesDead  CSV of defender heroes dead by tribe.
     * @param string $info_ram        Ram result fragment.
     * @param string $info_cat        Catapult result fragment (internally extended if village destroyed).
     * @param string $info_chief      Chief/senator result fragment.
     * @param string $info_spy        Scout report fragment (scout missions only).
     * @param string $info_trap       Prisoner-release fragment (from handlePrisoners).
     * @param string $info_hero       Hero result fragment.
     * @param string $info_troop      "None of your soldiers returned" fragment.
     * @param array  $data            Current attack row (for t11).
     * @param int    $dead11          Hero casualties.
     * @param int    $herosend_def    Defender hero count.
     * @param int    $deadhero        Defender hero casualties.
     * @param string $unitstraped_att CSV of attacker units trapped.
     * @param int    $village_destroyed 1 if the village was destroyed.
     * @param int    $can_destroy     1 if the village can be destroyed.
     * @param int    $catp_pic        Catapult unit-pic id for the HTML fragment.
     * @return array ['data2' => string, 'data_fail' => string]
     */
    private function buildCombatReport(
        $scout, $from, $to, $owntribe, $targettribe,
        $unitssend_att, $unitsdead_att, $steal, $battlepart,
        $unitssend_def, $unitsdead_def, $unitssend_deff, $unitsdead_deff,
        $rom, $ger, $gal, $nat, $natar,
        $hun, $egy, $spa, $vik,
        $DefenderHeroesTot, $DefenderHeroesDead,
        $info_ram, $info_cat, $info_chief, $info_spy,
        $info_trap, $info_hero, $info_troop,
        $data, $dead11, $herosend_def, $deadhero, $unitstraped_att,
        $village_destroyed, $can_destroy, $catp_pic
    ) {
        if (!empty($scout)) {
            $data2 = ''.$from['owner'].','.$from['wref'].','.$owntribe.','.$unitssend_att.','.$unitsdead_att
                    .',0,0,0,0,0,'.$to['owner'].','.$to['wref'].','.addslashes($to['name'])
                    .',,,,'.$targettribe
                    .','.$unitssend_def[0].','.$unitsdead_def[0].','.$rom
                    .','.$unitssend_def[1].','.$unitsdead_def[1].','.$ger
                    .','.$unitssend_def[2].','.$unitsdead_def[2].','.$gal
                    .','.$unitssend_def[3].','.$unitsdead_def[3].','.$nat
                    .','.$unitssend_def[4].','.$unitsdead_def[4].','.$natar
                    .','.$unitssend_def[5].','.$unitsdead_def[5]
                    .','.$hun.','.$unitssend_def[6].','.$unitsdead_def[6]
                    .','.$egy.','.$unitssend_def[7].','.$unitsdead_def[7]
                    .','.$spa.','.$unitssend_def[8].','.$unitsdead_def[8]
                    .','.$vik.','.$unitssend_def[9].','.$unitsdead_def[9]
                    .','.$DefenderHeroesTot.','.$DefenderHeroesDead
                    .','.$info_ram.','.$info_cat.','.$info_chief.','.$info_spy
                    .','.$data['t11'].','.$dead11.','.$herosend_def.','.$deadhero
                    .',,'.$unitstraped_att;
        } else {
            if (isset($village_destroyed) && $village_destroyed == 1 && $can_destroy == 1) {
                if (strpos($info_cat, rc_tok('RC_VILLAGE_DESTROYED')) === false) {
                    $info_cat .= "<tbody class=\"goods\"><tr><th>".INFORMATION."</th><td colspan=\"11\">"
                               . "<img class=\"unit u".$catp_pic."\" src=\"img/x.gif\" alt=\"".rc_tok('RC_CATAPULT')."\" title=\"".rc_tok('RC_CATAPULT')."\" />"
                               . " ".rc_tok('RC_VILLAGE_DESTROYED')."</td></tr></tbody>";
                }
            }
            $data2 = ''.$from['owner'].','.$from['wref'].','.$owntribe.','.$unitssend_att.','.$unitsdead_att
                    .','.$steal[0].','.$steal[1].','.$steal[2].','.$steal[3].','.$battlepart['bounty']
                    .','.$to['owner'].','.$to['wref'].','.addslashes($to['name'])
                    .',,,,'.$targettribe
                    .','.$unitssend_def[0].','.$unitsdead_def[0].','.$rom
                    .','.$unitssend_def[1].','.$unitsdead_def[1].','.$ger
                    .','.$unitssend_def[2].','.$unitsdead_def[2].','.$gal
                    .','.$unitssend_def[3].','.$unitsdead_def[3].','.$nat
                    .','.$unitssend_def[4].','.$unitsdead_def[4].','.$natar
                    .','.$unitssend_def[5].','.$unitsdead_def[5]
                    .','.$hun.','.$unitssend_def[6].','.$unitsdead_def[6]
                    .','.$egy.','.$unitssend_def[7].','.$unitsdead_def[7]
                    .','.$spa.','.$unitssend_def[8].','.$unitsdead_def[8]
                    .','.$vik.','.$unitssend_def[9].','.$unitsdead_def[9]
                    .','.$DefenderHeroesTot.','.$DefenderHeroesDead
                    .','.$info_ram.','.$info_cat.','.$info_chief.','.(isset($info_spy) ? $info_spy : '')
                    .',,'.$data['t11'].','.$dead11.','.$herosend_def.','.$deadhero
                    .','.$unitstraped_att;

            $data2 .= ','.($info_trap !== '' ? addslashes($info_trap) : '').',,'.$info_troop.','.$info_hero;
        }

        $data_fail = ''.$from['owner'].','.$from['wref'].','.$owntribe.','.$unitssend_att.','.$unitsdead_att
                    .','.$steal[0].','.$steal[1].','.$steal[2].','.$steal[3].','.$battlepart['bounty']
                    .','.$to['owner'].','.$to['wref'].','.addslashes($to['name'])
                    .',,,,'.$targettribe
                    .','.$unitssend_deff[0].','.$unitsdead_deff[0].','.$rom
                    .','.$unitssend_deff[1].','.$unitsdead_deff[1].','.$ger
                    .','.$unitssend_deff[2].','.$unitsdead_deff[2].','.$gal
                    .','.$unitssend_deff[3].','.$unitsdead_deff[3].','.$nat
                    .','.$unitssend_deff[4].','.$unitsdead_deff[4].','.$natar
                    .','.$unitssend_deff[5].','.$unitsdead_deff[5]
                    .','.$hun.','.$unitssend_deff[6].','.$unitsdead_deff[6]
                    .','.$egy.','.$unitssend_deff[7].','.$unitsdead_deff[7]
                    .','.$spa.','.$unitssend_deff[8].','.$unitsdead_deff[8]
                    .','.$vik.','.$unitssend_deff[9].','.$unitsdead_deff[9]
                    .','.$DefenderHeroesTot.','.$DefenderHeroesDead
                    .',,,'.$data['t11'].','.$dead11.','.$unitstraped_att
                    .',,'.$info_ram.','.$info_cat.','.$info_chief.','.$info_troop.','.$info_hero;

        return ['data2' => $data2, 'data_fail' => $data_fail];
    }

    /**
     * Distribute the battle bounty across the resources actually available in
     * the target (after cranny protection) and return how much of each is taken.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $available [wood, clay, iron, crop] lootable amounts.
     * @param int   $bounty    Total carry capacity / bounty from the battle.
     * @return array [wood, clay, iron, crop] amounts actually stolen.
     */
    private function applyBounty(array $available, $bounty) {
        $avtotal = $available;
        $steal   = [0, 0, 0, 0];
        $btotal  = $bounty;
        $bmod    = 0;

        for ($i = 0; $i < 5; $i++) {
            for ($j = 0; $j < 4; $j++) {
                if (isset($avtotal[$j])) {
                    if ($avtotal[$j] < 1) unset($avtotal[$j]);
                }
            }

            // No resources left to take
            if (empty($avtotal) || ($btotal < 1 && $bmod < 1)) break;

            if ($btotal < 1) {
                while ($bmod) {
                    // random select
                    $rs = array_rand($avtotal);
                    if (isset($avtotal[$rs])) {
                        $avtotal[$rs] -= 1;
                        $steal[$rs]   += 1;
                        $bmod         -= 1;
                    }
                }
            }

            // handle unbalanced amounts.
            $btotal += $bmod;
            $bmod    = $btotal % count($avtotal);
            $btotal -= $bmod;
            $bsplit  = $btotal / count($avtotal);

            $max_steal = (min($avtotal) < $bsplit) ? min($avtotal) : $bsplit;

            for ($j = 0; $j < 4; $j++) {
                if (isset($avtotal[$j])) {
                    $avtotal[$j] -= $max_steal;
                    $steal[$j]   += $max_steal;
                    $btotal      -= $max_steal;
                }
            }
        }

        return $steal;
    }

    /**
     * Apply battle casualties to the defender's own (in-village) troops, persist
     * the losses, and return the per-unit dead map used later for points/reports.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $data        Current attack row (uses 'to').
     * @param int   $targettribe Defender tribe (1-5).
     * @param array $battlepart  Battle result (index 2 = defender kill ratio,
     *                           'deadherodef' = defender hero losses).
     * @return array Map of dead own troops: keys are the unit index (int) plus 'hero'.
     */
    private function applyOwnDefenceCasualties($data, $targettribe, $battlepart) {
        global $database;

        $owndead = [];
        $unitlist = $database->getUnit($data['to'], false);
        $start = ($targettribe - 1) * 10 + 1;
        $end = ($targettribe * 10);

        $unitModifications_units = [];
        $unitModifications_amounts = [];
        $unitModifications_modes = [];
        for ($i = $start; $i <= $end; $i++) {
            if ($unitlist) {
                $owndead[$i] = round($battlepart[2] * $unitlist['u'.$i]);
                $unitModifications_units[] = $i;
                $unitModifications_amounts[] = $owndead[$i];
                $unitModifications_modes[] = 0;
            }
        }

        $owndead['hero'] = 0;

        if ($unitlist) {
            $owndead['hero'] = (isset($battlepart['deadherodef']) ? $battlepart['deadherodef'] : '');

            /**
             * BUG FIXED (issue #379): units.hero ended up at -1.
             *
             * The value is still reported (it feeds the battle report and the
             * points), but it must NOT be subtracted from units.hero here any
             * more. Battle::applyHeroBattleDamage() already clears the column
             * itself ("UPDATE units u JOIN vdata v SET u.hero = 0 WHERE
             * v.owner = <hero owner>") the moment the hero dies, and that runs
             * inside calculateBattle(), i.e. BEFORE this method.
             *
             * So the column went 1 -> 0 there and 0 - 1 -> -1 here: the
             * village kept showing "-1 Hero" in the troop list while the
             * Hero's Mansion correctly offered a revive.
             *
             * One writer only: the death is owned by applyHeroBattleDamage(),
             * which is also the only place that knows about the hero's other
             * possible locations (reinforcements, another village).
             */
        }

        // modify units in DB
        $database->modifyUnit($data['to'], $unitModifications_units, $unitModifications_amounts, $unitModifications_modes);

        // Spital: o parte din mortii proprii (aparare) devin raniti - doar unitatile de lupta X1-X8
        $woundedMap = [];
        for($wi = $start; $wi <= $start + 7; $wi++) {
            if(!empty($owndead[$wi])) $woundedMap[$wi] = $owndead[$wi];
        }
        $this->applyHospitalWounded($data['to'], $woundedMap);

        return $owndead;
    }

    /**
     * Apply battle casualties to the reinforcement (foreign) troops stationed in
     * the defender's village: compute each reinforcement's dead units/hero, persist
     * them (modifyEnforce), notify the reinforcing players, and delete fully-wiped
     * reinforcements. Accumulates the dead counts into $alldead and the dead heroes
     * into $DefenderHeroesDeadArray (both passed by reference), and reports which
     * tribes are present among the reinforcements.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $data                    Current attack row (uses 'to').
     * @param array $battlepart              Battle result (index 2 = defender kill ratio,
     *                                        'deadheroref' = dead hero per enforce id).
     * @param array $from                    Attacker village info (uses 'wref').
     * @param array $to                      Defender village info (uses 'name','wref').
     * @param int   $ownally                 Attacker's alliance id (for notices).
     * @param int   $AttackArrivalTime       Timestamp stamped on the reinforcement notices.
     * @param mixed $scout                    Scout marker; notices/deletion only fire when empty.
     * @param array $alldead                 [in/out] Per-unit dead reinforcements (+ 'hero'), accumulated.
     * @param array $DefenderHeroesDeadArray [in/out] Dead reinforcement heroes per tribe, accumulated.
     * @return array Tribe-presence flags among the reinforcements: keys rom,ger,gal,nat,natar (0/1).
     */
    private function applyReinforcementCasualties($data, $battlepart, $from, $to, $ownally, $AttackArrivalTime, $scout, array &$alldead, array &$DefenderHeroesDeadArray) {
        global $database;

        $rom = $ger = $gal = $nat = $natar = $hun = $egy = $spa = $vik = 0;

        //kill other defence in village
        // ... once again, units could have changed, so we need to reselect
        $enforcementarray3 = $database->getEnforceVillage($data['to'], 0);
        foreach ($enforcementarray3 as $enforce) {
            $life = ''; $notlife = ''; $wrong = false;
            if ($enforce['from'] != 0) {
                $tribe = $database->getUserArray($database->getVillageField($enforce['from'], "owner"), 1)["tribe"];
            } else {
                $tribe = 4;
            }

            $start = ($tribe - 1) * 10 + 1;
            $end = ($tribe * 10);
            unset($dead);

            switch ($tribe) {
                case 1: $rom = 1; break;
                case 2: $ger = 1; break;
                case 3: $gal = 1; break;
                case 4: $nat = 1; break;
                case 6: $hun = 1; break;
                case 7: $egy = 1; break;
                case 8: $spa = 1; break;
                case 9: $vik = 1; break;
                case 5:
                default: $natar = 1; break;
            }

            $enforceModificationsById = [];
            for ($i = $start; $i <= $end; $i++) {
                if ($enforce['u'.$i] > '0') {
                    if (!isset($enforceModificationsById[$enforce['id']])) {
                        $enforceModificationsById[$enforce['id']] = [
                            'units' => [],
                            'amounts' => [],
                            'modes' => []
                        ];
                    }
                    $enforceModificationsById[$enforce['id']]['units'][] = $i;
                    $enforceModificationsById[$enforce['id']]['amounts'][] = (isset($battlepart[2]) ? round($battlepart[2]*$enforce['u'.$i]) : 0);
                    $enforceModificationsById[$enforce['id']]['modes'][] = 0;
                    $dead[$i] = (isset($battlepart[2]) ? round($battlepart[2]*$enforce['u'.$i]) : 0);
                    $checkpoint = (isset($battlepart[2]) ? round($battlepart[2]*$enforce['u'.$i]) : 0);

                    if (!isset($enforce['u'.$i])) $enforce['u'.$i] = 0;

                    $alldead[$i] += $dead[$i];

                    $wrong = $checkpoint != $enforce['u'.$i];
                } else {
                    $dead[$i] = 0;
                }
            }

            if ($enforce['hero'] > 0) {
                /**
                 * BUG FIXED (issue #379), same double subtraction as in
                 * applyOwnDefenceCasualties(): a reinforcement hero who died
                 * was already removed by Battle::applyHeroBattleDamage()
                 * ("UPDATE enforcement e JOIN vdata v SET e.hero = 0"), so
                 * subtracting him again here drove enforcement.hero to -1.
                 *
                 * $enforce still reads 1 because getEnforceVillage() answers
                 * from the request cache filled before the battle, so the
                 * stale row hid the problem instead of preventing it.
                 *
                 * The dead count below is kept: it is what the reinforcement
                 * report and $wrong (which decides whether the wiped-out
                 * reinforcement row is deleted) are built from.
                 */
                $dead['hero'] = $battlepart['deadheroref'][$enforce['id']];
                $alldead['hero'] += $dead['hero'];
                $wrong = $dead['hero'] != $enforce['hero'];

                //Collecting information for the report
                $reinfTribe = ($enforce['from'] == 0) ? 4 : $database->getUserField($database->getVillageField($enforce['from'], "owner"), "tribe", 0);
                $DefenderHeroesDeadArray[$reinfTribe] += $dead['hero'];
            }

            // modify enforce in DB
            foreach ($enforceModificationsById as $enforceId => $enforceArray) {
                $database->modifyEnforce($enforceId, $enforceArray['units'], $enforceArray['amounts'], $enforceArray['modes']);
            }

            $notlife = ''.$dead[$start].','.$dead[$start+1].','.$dead[$start+2].','.$dead[$start+3].','.$dead[$start+4].','.$dead[$start+5].','.$dead[$start+6].','.$dead[$start+7].','.$dead[$start+8].','.$dead[$start+9].'';
            $notlife1 = $dead[$start]+$dead[$start+1]+$dead[$start+2]+$dead[$start+3]+$dead[$start+4]+$dead[$start+5]+$dead[$start+6]+$dead[$start+7]+$dead[$start+8]+$dead[$start+9];
            $life = ''.$enforce['u'.$start.''].','.$enforce['u'.($start+1).''].','.$enforce['u'.($start+2).''].','.$enforce['u'.($start+3).''].','.$enforce['u'.($start+4).''].','.$enforce['u'.($start+5).''].','.$enforce['u'.($start+6).''].','.$enforce['u'.($start+7).''].','.$enforce['u'.($start+8).''].','.$enforce['u'.($start+9).''].'';
            $life1 = $enforce['u'.$start.'']+$enforce['u'.($start+1).'']+$enforce['u'.($start+2).'']+$enforce['u'.($start+3).'']+$enforce['u'.($start+4).'']+$enforce['u'.($start+5).'']+$enforce['u'.($start+6).'']+$enforce['u'.($start+7).'']+$enforce['u'.($start+8).'']+$enforce['u'.($start+9).''];
            $lifehero = (isset($enforce['hero']) ? $enforce['hero'] : 0);
            $notlifehero = (isset($dead['hero']) ? $dead['hero'] : 0);
            $totallife = (isset($enforce['hero']) ? $enforce['hero'] : 0)+$life1;
            $totalnotlife = (isset($dead['hero']) ? $dead['hero'] : 0)+$notlife1;
            //NEED TO SEND A RAPPORTAGE!!!
            $data2 = ''.$database->getVillageField($enforce['from'], "owner").','.$to['wref'].','.addslashes($to['name']).','.$tribe.','.$life.','.$notlife.','.$lifehero.','.$notlifehero.','.$enforce['from'].'';
            if (empty($scout)) {
                if ($totalnotlife == 0) {
                    $database->addNotice($database->getVillageField($enforce['from'], "owner"), $from['wref'], $ownally, 15, 'Reinforcement in '.addslashes($to['name']).' was attacked', $data2, $AttackArrivalTime);
                } else if ($totallife > $totalnotlife) {
                    $database->addNotice($database->getVillageField($enforce['from'], "owner"), $from['wref'], $ownally, 16, 'Reinforcement in '.addslashes($to['name']).' was attacked', $data2, $AttackArrivalTime);
                } else {
                    $database->addNotice($database->getVillageField($enforce['from'], "owner"), $from['wref'], $ownally, 17, 'Reinforcement in '.addslashes($to['name']).' was attacked', $data2, $AttackArrivalTime);
                }
                //delete reinf sting when its killed all.
                if (!$wrong) $database->deleteReinf($enforce['id']);
            }
        }

        return ['rom' => $rom, 'ger' => $ger, 'gal' => $gal, 'nat' => $nat, 'natar' => $natar, 'hun' => $hun, 'egy' => $egy, 'spa' => $spa, 'vik' => $vik];
    }

    /**
     * Collect the defender-side report data BEFORE casualties are applied: reset and
     * rebuild the reinforcement unit totals ($DefenderEnf) and the per-tribe hero
     * totals from a fresh re-select, fold reinforcement heroes into the defender's
     * hero count, build the "units sent" report rows (own troops + reinforcements,
     * plus their masked variants), and (re)initialise the per-tribe dead-hero
     * accumulator to zero. Returns everything the caller needs downstream.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $data        Current attack row (uses 'to').
     * @param int   $targettribe Defender tribe (1-5).
     * @param array $Defender    Defender unit array (own troops + 'hero'); passed by value,
     *                           its 'hero' is folded with reinforcement heroes and returned
     *                           via the 'defenderHero' key.
     * @return array Report bundle: DefenderEnf, DefenderHeroesTotArray,
     *               DefenderHeroesDeadArray, unitssend_def, unitssend_deff,
     *               totalsend_alldef, defenderHero.
     */
    private function collectReinforcementReport($data, $targettribe, $Defender) {
        global $database;

        $DefenderEnf = [];
        $DefenderHeroesTotArray = [];
        $DefenderHeroesDeadArray = [];
        $unitssend_def = [];
        $unitssend_deff = [];

        //Resetting the enforcement arrays
        for ($i = 1; $i <= 90; $i++) {
            $DefenderEnf['u'.$i] = 0;
            if ($i <= 9) {
                $DefenderHeroesTotArray[$i] = 0;
                $DefenderHeroesDeadArray[$i] = 0;
            }
        }

        // our reinforcements count could have changed at this point, thus the re-select
        $enforcementarray2 = $database->getEnforceVillage($data['to'], 0);
        if (count($enforcementarray2) > 0) {
            foreach ($enforcementarray2 as $enforce2) {
                for ($i = 1; $i <= 90; $i++) {
                    $DefenderEnf['u'.$i] += $enforce2['u'.$i];
                }

                //Divide heroes by tribe
                if ($enforce2['hero'] > 0) {
                    $reinfTribe = ($enforce2['from'] == 0) ? 4 : $database->getUserField($database->getVillageField($enforce2['from'], "owner"), "tribe", 0);
                    $DefenderHeroesTotArray[$reinfTribe] += $enforce2['hero'];
                    $Defender['hero'] += $enforce2['hero'];
                }
            }
        }

        $totalsend_alldef = 0;

        //Own troops
        $ownTroops = array_slice($Defender, ($targettribe - 1) * 10 + 1, 10);
        $totalsend_alldef = array_sum($ownTroops);

        //Collecting informations for the report
        $unitssend_def[0] = implode(",", $ownTroops);
        $unitssend_deff[0] = '?,?,?,?,?,?,?,?,?,?,';

        for ($i = 1; $i <= 9; $i++) {
            //Reinforcements
            $reinfTroops = array_slice($DefenderEnf, ($i - 1) * 10, 10);
            $totalsend_alldef += array_sum($reinfTroops);

            //Collecting informations for the report
            $unitssend_def[$i] = implode(",", $reinfTroops);
            $unitssend_deff[$i] = $unitssend_deff[0];
        }
        $totalsend_alldef += $Defender['hero'];

        return [
            'DefenderEnf' => $DefenderEnf,
            'DefenderHeroesTotArray' => $DefenderHeroesTotArray,
            'DefenderHeroesDeadArray' => $DefenderHeroesDeadArray,
            'unitssend_def' => $unitssend_def,
            'unitssend_deff' => $unitssend_deff,
            'totalsend_alldef' => $totalsend_alldef,
            'defenderHero' => $Defender['hero'],
        ];
    }

    /**
     * Compute attacker/defender total populations and fetch their village lists.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $to      Defender village info (getMInfo/getOMInfo).
     * @param array $from    Attacker village info (getMInfo).
     * @param int   $isoasis Whether the target is an oasis (defender pop counts only for villages).
     * @param int   $defpop  Initial defender population (accumulated onto).
     * @param int   $attpop  Initial attacker population (accumulated onto).
     * @return array{varray:array,varray1:array,defpop:int,attpop:int}
     */
    private function calculatePopulations($to, $from, $isoasis, $defpop, $attpop) {
        global $database;

        $varray = $database->getProfileVillages($to['owner'], 0, false);

        if ($to['owner'] == $from['owner']) $varray1 = $varray;
        else $varray1 = $database->getProfileVillages($from['owner'], 0, false);

        // total population of the defender
        if($isoasis == 0){
            foreach($varray as $defenderVillage) $defpop += $defenderVillage['pop'];
        }

        // total population of the attacker
        foreach($varray1 as $attackerVillage) $attpop += $attackerVillage['pop'];

        return [
            'varray'  => $varray,
            'varray1' => $varray1,
            'defpop'  => $defpop,
            'attpop'  => $attpop,
        ];
    }

    /**
     * Resolve how many incoming attacker units get caught in the defender's
     * traps (Gaul trapper / Natar capital), update the trap counters and the
     * prisoners table, and subtract the trapped troops from the attacking army.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $data         Current attack row.
     * @param array $Defender     Defender unit counts (reads u99 / u99o).
     * @param array $Attacker     Attacker units (u<start..end> + uhero).
     * @param bool  $NatarCapital True when the target is a Natar capital (all troops trapped).
     * @param int   $scout        1 for a scouting attack (no trapping).
     * @param int   $start        First unit index of the attacker tribe.
     * @param int   $end          Last unit index of the attacker tribe.
     * @return array {
     *     traped:          int[] trapped count per unit slot (1..11),
     *     totaltraped_att: int   total trapped troops,
     *     Attacker:        array attacker units with trapped troops removed
     * }
     */
    private function calculateTrappedUnits($data, $Defender, $Attacker, $NatarCapital, $scout, $start, $end) {
        global $database;

        $traped = [];
        for($i = 1; $i <= 11; $i++) $traped[$i] = 0;
        $totaltraped_att = 0;

        //impossible to attack or scout NATAR Capital Village
        if ($NatarCapital){
            for($i = 1; $i <= 11; $i++) $traped[$i] = $data['t'.$i];
        }
        elseif(empty($scout))
        {
            $traps = max($Defender['u99'] - $Defender['u99o'], 0);

            $totalTroops = 0;
            for($i = 1; $i <= 11; $i++) $totalTroops += $data['t'.$i];

            if($traps >= $totalTroops){
                for($i = 1; $i <= 11; $i++) $traped[$i] = $data['t'.$i];
            }
            else if($totalTroops > 0)
            {
                $multiplier = $traps / $totalTroops;

                //The hero is excluded, because it can be only trapped if traps > totalTroops
                for($i = 1; $i <= 10; $i++){
                    $trappedUnits = intval($data['t'.$i] * $multiplier);
                    $traped[$i] = $trappedUnits;
                    $traps -= $trappedUnits;
                }

                while($traps > 0){
                    //There are some traps left, let's distribute them
                    for($i = 1; $i <= 10 && $traps > 0; $i++){
                        if($data['t'.$i] != 0){
                            $traped[$i]++;
                            $traps--;
                        }
                    }
                }
            }
            else {
                for($i = 1; $i <= 11; $i++) $traped[$i] = 0;
            }
        }

        if(empty($scout) || $NatarCapital){
            for ($i = 1; $i <= 11; $i++){
                $totaltraped_att += $traped[$i];
            }

            $database->modifyUnit($data['to'], ["99o"], [$totaltraped_att], [1]);

            for($i = $start; $i <= $end; $i++){
                $j = $i-$start+1;
                $Attacker['u'.$i] -= $traped[$j];
            }
            $Attacker['uhero'] -= $traped[11];

            if($totaltraped_att > 0){
                $prisoners2 = $database->getPrisoners2($data['to'], $data['from'], false);
                if(empty($prisoners2)){
                    $database->addPrisoners($data['to'],$data['from'],$traped[1],$traped[2],$traped[3],$traped[4],$traped[5],$traped[6],$traped[7],$traped[8],$traped[9],$traped[10],$traped[11]);
                }else{
                    $database->updatePrisoners($data['to'],$data['from'],$traped[1],$traped[2],$traped[3],$traped[4],$traped[5],$traped[6],$traped[7],$traped[8],$traped[9],$traped[10],$traped[11]);
                }
            }
        }

        return [
            'traped'          => $traped,
            'totaltraped_att' => $totaltraped_att,
            'Attacker'        => $Attacker,
        ];
    }

    /**
     * Apply ram damage to the defender's wall during a normal attack (type 3):
     * compute the new wall level, update it in the database (recounting the
     * village population if the wall is destroyed), build the report fragment,
     * and recalculate the battle when the wall level changed.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array  $data       Current attack row (uses t7, to).
     * @param int    $walllevel  Current wall level.
     * @param int    $wallid     Wall building field id (40).
     * @param int    $ram_pic    Ram unit id (for the report fragment).
     * @param array  $battlepart Battle result (provides the ram damage factors,
     *                           and the hero verdict carried over to the recalculation).
     * @param array  $ctx        Battle context for the post-damage recalculation
     *                           (Attacker, Defender, tribes, populations, AB tech, ...).
     * @return array { battlepart: array (possibly recalculated), info_ram: string }
     */
    private function applyRamDamage($data, $walllevel, $wallid, $ram_pic, $battlepart, array $ctx) {
        global $database, $battle;

        $info_ram = "".$ram_pic.",".rc_tok('RC_NO_WALL');

        if($walllevel > 0){
            $ramsDamage = $battle->calculateCatapultsDamage($data['t7'],
                                                            $battlepart['rams']['upgrades'],
                                                            $battlepart['rams']['durability'],
                                                            $battlepart['rams']['attackDefenseRatio'],
                                                            $battlepart['rams']['strongerBuildings'],
                                                            $battlepart['rams']['moraleBonus']);
            $newLevel = $battle->calculateNewBuildingLevel($walllevel, $ramsDamage);

            if ($newLevel == 0){
                $info_ram = "".$ram_pic.",".rc_tok('RC_WALL_DESTROYED');
                $database->setVillageLevel($data['to'], ["f".$wallid, "f".$wallid."t"], [0, 0]);
                $this->recountPop($data['to']);
            }elseif ($newLevel == $walllevel){
                $info_ram = "".$ram_pic.",".rc_tok('RC_WALL_NOT_DAMAGED');
            }else{
                $info_ram = "".$ram_pic.",".rc_tok('RC_WALL_DAMAGED_FROM_TO', $walllevel, $newLevel);
                $database->setVillageLevel($data['to'],"f".$wallid."",$newLevel);
            }

            //If the wall got damaged/destroyed during the attack
            //we need to recalculate the whole battle
            if($newLevel != $walllevel){
                // The heroes already took their damage during the first pass and
                // it was written to the hero table, so the recalculation must not
                // apply it again: it would look up living heroes only, miss the
                // one who just died and drop him from the report (issue #372).
                // The first verdict is handed over instead.
                $battlepart = $battle->calculateBattle($ctx['Attacker'], $ctx['Defender'], $newLevel, $ctx['att_tribe'], $ctx['def_tribe'], $ctx['residence'], $ctx['attpop'], $ctx['defpop'], $ctx['type'], $ctx['def_ab'], $ctx['att_ab1'], $ctx['att_ab2'], $ctx['att_ab3'], $ctx['att_ab4'], $ctx['att_ab5'], $ctx['att_ab6'], $ctx['att_ab7'], $ctx['att_ab8'], $ctx['tblevel'], $ctx['stonemason'], $newLevel, 0, 0, 0, $ctx['AttackerID'], $ctx['DefenderID'], $ctx['AttackerWref'], $ctx['DefenderWref'], $ctx['conqureby'], $ctx['enforcementarray'], $battlepart);
            }
        }

        return ['battlepart' => $battlepart, 'info_ram' => $info_ram];
    }

    /**
     * Build the attacking army for the current attack: per-slot unit counts
     * (u<start..end> + uhero) plus the catapult / ram / chief / scout unit ids
     * used in the battle report. Oasis attacks include the Nature siege/chief
     * slots (37/38/39). Pure behaviour-preserving extraction (issue #155).
     *
     * @param array $attackRow The attack row (t1..t11, tribe-relative).
     * @param int   $owntribe  Attacker tribe (1..5).
     * @param int   $isoasis   0 for a village target, otherwise an oasis.
     * @return array { Attacker, start, end, u, catp_pic, ram_pic, chief_pic, spy_pic, hero_pic }
     */
    private function buildAttackerUnits(array $attackRow, $owntribe, $isoasis) {
        $Attacker = [];
        $start = ($owntribe - 1) * 10 + 1;
        $end   = $owntribe * 10;
        $u     = ($owntribe - 1) * 10;

        if ($isoasis == 0) {
            $catapult = [8, 18, 28, 48, 58, 68, 78, 88];
            $ram      = [7, 17, 27, 47, 57, 67, 77, 87];
            $chief    = [9, 19, 29, 49, 59, 69, 79, 89];
        } else {
            $catapult = [8, 18, 28, 38, 48, 58, 68, 78, 88];
            $ram      = [7, 17, 27, 37, 47, 57, 67, 77, 87];
            $chief    = [9, 19, 29, 39, 49, 59, 69, 79, 89];
        }
        $spys = [4, 14, 23, 44, 52, 64, 74, 82];

        $catp_pic = $ram_pic = $chief_pic = $spy_pic = null;

        for ($i = $start; $i <= $end; $i++) {
            $y = $i - $u;
            $Attacker['u'.$i] = $attackRow['t'.$y];
            //there are catas
            if (in_array($i, $catapult)) $catp_pic = $i;
            if (in_array($i, $ram))      $ram_pic = $i;
            if (in_array($i, $chief))    $chief_pic = $i;
            if (in_array($i, $spys))     $spy_pic = $i;
        }
        $Attacker['uhero'] = $attackRow['t11'];

        return [
            'Attacker'  => $Attacker,
            'start'     => $start,
            'end'       => $end,
            'u'         => $u,
            'catp_pic'  => $catp_pic,
            'ram_pic'   => $ram_pic,
            'chief_pic' => $chief_pic,
            'spy_pic'   => $spy_pic,
            'hero_pic'  => 'hero',
        ];
    }

    /**
     * Gather the defender's units for the current attack: the village's own
     * troops (normalised to non-negative ints) plus the aggregated reinforcement
     * totals, and the raw reinforcement rows. Used for both village and oasis
     * targets. Pure behaviour-preserving extraction (issue #155).
     *
     * @param int $wref Defending village/oasis wref.
     * @return array { Defender, enforDefender, enforcementarray }
     */
    private function buildDefenderUnits($wref) {
        global $database;

        $enforDefender = [];
        $Defender = $database->getUnit($wref, false);
        $enforcementarray = $database->getEnforceVillage($wref, 0);

        if (count($enforcementarray) > 0) {
            foreach ($enforcementarray as $enforce) {
                for ($i = 1; $i <= 90; $i++) {
                    if (!isset($enforDefender['u'.$i])) {
                        $enforDefender['u'.$i] = 0;
                    }
                    $enforDefender['u'.$i] += $enforce['u'.$i];
                }
                if (!isset($enforDefender['hero'])) {
                    $enforDefender['hero'] = 0;
                }
                $enforDefender['hero'] += $enforce['hero'];
            }
        }

        for ($i = 1; $i <= 90; $i++) {
            if (!isset($Defender['u'.$i]) || empty($Defender['u'.$i]) || $Defender['u'.$i] < 0) {
                $Defender['u'.$i] = 0;
            }
        }

        if (!isset($Defender['hero']) || empty($Defender['hero']) || $Defender['hero'] < 0) {
            $Defender['hero'] = 0;
        }

        return [
            'Defender'         => $Defender,
            'enforDefender'    => $enforDefender,
            'enforcementarray' => $enforcementarray,
        ];
    }

    /**
     * Resolve the per-attack context that does not depend on the target type:
     * the attacker village/owner data (tribe, alliance), the war references and
     * a few base flags. Read-only — no DB writes.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $data Current attack row.
     * @return array {isoasis, AttackArrivalTime, AttackerWref, DefenderWref,
     *                NatarCapital, AttackerID, owntribe, ownally, from, fromF}
     */
    private function resolveAttackContext($data) {
        global $database;

        $owner = $this->getCachedUser($database->getVillageField($data['from'], "owner"), 1);

        return [
            'isoasis'           => $data['oasistype'],
            'AttackArrivalTime' => $data['endtime'],
            'AttackerWref'      => $data['from'],
            'DefenderWref'      => $data['to'],
            'NatarCapital'      => false,
            'AttackerID'        => $owner['id'],
            'owntribe'          => $owner['tribe'],
            'ownally'           => $owner['alliance'],
            'from'              => $database->getMInfo($data['from']),
            'fromF'             => $database->getVillage($data['from']),
        ];
    }

    /**
     * Resolve the defender target context for a VILLAGE attack: target owner
     * (tribe/alliance), map info, conquest flag, evasion inputs and the battle
     * environment (wall, armory/blacksmith tech, residence, siege masonry).
     * Read-only — no DB writes.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $data      Current attack row.
     * @param array $dataarray Full batch of completed attacks.
     * @param int   $data_num  Index of $data within $dataarray.
     * @param int   $owntribe  Attacker tribe.
     * @return array Target + battle-environment context.
     */
    private function resolveVillageTarget($data, $dataarray, $data_num, $owntribe) {
        global $database, $units;

        $DefenderUserData = $this->getCachedUser($database->getVillageField($data['to'],"owner"),1);
        // BUG FIXED: when the target village was razed mid-batch (issue #298 scenario),
        // getVillageField(...,"owner") returns 0 -> getCachedUser(0) finds no user ->
        // $DefenderUserData is null, and getVillage() below also returns null (no vdata
        // row). Every read from them then threw "Trying to access array offset on null".
        // The razed-target bounce-home check downstream (empty($to['wref'])) already
        // handles this case correctly - it just needs to survive getting there without
        // warnings, so null-coalesce each read here; values stay null exactly as before.
        $DefenderID = $DefenderUserData["id"] ?? null;
        $targettribe = $DefenderUserData["tribe"] ?? null;
        $targetally = $DefenderUserData["alliance"] ?? null;
        $to = $database->getMInfo($data['to']);
        $toF = $database->getVillage($data['to']);
        $conqureby = 0;
        $NatarCapital = (($toF['owner'] ?? null) == 3 && ($toF['capital'] ?? null) == 1);
        if(!isset($to['name']) || empty($to['name'])) $to['name'] = "[?]";

        $DefenderUnit = $database->getUnit($data['to']);
        $evasion = $toF["evasion"] ?? null;
        $maxevasion = $DefenderUserData["maxevasion"] ?? null;
        $gold = $DefenderUserData["gold"] ?? null;
        $cannotsend = false;

        $movements = $database->getMovement(34, $data['to'], 1);
        for($y = 0; $y < count($movements); $y++){
            if(property_exists($units, $y)){
                $returntime = $units->$y['endtime'] - time();
                if($units->$y['sort_type'] == 4 && $units->$y['from'] != 0 && $returntime <= 10){
                    $cannotsend = true;
                }
            }
        }

        //need to set these variables.
        $def_wall = $database->getFieldLevel($data['to'], 40, false);
        $att_tribe = $owntribe;
        $def_tribe = $targettribe;
        $attpop = $defpop = $residence = 0;
        $def_ab = [];

        //get level of palace or residence
        $residence = $database->getFieldLevelInVillage($data['to'], '25, 26', false);

        //type of attack
        $type = $dataarray[$data_num]['attack_type'];
        $scout = ($type == 1) ? 1 : 0;

        $ud = ($def_tribe - 1) * 10;
        $att_ab = $database->getABTech($data['from']); // Blacksmith level
        $att_ab1 = $att_ab['b1'];
        $att_ab2 = $att_ab['b2'];
        $att_ab3 = $att_ab['b3'];
        $att_ab4 = $att_ab['b4'];
        $att_ab5 = $att_ab['b5'];
        $att_ab6 = $att_ab['b6'];
        $att_ab7 = $att_ab['b7'];
        $att_ab8 = $att_ab['b8'];
        $armory = $database->getABTech($data['to']); // Armory level
        $def_ab[$ud + 1] = $armory['a1'];
        $def_ab[$ud + 2] = $armory['a2'];
        $def_ab[$ud + 3] = $armory['a3'];
        $def_ab[$ud + 4] = $armory['a4'];
        $def_ab[$ud + 5] = $armory['a5'];
        $def_ab[$ud + 6] = $armory['a6'];
        $def_ab[$ud + 7] = $armory['a7'];
        $def_ab[$ud + 8] = $armory['a8'];

        //rams attack
        $walllevel = 0;
        $wallid = 0;
        if (($data['t7']) > 0 && $type == 3) {
            $basearraywall = $to;
            if (($walllevel = $database->getFieldLevel($basearraywall['wref'], 40, false)) > 0){
                $wallid = 40;
            }
        }

        $tblevel = 1;
        $stonemason = $database->getFieldLevelInVillage($data['to'], 34);

        return [
            'DefenderID' => $DefenderID, 'targettribe' => $targettribe, 'targetally' => $targetally,
            'to' => $to, 'toF' => $toF, 'conqureby' => $conqureby, 'NatarCapital' => $NatarCapital,
            'DefenderUnit' => $DefenderUnit, 'evasion' => $evasion, 'maxevasion' => $maxevasion,
            'gold' => $gold, 'cannotsend' => $cannotsend,
            'def_wall' => $def_wall, 'att_tribe' => $att_tribe, 'def_tribe' => $def_tribe,
            'attpop' => $attpop, 'defpop' => $defpop, 'residence' => $residence, 'def_ab' => $def_ab,
            'type' => $type, 'scout' => $scout,
            'att_ab1' => $att_ab1, 'att_ab2' => $att_ab2, 'att_ab3' => $att_ab3, 'att_ab4' => $att_ab4,
            'att_ab5' => $att_ab5, 'att_ab6' => $att_ab6, 'att_ab7' => $att_ab7, 'att_ab8' => $att_ab8,
            'walllevel' => $walllevel, 'wallid' => $wallid,
            'tblevel' => $tblevel, 'stonemason' => $stonemason,
        ];
    }

    /**
     * Resolve the defender target context for an OASIS attack: target owner
     * (tribe/alliance), oasis map info, conquest flag and the (mostly fixed)
     * battle environment. Read-only — no DB writes.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $data      Current attack row.
     * @param array $dataarray Full batch of completed attacks.
     * @param int   $data_num  Index of $data within $dataarray.
     * @param int   $owntribe  Attacker tribe.
     * @return array Target + battle-environment context.
     */
    private function resolveOasisTarget($data, $dataarray, $data_num, $owntribe) {
        global $database;

        $DefenderUserData = $this->getCachedUser($database->getOasisField($data['to'], "owner"),1);
        $DefenderID = $DefenderUserData["id"];
        $targettribe = $DefenderUserData["tribe"];
        $targetally = $DefenderUserData["alliance"];
        $to = $database->getOMInfo($data['to']);
        $toF = $database->getOasisV($data['to']);
        $conqureby = $toF['conqured'];

        //need to set these variables.
        $def_wall = $residence = $attpop = 0;
        $att_tribe = $owntribe;
        $def_tribe = $targettribe;
        $defpop = 500;

        //type of attack
        $type = $dataarray[$data_num]['attack_type'];
        $scout = ($type == 1) ? 1 : 0;

        $att_ab1 = $att_ab2 = $att_ab3 = $att_ab4 = $att_ab5 = $att_ab6 = $att_ab7 = $att_ab8 = 0;
        $def_ab = [];
        $def_ab[31] = $def_ab[32] = $def_ab[33] = $def_ab[34] = $def_ab[35] = $def_ab[36] = $def_ab[37] = $def_ab[38] = 0;

        $walllevel = $tblevel = $stonemason = 0;

        return [
            'DefenderID' => $DefenderID, 'targettribe' => $targettribe, 'targetally' => $targetally,
            'to' => $to, 'toF' => $toF, 'conqureby' => $conqureby,
            'def_wall' => $def_wall, 'att_tribe' => $att_tribe, 'def_tribe' => $def_tribe,
            'attpop' => $attpop, 'defpop' => $defpop, 'residence' => $residence, 'def_ab' => $def_ab,
            'type' => $type, 'scout' => $scout,
            'att_ab1' => $att_ab1, 'att_ab2' => $att_ab2, 'att_ab3' => $att_ab3, 'att_ab4' => $att_ab4,
            'att_ab5' => $att_ab5, 'att_ab6' => $att_ab6, 'att_ab7' => $att_ab7, 'att_ab8' => $att_ab8,
            'walllevel' => $walllevel, 'tblevel' => $tblevel, 'stonemason' => $stonemason,
        ];
    }

    /**
     * Compute attacker/defender hero XP and victory points from the battle
     * casualties, persist them (hero XP, player points, alliance points) and
     * return the total defender losses.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $alldead        Reinforcement casualties by global unit id (+ 'hero').
     * @param array $owndead        Defender own casualties by global unit id (+ 'hero').
     * @param array $attackerDead   Attacker casualties, 0-indexed t1..t11 (index 10 = hero).
     * @param int   $targettribe    Defender tribe id.
     * @param int   $att_tribe      Attacker tribe id.
     * @param array $Attacker       Attacker troop row (uses 'uhero').
     * @param mixed $AttackerHeroID Attacker hero id (for the XP write).
     * @param array $Defender       Defender troop row (uses 'hero').
     * @param array $DefendersHeroID Defender hero ids (for the XP writes).
     * @param array $toF            Defender village (uses 'owner').
     * @param array $from           Attacker village info (uses 'owner').
     * @param int   $targetally     Defender alliance id.
     * @param int   $ownally        Attacker alliance id.
     * @param mixed $heroxp         OUT (by ref): attacker hero XP, set only if the attacker has a hero.
     * @param mixed $defheroxp      OUT (by ref): per-defender hero XP, set only if the defender has a hero.
     * @return int Total defender losses (reinforcements + own troops).
     */
    private function calculateHeroXpAndPoints(
        array $alldead, array $owndead, array $attackerDead,
        $targettribe, $att_tribe,
        $Attacker, $AttackerHeroID, $Defender, $DefendersHeroID,
        $toF, $from, $targetally, $ownally,
        &$heroxp, &$defheroxp,
        array $defensePointShares = []
    ) {
        global $database;

        $totaldead_def = 0;
        $totalpoint_att = 0;

        for($i = 1 ;$i <= 90; $i++) {
            $unitarray = $GLOBALS["u".$i];

            //Reinforcements dead troops
            $totaldead_def += $alldead[$i];
            $totalpoint_att += ($alldead[$i] * $unitarray['pop']);

            //Own dead troops
            if($i >= ($targettribe - 1) * 10 + 1 && $i <= $targettribe * 10){
                $totaldead_def += $owndead[$i];
                $totalpoint_att += ($owndead[$i] * $unitarray['pop']);
            }
        }
        $totalpoint_att += ((isset($alldead['hero']) ? $alldead['hero'] : 0) * 6);
        $totalpoint_att += ((isset($owndead['hero']) ? $owndead['hero'] : 0) * 6);

        if ($Attacker['uhero'] > 0){
            $heroxp = $totalpoint_att;
            $database->modifyHeroXp("experience", $heroxp, $AttackerHeroID);
        }

        for($i = 1; $i <= 10; $i++){
            $unitarray = $GLOBALS["u".(($att_tribe - 1) * 10 + $i)];
            if ( !isset($totalpoint_def) ) {
                $totalpoint_def = 0;
            }
            $totalpoint_def += ($attackerDead[$i - 1] * $unitarray['pop']);
        }

        $totalpoint_def += $attackerDead[10] * 6;

        if($Defender['hero'] > 0){
            //counting heroxp
            $defheroxp = intval($totalpoint_def / count($DefendersHeroID));
            foreach($DefendersHeroID as $HeroID){
                $database->modifyHeroXp("experience",$defheroxp,$HeroID);
            }
        }

        // we don't need these two variables anymore
        unset($AttackerHeroID, $DefendersHeroID);

        // BUG FIXED (defense points awarded to the wrong player(s)): $totalpoint_def
        // is the total defense-point value for the WHOLE battle (every defender's
        // troops combined) - it was previously credited entirely to $toF['owner']
        // (the attacked village's owner) even when other players' reinforcements
        // did some or all of the actual defending. $defensePointShares (from
        // Battle::computeDefenderForces(), threaded through calculateBattle()'s
        // result as 'DefensePointShares') maps owner id -> that owner's own
        // dp+cdp contribution to the battle. Split $totalpoint_def proportionally
        // by each owner's share of the total, and credit each owner (and each
        // owner's alliance) individually instead of dumping it all on one player.
        //
        // Fallback: if the breakdown is empty or sums to 0 (e.g. a pure scout
        // skirmish with no measurable dp/cdp, or the caller didn't pass one),
        // preserve the exact previous behavior - credit $toF['owner']/$targetally
        // in full - rather than silently discarding the points.
        $totalDefenseShare = array_sum($defensePointShares);

        if ($totalDefenseShare <= 0) {

            $database->modifyPoints(
                $toF['owner'],
                ['dpall', 'dp'],
                [$totalpoint_def, $totalpoint_def]
            );

            $database->modifyPointsAlly(
                $targetally,
                ['Adp', 'dp'],
                [$totalpoint_def, $totalpoint_def]
            );

        } else {

            $perAlliancePoints = [];
            $allianceCache = [];
            $creditedTotal = 0;

            // filter down to valid, positive shares FIRST, so "last owner gets
            // the rounding remainder" below always lands on someone who is
            // actually credited - picking it from the unfiltered list could
            // land on a skipped (invalid or zero-share) entry and lose the
            // remainder entirely.
            $validShares = array_filter(
                $defensePointShares,
                function ($share, $ownerId) { return (int) $ownerId > 0 && $share > 0; },
                ARRAY_FILTER_USE_BOTH
            );

            $ownerIds = array_keys($validShares);
            $lastOwnerId = end($ownerIds);

            foreach ($validShares as $ownerId => $share) {

                $ownerId = (int) $ownerId;

                // last owner in the split gets the rounding remainder, so the
                // sum of everything credited always equals $totalpoint_def
                // exactly (no points lost or invented to floating-point rounding)
                if ($ownerId === $lastOwnerId) {
                    $ownerPoints = $totalpoint_def - $creditedTotal;
                } else {
                    $ownerPoints = (int) round(($share / $totalDefenseShare) * $totalpoint_def);
                }

                $creditedTotal += $ownerPoints;

                if ($ownerPoints == 0) {
                    continue;
                }

                $database->modifyPoints(
                    $ownerId,
                    ['dpall', 'dp'],
                    [$ownerPoints, $ownerPoints]
                );

                if (!isset($allianceCache[$ownerId])) {
                    $allianceCache[$ownerId] = (int) $database->getUserField($ownerId, "alliance", 1);
                }

                $ownerAlliance = $allianceCache[$ownerId];

                if ($ownerAlliance > 0) {
                    $perAlliancePoints[$ownerAlliance] = ($perAlliancePoints[$ownerAlliance] ?? 0) + $ownerPoints;
                }
            }

            foreach ($perAlliancePoints as $allianceId => $alliPoints) {
                $database->modifyPointsAlly(
                    $allianceId,
                    ['Adp', 'dp'],
                    [$alliPoints, $alliPoints]
                );
            }
        }

        $database->modifyPoints(
            $from['owner'],
            ['apall', 'ap'],
            [$totalpoint_att, $totalpoint_att]
        );

        $database->modifyPointsAlly(
            $ownally,
            ['Aap', 'ap'],
            [$totalpoint_att, $totalpoint_att]
        );

        return $totaldead_def;
    }

    /**
     * Resolve the resources lootable from the battle target after cranny
     * protection: compute the cranny efficiency, pull the current (capped)
     * stocks of the target village/oasis and derive the lootable amounts.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array $data        Current attack row (uses 'to').
     * @param int   $isoasis     Oasis flag (0 = village).
     * @param int   $conqureby   Owner village of a conquered oasis (0 = none).
     * @param int   $owntribe    Attacker tribe id (Teuton cranny bonus).
     * @param int   $targettribe Defender tribe id (Gaul cranny bonus).
     * @param mixed $crannySpy   OUT (by ref): cranny value seen by a scout; set only for village targets (isoasis == 0).
     * @return array ['totclay','totiron','totwood','totcrop','avclay','aviron','avwood','avcrop'].
     */
    private function resolveResourcesAfterBattle($data, $isoasis, $conqureby, $owntribe, $targettribe, &$crannySpy) {
        global $database, $bid23;

        if ($isoasis == 0){
            // get total cranny value:
            $buildarray = $database->getResourceLevel($data['to']);
            $cranny = 0;
            for($i = 19; $i < 39;$i++){
                if($buildarray['f'.$i.'t'] == 23){
                    $cranny += $bid23[$buildarray['f'.$i]]['attri'] * CRANNY_CAPACITY;
                }
            }

            //cranny efficiency
            $atk_bonus = ($owntribe == 2) ? (4 / 5) : 1;
            $def_bonus = ($targettribe == 3) ? 2 : 1;
            $to_owner = $database->getVillageField($data['to'], "owner");

            $crannySpy = $database->getArtifactsValueInfluence($to_owner, $data['to'], 7, $cranny * $def_bonus);
            $cranny_eff = $crannySpy * $atk_bonus;

            // work out available resources.
            $this->updateRes($data['to']);
            $this->pruneResource();

            $villageData = $database->getVillageFields($data['to'], 'clay, iron, wood, crop', false);
            $totclay = $villageData['clay'];
            $totiron = $villageData['iron'];
            $totwood = $villageData['wood'];
            $totcrop = $villageData['crop'];
        }else{
            $cranny_eff = 0;

            if ($conqureby > 0) { //10% from owner proc village owner - fix by ronix - exploit fixed by iopietro
                $this->updateRes($conqureby);
                $this->pruneResource();

                $villageData = $database->getVillageFields($conqureby, 'clay, iron, wood, crop', false);
                // BUG FIXED: $conqureby (the village about to receive the oasis) can itself
                // have been razed earlier in the same batch, in which case getVillageFields()
                // returns null and every read below threw "Trying to access array offset on
                // null". Null-coalescing to 0 keeps the same effective result as before
                // (intval(null / 10) already evaluated to 0) without the warning.
                $totclay = intval(($villageData['clay'] ?? 0) / 10);
                $totiron = intval(($villageData['iron'] ?? 0) / 10);
                $totwood = intval(($villageData['wood'] ?? 0) / 10);
                $totcrop = intval(($villageData['crop'] ?? 0) / 10);
            }else{
                // work out available resources.
                $this->updateORes($data['to']);
                $this->pruneOResource();

                $oasisData = $database->getOasisFields($data['to'], false);
                $totclay = $oasisData['clay'];
                $totiron = $oasisData['iron'];
                $totwood = $oasisData['wood'];
                $totcrop = $oasisData['crop'];
            }
        }

        $avclay = floor($totclay - $cranny_eff);
        $aviron = floor($totiron - $cranny_eff);
        $avwood = floor($totwood - $cranny_eff);
        $avcrop = floor($totcrop - $cranny_eff);

        $avclay = ($avclay < 0) ? 0 : $avclay;
        $aviron = ($aviron < 0) ? 0 : $aviron;
        $avwood = ($avwood < 0) ? 0 : $avwood;
        $avcrop = ($avcrop < 0) ? 0 : $avcrop;

        return [
            'totclay' => $totclay, 'totiron' => $totiron, 'totwood' => $totwood, 'totcrop' => $totcrop,
            'avclay'  => $avclay,  'aviron'  => $aviron,  'avwood'  => $avwood,  'avcrop'  => $avcrop,
        ];
    }

    /**
     * Resolve the attacker hero's outcome after the battle: build the hero
     * report line ($info_hero), and on a surviving hero handle oasis conquest /
     * loyalty reduction (oasis targets) or artifact claiming with possible
     * village destruction (village targets).
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param array  $data              Current attack row (uses 't11', 'from', 'to').
     * @param array  $from              Attacker village info (uses 'owner').
     * @param array  $to                Defender village/oasis info (uses 'owner', 'loyalty').
     * @param int    $dead11            Attacker hero casualties.
     * @param int    $traped11          Attacker hero trapped count.
     * @param mixed  $heroxp            Attacker hero XP gained this battle.
     * @param string $hero_pic          Hero unit picture id (report markup).
     * @param int    $isoasis           Oasis flag (0 = village).
     * @param int    $type              Attack type (3 = normal with rams/catas).
     * @param int    $targettribe       Defender tribe id.
     * @param string $info_cat          Catapult report fragment (read for the destruction notice).
     * @param string $info_hero         IN/OUT (by ref): hero report line.
     * @param int    $can_destroy       IN/OUT (by ref): village-destruction allowed flag.
     * @param int    $village_destroyed IN/OUT (by ref): village-destroyed flag.
     * @return void
     */
    private function handleHeroPostBattle($data, $from, $to, $dead11, $traped11, $heroxp, $hero_pic, $isoasis, $type, $targettribe, $info_cat, &$info_hero, &$can_destroy, &$village_destroyed) {
        global $database;

        if(($data['t11'] - $dead11 - $traped11) > 0){ //hero
            if ($heroxp == 0) {
                $xp = "";
                $info_hero = $hero_pic.",".rc_tok('RC_HERO_NO_KILL');
            } else {
                $xp = " ".rc_tok('RC_HERO_AND_GAINED_XP_BATTLE', $heroxp);
                $info_hero = $hero_pic.",".rc_tok('RC_HERO_GAINED_XP', $heroxp);
            }

            if ($isoasis != 0) { //oasis fix by ronix
                if ($to['owner'] != $from['owner']) {
                    $troopcount = $database->countOasisTroops($data['to'], false);
                    $canqured = $database->canConquerOasis($data['from'], $data['to'], false);
                    if ($canqured == 1 && $troopcount == 0) {
                        $database->conquerOasis($data['from'], $data['to']);
                        $info_hero = $hero_pic.",".rc_tok('RC_HERO_CONQUERED_OASIS').$xp;
                    }else{
                        if ($canqured == 3 && $troopcount == 0) {
                            if ($type == 3) {
                                $Oloyaltybefore = intval($to['loyalty']);
                                //$database->modifyOasisLoyalty($data['to']);
                                //$OasisInfo = $database->getOasisInfo($data['to']);
                                $Oloyaltynow = intval($database->modifyOasisLoyalty($data['to']));//intval($OasisInfo['loyalty']);
                                $info_hero = $hero_pic.",".rc_tok('RC_HERO_REDUCED_OASIS_LOYALTY', $Oloyaltynow, $Oloyaltybefore).$xp;
                            }
                            else $info_hero = $hero_pic.",".rc_tok('RC_NO_REDUCE_LOYALTY_RAID').$xp;
                        }
                    }
                }
            } else {
                if ($heroxp == 0) $xp=" ".rc_tok('RC_HERO_NO_XP_BATTLE');
                else $xp=" ".rc_tok('RC_HERO_GAINED_XP_BATTLE', $heroxp);

                $artifact = reset($database->getOwnArtefactInfo($data['to']));
                if (!empty($artifact)) {
                    if ($type == 3) {
                        if (empty($artifactError = $database->canClaimArtifact($data['from'], $artifact['vref'], $artifact['size'], $artifact['type']))) {
                            $database->claimArtefact($data['from'], $data['to'], $database->getVillageField($data['from'], "owner"));
                            $info_hero = $hero_pic.",".rc_tok('RC_HERO_CARRYING_ARTIFACT', $artifact['name']).$xp;

                            // if the defender pop is 0 with no artefact, then destroy the village
                            if($database->getVillageField($data['to'], "pop") == 0 || $targettribe == 5){
                                $can_destroy = $village_destroyed = 1;
                                if(strpos($info_cat, rc_tok('RC_VILLAGE_DESTROYED')) === false) $info_hero .= " ".rc_tok('RC_VILLAGE_DESTROYED');
                            }
                        }
                        else $info_hero = $hero_pic.",".$artifactError.$xp;
                    }
                    else $info_hero = $hero_pic.",".rc_tok('RC_HERO_NO_ARTIFACT_RAID').$xp;
                }
            }
        }elseif($data['t11'] > 0) {
            if ($heroxp == 0) $xp = "";     
            else $xp = " ".rc_tok('RC_HERO_BUT_GAINED_XP_BATTLE', $heroxp);

            if ($traped11 > 0) $info_hero = $hero_pic.",".rc_tok('RC_HERO_TRAPPED').$xp;
            else $info_hero = $hero_pic.",".rc_tok('RC_HERO_DIED').$xp;
        }
    }

    /**
     * Send the battle/scout report notification to the defender.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param mixed  $scout            Truthy when the attack is a scouting run.
     * @param array  $battlepart       Battle result (uses 'casualties_attacker').
     * @param array  $from             Attacker village info (uses 'owner', 'name').
     * @param array  $to               Defender village/oasis info (uses 'owner', 'wref', 'name').
     * @param int    $targetally       Defender alliance id.
     * @param string $data2            Combat-report payload.
     * @param int    $AttackArrivalTime Battle resolution timestamp.
     * @param string $unitsdead_att    Attacker dead units string.
     * @param string $unitssend_att    Attacker sent units string.
     * @param bool   $defspy           Whether the defence has scouts.
     * @param string $info_troop       Troop-return report fragment.
     * @param int    $totalsend_alldef Total defending troops (incl. reinforcements).
     * @param int    $totalsend_att    Total attacking troops sent.
     * @param int    $totaldead_att    Total attacking troops killed.
     * @param int    $totaltraped_att  Total attacking troops trapped.
     * @param int    $totaldead_alldef Total defending troops killed.
     * @return void
     */
    private function sendBattleNotifications($scout, $battlepart, $from, $to, $targetally, $data2, $AttackArrivalTime, $unitsdead_att, $unitssend_att, $defspy, $info_troop, $totalsend_alldef, $totalsend_att, $totaldead_att, $totaltraped_att, $totaldead_alldef) {
        global $database;

        //Undetected and detected in here.
        if(!empty($scout)){
            for($i = 1; $i <= 10; $i++){
                if($battlepart['casualties_attacker'][$i]){
                    if($from['owner'] == 3){
                        $database->addNotice($to['owner'],$to['wref'],$targetally,20,''.addslashes($from['name']).' scouts '.addslashes($to['name']).'',$data2,$AttackArrivalTime);
                        break;
                    }else if($unitsdead_att == $unitssend_att && $defspy){ //fix by ronix
                        $database->addNotice($to['owner'],$to['wref'],$targetally,20,''.addslashes($from['name']).' scouts '.addslashes($to['name']).'',$data2.',,'.$info_troop,$AttackArrivalTime);
                        break;
                    }else if($defspy){ //fix by ronix
                        $database->addNotice($to['owner'],$to['wref'],$targetally,21,''.addslashes($from['name']).' scouts '.addslashes($to['name']).'',$data2,$AttackArrivalTime);
                        break;
                    }
                }
            }
        }else{
            if($totalsend_alldef == 0 && $totalsend_att - ($totaldead_att + (isset($totaltraped_att) ? $totaltraped_att : 0)) > 0){
                $database->addNotice($to['owner'],$to['wref'],$targetally,7,''.addslashes($from['name']).' attacks '.addslashes($to['name']).'',$data2,$AttackArrivalTime);
            }else if($totaldead_alldef == 0){
                $database->addNotice($to['owner'],$to['wref'],$targetally,4,''.addslashes($from['name']).' attacks '.addslashes($to['name']).'',$data2,$AttackArrivalTime);
            }else if($totalsend_alldef > $totaldead_alldef){
                $database->addNotice($to['owner'],$to['wref'],$targetally,5,''.addslashes($from['name']).' attacks '.addslashes($to['name']).'',$data2,$AttackArrivalTime);
            }else if($totalsend_alldef == $totaldead_alldef){
                $database->addNotice($to['owner'],$to['wref'],$targetally,6,''.addslashes($from['name']).' attacks '.addslashes($to['name']).'',$data2,$AttackArrivalTime);
            }
        }
    }

    /**
     * Finalize the fate of the attacking troops: if any survived they return
     * home (attacker report + return movement + loot transfer / RR points /
     * leftover-chief enforcement), otherwise they die without returning (only
     * the all-dead report is sent). Pure behaviour-preserving extraction
     * (refactor for issue #155).
     *
     * @param array  $data             Current attack row (uses 't1..t11', 'moveid', 'ref').
     * @param array  $from             Attacker village info (uses 'wref', 'owner', 'name').
     * @param array  $to               Defender village/oasis info (uses 'wref', 'owner', 'name').
     * @param int    $owntribe         Attacker tribe id.
     * @param int    $type             Attack type (1 = scout).
     * @param int    $isoasis          Oasis flag (0 = village).
     * @param int    $conqureby        Owner village of a conquered oasis (0 = none).
     * @param int    $AttackArrivalTime Battle resolution timestamp.
     * @param int    $targetally       Defender alliance id.
     * @param int    $ownally          Attacker alliance id.
     * @param string $data2            Combat-report payload (survivors).
     * @param string $data_fail        Combat-report payload (all dead).
     * @param int    $chiefing_village Whether this attack chiefed the village.
     * @param int    $DefenderWref     Defender world ref.
     * @param int    $AttackerWref     Attacker world ref.
     * @param array  $steal            [wood, clay, iron, crop] looted.
     * @param array  $dead             Attacker casualties, 1-indexed t1..t11.
     * @param array  $traped           Attacker trapped, 1-indexed t1..t11.
     * @param array  $troopsdead       Attacker dead counts for enforcement, 1-indexed t1..t11.
     * @param int    $totalsend_att    Total attacking troops sent.
     * @param int    $totaldead_att    Total attacking troops killed.
     * @param int    $totaltraped_att  Total attacking troops trapped.
     * @param int    $totalstolentaken IN/OUT (by ref): running RR loss accumulator for the defender.
     * @return void
     */
    private function finalizeReturnOrDeath($data, $from, $to, $owntribe, $type, $isoasis, $conqureby, $AttackArrivalTime, $targetally, $ownally, $data2, $data_fail, $chiefing_village, $DefenderWref, $AttackerWref, $steal, $dead, $traped, $troopsdead, $totalsend_att, $totaldead_att, $totaltraped_att, &$totalstolentaken) {
        global $database, $units;

        // Spital: o parte din trupele proprii moarte IN ATAC devin ranite acasa.
        // $dead e indexat 1..11 pe sloturile tribului; doar unitatile de lupta (1-8).
        if($owntribe >= 1 && $owntribe <= 9) {
            $woundedAtt = [];
            for($wj = 1; $wj <= 8; $wj++) {
                if(!empty($dead[$wj])) $woundedAtt[($owntribe - 1) * 10 + $wj] = $dead[$wj];
            }
            if(!empty($woundedAtt)) $this->applyHospitalWounded($from['wref'], $woundedAtt);
        }

                    // If the dead units not equal the ammount sent they will return and report
                    if($totalsend_att - ($totaldead_att + (isset($totaltraped_att) ? $totaltraped_att : 0)) > 0)
                    {                       
                        $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);
							}elseif($totaldead_att == 0 && $totaltraped_att == 0){
								$database->addNotice($from['owner'], $to['wref'], $ownally, 18, '' . addslashes($from['name']) . ' scouts ' . addslashes($to['name']) . '', $data2, $AttackArrivalTime);
							}else{
								$database->addNotice($from['owner'], $to['wref'], $ownally, 21, '' . addslashes($from['name']) . ' scouts ' . addslashes($to['name']) . '', $data2, $AttackArrivalTime);
							}
						}else{
							if((empty($totaldead_att) || $totaldead_att == 0) && (empty($totaltraped_att) || $totaltraped_att == 0)){
								$database->addNotice($from['owner'], $to['wref'], $ownally, 1, '' . addslashes($from['name']) . ' attacks ' . addslashes($to['name']) . '', $data2, $AttackArrivalTime);
							}else{
								$database->addNotice($from['owner'], $to['wref'], $ownally, 2, '' . addslashes($from['name']) . ' attacks ' . addslashes($to['name']) . '', $data2, $AttackArrivalTime);
							}
						}

                        $database->setMovementProc($data['moveid']);

                        if (!isset($chiefing_village)) $chiefing_village = 0;

                        if($chiefing_village != 1){
                        	$database->addMovement(4, $DefenderWref, $AttackerWref, $data['ref'], $AttackArrivalTime, $endtime, 1, $steal[0], $steal[1], $steal[2], $steal[3]);
                            if($type !== 1){
                            	if ($isoasis == 0) $database->modifyResource($DefenderWref, $steal[0], $steal[1], $steal[2], $steal[3], 0);                   
                                else
                                {
                                	//if it's an oasis but it's conquered by someone, resources must be modified in the owner's village
                                	if($conqureby > 0) $database->modifyResource($conqureby, $steal[0], $steal[1], $steal[2], $steal[3], 0);
                                    else $database->modifyOasisResource($DefenderWref, $steal[0], $steal[1], $steal[2], $steal[3], 0);                                  
                                }
								$totalstolengain = $steal[0] + $steal[1] + $steal[2] + $steal[3];
								$totalstolentaken = ((isset($totalstolentaken) ? $totalstolentaken : 0) - ($steal[0] + $steal[1] + $steal[2] + $steal[3]));
								$database->modifyPoints($from['owner'], 'RR', $totalstolengain);
								$database->modifyPoints($to['owner'], 'RR', $totalstolentaken);
								$database->modifyPointsAlly($targetally, 'RR', $totalstolentaken);
								$database->modifyPointsAlly($ownally, 'RR', $totalstolengain);
                            }
                        }else{ //fix by ronix if only 1 chief left to conqured - don't add with zero enforces
                            if($totalsend_att - ($totaldead_att + (isset($totaltraped_att) ? $totaltraped_att : 0)) > 1){
								$database->addEnforce2($data, $owntribe, $troopsdead[1], $troopsdead[2], $troopsdead[3], $troopsdead[4], $troopsdead[5], $troopsdead[6], $troopsdead[7], $troopsdead[8], $troopsdead[9], $troopsdead[10], $troopsdead[11]);
							}
                        }
                    }
                    else //else they die and don't return or report.
                    {
                        $database->setMovementProc($data['moveid']);
						if($type == 1){
							$database->addNotice($from['owner'], $to['wref'], $ownally, 19, addslashes($from['name']) . ' scouts ' . addslashes($to['name']) . '', $data_fail, $AttackArrivalTime);
						}else{
							$database->addNotice($from['owner'], $to['wref'], $ownally, 3, '' . addslashes($from['name']) . ' attacks ' . addslashes($to['name']) . '', $data_fail, $AttackArrivalTime);
						}
                    }
    }

    /**
     * Destroy the target village when the battle flagged it for destruction:
     * reassign the capital to the player's most populated remaining village if
     * needed, delete the village and reassign its hero.
     * Pure behaviour-preserving extraction (refactor for issue #155).
     *
     * @param int   $village_destroyed Village-destroyed flag.
     * @param int   $can_destroy       Village-destruction allowed flag.
     * @param array $data              Current attack row (uses 'to').
     * @param array $to                Defender village info (uses 'capital').
     * @param array $varray            The defender owner's villages.
     * @return void
     */
    private function handleVillageDestruction($village_destroyed, $can_destroy, $data, $to, $varray) {
        global $database;

        if (!($village_destroyed == 1 && $can_destroy == 1)) {
            return;
        }

        if($to['capital'] == 1){
            $mostPopulatedVillage = [];
            //Search for the most populated village
            foreach($varray as $village){
                if($village['wref'] != $data['to'] && (empty($mostPopulatedVillage) || $mostPopulatedVillage['pop'] < $village['pop'])){
                    $mostPopulatedVillage = $village;
                }
            }
            //Set the new capital
            $database->changeCapital($mostPopulatedVillage['wref']);
        }

        //Delete the village
        $database->DelVillage($data['to']);

        //Reassign the hero, if dead and assigned to the deleted village
        $database->reassignHero($data['to']);
    }

    private function sendunitsComplete() {
		// COMPLETE ATTACK PROCESSING - critical function, kept 100% compatible
		// This function handles all attacks that reach their destination.
		// Includes: battles, traps, hero evasion, building destruction, and village conquest.
        global $bid23, $database, $battle, $technology, $units;

        $time = time();
        $dataarray = $this->fetchCompletedAttacks($time);
        $totalattackdead = $data_num = 0;

        if ($dataarray && count($dataarray)) {
            // preload village data (batched) so the per-attack loop hits the
            // cache instead of querying row by row
            $this->preloadBattleData($dataarray);

            // tiles whose village was razed earlier in this same batch (issue #298)
            $razedTargets = [];

            // calculate battles
            foreach($dataarray as $data) {
                //set base things
				$totaltraped_att = 0;
				for($i = 1; $i <= 11; $i++) ${'traped'.$i} = 0;
                // per-attack context (attacker village/owner) — extracted to resolveAttackContext() [#155]
                $ctx = $this->resolveAttackContext($data);
                $isoasis           = $ctx['isoasis'];
                $AttackArrivalTime = $ctx['AttackArrivalTime'];
                $AttackerWref      = $ctx['AttackerWref'];
                $DefenderWref      = $ctx['DefenderWref'];
                $NatarCapital      = $ctx['NatarCapital'];
                $AttackerID        = $ctx['AttackerID'];
                $Attacker['id']    = $ctx['AttackerID'];
                $owntribe          = $ctx['owntribe'];
                $ownally           = $ctx['ownally'];
                $from              = $ctx['from'];
                $fromF             = $ctx['fromF'];

                //It's a village
                if ($isoasis == 0){
                    // target + battle environment — extracted to resolveVillageTarget() [#155]
                    $vt = $this->resolveVillageTarget($data, $dataarray, $data_num, $owntribe);
                    $DefenderID   = $vt['DefenderID'];
                    $targettribe  = $vt['targettribe'];
                    $targetally   = $vt['targetally'];
                    $to           = $vt['to'];
                    $toF          = $vt['toF'];
                    $conqureby    = $vt['conqureby'];
                    $NatarCapital = $vt['NatarCapital'];
                    $DefenderUnit = $vt['DefenderUnit'];
                    $def_wall     = $vt['def_wall'];
                    $att_tribe    = $vt['att_tribe'];
                    $def_tribe    = $vt['def_tribe'];
                    $attpop       = $vt['attpop'];
                    $defpop       = $vt['defpop'];
                    $residence    = $vt['residence'];
                    $def_ab       = $vt['def_ab'];
                    $type         = $vt['type'];
                    $scout        = $vt['scout'];
                    $att_ab1      = $vt['att_ab1'];
                    $att_ab2      = $vt['att_ab2'];
                    $att_ab3      = $vt['att_ab3'];
                    $att_ab4      = $vt['att_ab4'];
                    $att_ab5      = $vt['att_ab5'];
                    $att_ab6      = $vt['att_ab6'];
                    $att_ab7      = $vt['att_ab7'];
                    $att_ab8      = $vt['att_ab8'];
                    $walllevel    = $vt['walllevel'];
                    $wallid       = $vt['wallid'];
                    $tblevel      = $vt['tblevel'];
                    $stonemason   = $vt['stonemason'];

                    // Issue #298: the target village no longer exists — it was razed
                    // either earlier in this same batch ($razedTargets), or in an
                    // earlier tick whose still-in-flight follow-up waves DelVillage()
                    // failed to bounce. getMInfo() then returns NULL vdata columns
                    // ($to['wref'] is NULL), so resolving a battle here would fight a
                    // phantom village and compute the return trip from NULL coordinates
                    // — a bogus arrival time that strands the troops in an endless loop
                    // (report against "[?]"). Bounce the whole army straight home
                    // instead, exactly like DelVillage() does for in-flight attacks,
                    // and mark the movement processed so it stops being re-fetched.
                    if (isset($razedTargets[$data['to']]) || empty($to['wref'])) {
                        // only own the bounce if DelVillage() hasn't already handled it
                        // (setMovementProc() returns true only when it flips proc 0->1),
                        // so we never create a duplicate return movement
                        //
                        // BUG FIXED: setMovementProc() lives on $database (DatabaseMovementQueries),
                        // not on Automation - "$this->" was throwing "Call to undefined method
                        // Automation::setMovementProc()" (fatal, killed the whole automation tick)
                        // every time this razed-target bounce path was hit. Every other call site
                        // in this file already uses $database->setMovementProc().
                        if ($database->setMovementProc($data['moveid'])) {
                            $bounceTime = $units->getWalkingTroopsTime($from['wref'], $data['to'], $from['owner'], $owntribe, $data, 1, 't');
                            $bounceEnd  = $database->getArtifactsValueInfluence($from['owner'], $from['wref'], 2, $bounceTime) + $AttackArrivalTime;
                            $database->addMovement(4, $data['to'], $from['wref'], $data['ref'], $AttackArrivalTime, $bounceEnd);
                        }
                        $data_num++;
                        continue;
                    }

                    $this->handleEvasion($data, $DefenderID, $DefenderUnit, $targettribe, $vt['evasion'], $vt['maxevasion'], $vt['gold'], $vt['cannotsend'], $dataarray[$data_num]['attack_type']);

                    // defence units gathered — extracted to buildDefenderUnits() [#155]
                    $defUnits = $this->buildDefenderUnits($data['to']);
                    $Defender         = $defUnits['Defender'];
                    $enforDefender    = $defUnits['enforDefender'];
                    $enforcementarray = $defUnits['enforcementarray'];

                    // attacker army built — extracted to buildAttackerUnits() [#155]
                    $atkUnits  = $this->buildAttackerUnits($dataarray[$data_num], $owntribe, $isoasis);
                    $Attacker  = $atkUnits['Attacker'];
                    $start     = $atkUnits['start'];
                    $end       = $atkUnits['end'];
                    $catp_pic  = $atkUnits['catp_pic'];
                    $ram_pic   = $atkUnits['ram_pic'];
                    $chief_pic = $atkUnits['chief_pic'];
                    $spy_pic   = $atkUnits['spy_pic'];
                    $hero_pic  = $atkUnits['hero_pic'];

                }else{ //It's an oasis
                    // target + battle environment — extracted to resolveOasisTarget() [#155]
                    $ot = $this->resolveOasisTarget($data, $dataarray, $data_num, $owntribe);
                    $DefenderID  = $ot['DefenderID'];
                    $targettribe = $ot['targettribe'];
                    $targetally  = $ot['targetally'];
                    $to          = $ot['to'];
                    $toF         = $ot['toF'];
                    $conqureby   = $ot['conqureby'];
                    $def_wall    = $ot['def_wall'];
                    $att_tribe   = $ot['att_tribe'];
                    $def_tribe   = $ot['def_tribe'];
                    $attpop      = $ot['attpop'];
                    $defpop      = $ot['defpop'];
                    $residence   = $ot['residence'];
                    $def_ab      = $ot['def_ab'];
                    $type        = $ot['type'];
                    $scout       = $ot['scout'];
                    $att_ab1     = $ot['att_ab1'];
                    $att_ab2     = $ot['att_ab2'];
                    $att_ab3     = $ot['att_ab3'];
                    $att_ab4     = $ot['att_ab4'];
                    $att_ab5     = $ot['att_ab5'];
                    $att_ab6     = $ot['att_ab6'];
                    $att_ab7     = $ot['att_ab7'];
                    $att_ab8     = $ot['att_ab8'];
                    $walllevel   = $ot['walllevel'];
                    $tblevel     = $ot['tblevel'];
                    $stonemason  = $ot['stonemason'];

                    // defence units gathered — extracted to buildDefenderUnits() [#155]
                    $defUnits = $this->buildDefenderUnits($data['to']);
                    $Defender         = $defUnits['Defender'];
                    $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'];
                    $start     = $atkUnits['start'];
                    $end       = $atkUnits['end'];
                    $catp_pic  = $atkUnits['catp_pic'];
                    $ram_pic   = $atkUnits['ram_pic'];
                    $chief_pic = $atkUnits['chief_pic'];
                    $spy_pic   = $atkUnits['spy_pic'];
                    $hero_pic  = $atkUnits['hero_pic'];
                }

                // attacker/defender populations + village lists — extracted to calculatePopulations() [#155]
                $popData = $this->calculatePopulations($to, $from, $isoasis, $defpop, $attpop);
                $varray  = $popData['varray'];
                $varray1 = $popData['varray1'];
                $defpop  = $popData['defpop'];
                $attpop  = $popData['attpop'];

                //fix by ronix
                for ($i = 1; $i <= 90; $i++) {
                    if (!isset($enforDefender['u'.$i])) {
                        $enforDefender['u'.$i] = 0;
                    }
                    $enforDefender['u'.$i] += (isset($Defender['u'.$i]) ? $Defender['u'.$i] : 0);
                }

                $defspy = $enforDefender['u4'] > 0 || $enforDefender['u14'] > 0 || $enforDefender['u23'] > 0 || $enforDefender['u44'] > 0;

                if(PEACE == 0 || $targettribe == 4 || $targettribe == 5 || $scout){
                    // trapper resolution + prisoners — extracted to calculateTrappedUnits() [#155]
                    $trapResult = $this->calculateTrappedUnits($data, $Defender, $Attacker, $NatarCapital, $scout, $start, $end);
                    for($i = 1; $i <= 11; $i++) ${'traped'.$i} = $trapResult['traped'][$i];
                    $totaltraped_att = $trapResult['totaltraped_att'];
                    $Attacker        = $trapResult['Attacker'];

                    // we need to save the attacker heroid before the battle
					$AttackerHeroID = 0;
                    if(isset($Attacker['uhero']) && $Attacker['uhero'] > 0){
                        $AttackerHeroID = $database->getHeroField($from['owner'], "heroid");
                    }
                    
                    // and the defender(s) heroid
                    $DefendersHeroID = [];
                    $herosend_def = 0;
                    
                    // check if our hero is defending the village, if so, add it to the list
                    if (isset($Defender['hero']) && $Defender['hero'] > 0) {                        
                        $DefendersHeroID[] = $database->getHeroField($DefenderID, "heroid");
                        
                        // collecting information for the battle report
                        $herosend_def += $Defender['hero'];
                    }

                    // check if there are other heroes defending the village
                    if(count($enforcementarray) > 0){
                        foreach($enforcementarray as $enforcement){
                            if($enforcement['hero'] > 0){
                                $heroOwner = $database->getVillageField($enforcement['from'], "owner");
                                $DefendersHeroID[] = $database->getHeroField($heroOwner, "heroid");
                            }                          
                        }
                    }

                    //fix by ronix
                    if (!isset($walllevel)) $walllevel = 0;

                    $battlepart = $battle->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, 0, 0, 0, $AttackerID, $DefenderID, $AttackerWref, $DefenderWref, $conqureby, $enforcementarray);

                    //Data for when troops return.
                    //catapults look :D
                    $info_cat = $info_chief = $info_ram = $info_hero = ",";
                    
                    //check to see if can destroy village
                    if (count($varray) > 1 && !$database->villageHasArtefact($DefenderWref) && empty($to['natar'])) {
                    	$can_destroy = 1;
                    }
                    else $can_destroy = 0;
                    
                    //Catapults and rams management
                    //TODO: Move this in Battle.php
                    if($isoasis == 0){
                    	if ($type == 3){
                    		if (($data['t7'] - $traped7) > 0){
                    			// ram damage on the wall (+ battle recalc) — extracted to applyRamDamage() [#155]
                    			$ramResult = $this->applyRamDamage($data, $walllevel, $wallid, $ram_pic, $battlepart, [
                    			    'Attacker' => $Attacker, 'Defender' => $Defender,
                    			    'att_tribe' => $att_tribe, 'def_tribe' => $def_tribe,
                    			    'residence' => $residence, 'attpop' => $attpop, 'defpop' => $defpop,
                    			    'type' => $type, 'def_ab' => $def_ab,
                    			    'att_ab1' => $att_ab1, 'att_ab2' => $att_ab2, 'att_ab3' => $att_ab3, 'att_ab4' => $att_ab4,
                    			    'att_ab5' => $att_ab5, 'att_ab6' => $att_ab6, 'att_ab7' => $att_ab7, 'att_ab8' => $att_ab8,
                    			    'tblevel' => $tblevel, 'stonemason' => $stonemason,
                    			    'AttackerID' => $AttackerID, 'DefenderID' => $DefenderID,
                    			    'AttackerWref' => $AttackerWref, 'DefenderWref' => $DefenderWref,
                    			    'conqureby' => $conqureby, 'enforcementarray' => $enforcementarray,
                    			]);
                    			$battlepart = $ramResult['battlepart'];
                    			$info_ram   = $ramResult['info_ram'];
                    		}
                    		
                    		if (($data['t8'] - $traped8) > 0)
                    		{
                    			$catResult = $this->applyCatapults($data, $battlepart, $catp_pic, $can_destroy, $isoasis, $targettribe, $info_cat);
                    			$battlepart        = $catResult['battlepart'];
                    			$info_cat          = $catResult['info_cat'];
                    			$village_destroyed = $catResult['village_destroyed'];
                    		}
                    	} elseif (($data['t7'] - $traped7) > 0) {
                    		$info_ram = "".$ram_pic.",Hint: The ram does not work during a raid.";
                    	}
                    }
                    else $can_destroy = 0;  
                    
                    //units attack string for battleraport
                    $unitssend_att = ''.$data['t1'].','.$data['t2'].','.$data['t3'].','.$data['t4'].','.$data['t5'].','.$data['t6'].','.$data['t7'].','.$data['t8'].','.$data['t9'].','.$data['t10'].'';
                    $herosend_att = $data['t11'];

                    //reinforcement report (pre-casualty defender data) — extracted to collectReinforcementReport() [#155]
                    $reinfReport = $this->collectReinforcementReport($data, $targettribe, $Defender);
                    $DefenderHeroesTotArray  = $reinfReport['DefenderHeroesTotArray'];
                    $DefenderHeroesDeadArray = $reinfReport['DefenderHeroesDeadArray'];
                    $unitssend_def           = $reinfReport['unitssend_def'];
                    $unitssend_deff          = $reinfReport['unitssend_deff'];
                    $totalsend_alldef        = $reinfReport['totalsend_alldef'];
                    $Defender['hero']        = $reinfReport['defenderHero'];

                    for($i = 1; $i <= 11; $i++){
                        //MUST TO BE FIX : This is only for defender and still not properly coded
                        if (isset($battlepart['casualties_attacker']) && isset($battlepart['casualties_attacker'][$i]) && $battlepart['casualties_attacker'][$i] <= 0) {
                            ${'dead'.$i} = 0;
                        } else if (isset($data['t'.$i]) && isset($battlepart['casualties_attacker']) && isset($battlepart['casualties_attacker'][$i]) && $battlepart['casualties_attacker'][$i] > $data['t'.$i]) {
                            ${'dead'.$i} = $data['t'.$i];
                        } else {
                            ${'dead'.$i} = (isset($battlepart['casualties_attacker']) && isset($battlepart['casualties_attacker'][$i]) ? $battlepart['casualties_attacker'][$i] : 0);
                        }
                    }

                    $dead = [];
                    $owndead = [];
                    $alldead = [];
                    
                    for($i = 1; $i <= 90; $i++) $alldead[$i] = 0;
                    // BUG FIXED: 'hero' key was only defensively set AFTER applyReinforcementCasualties()
                    // ran (see the guard a few lines below), but that function itself does
                    // "$alldead['hero'] += $dead['hero']" (by reference) as soon as a reinforcement
                    // hero dies - triggering "Undefined array key 'hero'" on every such battle.
                    // Initializing it here, like the numeric 1..90 keys above, removes the warning
                    // with zero behavior change (0 + amount === the previous auto-vivified null + amount).
                    $alldead['hero'] = 0;

                    //kill own defence — extracted to applyOwnDefenceCasualties() [#155]
                    $owndead = $this->applyOwnDefenceCasualties($data, $targettribe, $battlepart);

                    //kill other defence in village (reinforcements) — extracted to applyReinforcementCasualties() [#155]
                    $tribesPresent = $this->applyReinforcementCasualties($data, $battlepart, $from, $to, $ownally, $AttackArrivalTime, $scout, $alldead, $DefenderHeroesDeadArray);
                    $rom   = $tribesPresent['rom'];
                    $ger   = $tribesPresent['ger'];
                    $gal   = $tribesPresent['gal'];
                    $nat   = $tribesPresent['nat'];
                    $natar = $tribesPresent['natar'];
                    $hun   = $tribesPresent['hun'];
                    $egy   = $tribesPresent['egy'];
                    $spa   = $tribesPresent['spa'];
                    $vik   = $tribesPresent['vik'];
                    $totalsend_att = $data['t1']+$data['t2']+$data['t3']+$data['t4']+$data['t5']+$data['t6']+$data['t7']+$data['t8']+$data['t9']+$data['t10']+$data['t11'];              
                    
                    $DefenderHeroesTot = implode(",", $DefenderHeroesTotArray);
                    $DefenderHeroesDead = implode(",", $DefenderHeroesDeadArray);
                    
                    if (empty($alldead['hero'])) $alldead['hero'] = 0;
                    if (empty($owndead['hero'])) $owndead['hero'] = 0;
                    // sursa autoritativa: rezultatul luptei (owndead['hero'] se poate pierde
                    // pe unele cai de executie, iar raportul afisa eroul aparator ca viu)
                    $deadhero = (int)(isset($battlepart['deadherodef']) && $battlepart['deadherodef'] > 0
                        ? $battlepart['deadherodef']
                        : $owndead['hero']);

                    //Counting own total dead troops
                    $ownDeadTroops = array_slice($owndead, 0, 10);                   
                    $totaldead_alldef = array_sum($ownDeadTroops);
                    
                    //Collecting informations for the report
                    $unitsdead_def[0] = implode(",", $ownDeadTroops);
                    $unitsdead_deff[0] = '?,?,?,?,?,?,?,?,?,?,';
                    for($i = 1; $i <= 9; $i++){
                        //Counting reinforcements total dead troops
                        $deadTroops = array_slice($alldead, ($i - 1) * 10, 10);
                        $totaldead_alldef += array_sum($deadTroops);
                        //Collecting informations for the report
                        $unitsdead_def[$i] = implode(",", $deadTroops);
                        $unitsdead_deff[$i] = $unitsdead_deff[0];
                    }
                    $totaldead_alldef += ($deadhero + $alldead['hero']);

                    if (!isset($totalattackdead))  $totalattackdead = 0;
                    $totalattackdead += $totaldead_alldef;

                    // Set units returning from attack

                    $p_units = [];
                    for ($i = 1; $i <= 11; $i++) {
                        if (!isset(${'dead'.$i})) ${'dead'.$i} = 0;
                        if (!isset(${'traped'.$i})) ${'traped'.$i} = 0;
                        $p_units[] = "t".$i." = t".$i." - ".(${'dead'.$i} + ${'traped'.$i});
                    }

                    $database->modifyAttack3($data['ref'],implode(', ', $p_units));

                    $unitsdead_att = $dead1.','.$dead2.','.$dead3.','.$dead4.','.$dead5.','.$dead6.','.$dead7.','.$dead8.','.$dead9.','.$dead10;
                    $unitstraped_att = $traped1.','.$traped2.','.$traped3.','.$traped4.','.$traped5.','.$traped6.','.$traped7.','.$traped8.','.$traped9.','.$traped10.','.$traped11;

                    //top 10 attack and defence update
                    $totaldead_att = $dead1 + $dead2 + $dead3 + $dead4 + $dead5 + $dead6 + $dead7 + $dead8 + $dead9 + $dead10 + $dead11;
                    $totalattackdead += $totaldead_att;
                    $troopsdead1 = $dead1;
                    $troopsdead2 = $dead2;
                    $troopsdead3 = $dead3;
                    $troopsdead4 = $dead4;
                    $troopsdead5 = $dead5;
                    $troopsdead6 = $dead6;
                    $troopsdead7 = $dead7;
                    $troopsdead8 = $dead8;
                    $troopsdead9 = $dead9 + 1;
                    $troopsdead10 = $dead10;
                    $troopsdead11 = $dead11;
                    
                    // hero XP, player points and alliance points — extracted to calculateHeroXpAndPoints() [#155]
                    // BUG FIXED: pass the per-owner defense breakdown through so
                    // defense points get split across village owner + reinforcers
                    // instead of all going to $toF['owner'] alone (see the fix
                    // inside calculateHeroXpAndPoints() itself for the full story).
                    $totaldead_def = $this->calculateHeroXpAndPoints(
                        $alldead, $owndead,
                        [$dead1, $dead2, $dead3, $dead4, $dead5, $dead6, $dead7, $dead8, $dead9, $dead10, $dead11],
                        $targettribe, $att_tribe,
                        $Attacker, $AttackerHeroID, $Defender, $DefendersHeroID,
                        $toF, $from, $targetally, $ownally,
                        $heroxp, $defheroxp,
                        $battlepart['DefensePointShares'] ?? []
                    );

                    // resources lootable after cranny protection — extracted to resolveResourcesAfterBattle() [#155]
                    $resAfter = $this->resolveResourcesAfterBattle($data, $isoasis, $conqureby, $owntribe, $targettribe, $crannySpy);
                    $totclay = $resAfter['totclay'];
                    $totiron = $resAfter['totiron'];
                    $totwood = $resAfter['totwood'];
                    $totcrop = $resAfter['totcrop'];
                    $avclay  = $resAfter['avclay'];
                    $aviron  = $resAfter['aviron'];
                    $avwood  = $resAfter['avwood'];
                    $avcrop  = $resAfter['avcrop'];

                    // bounty distributed across the resources available after cranny protection (extracted to applyBounty() [#155])
                    // 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;
                    $chiefResult       = $this->handleConquest($data, $type, $dead9, $traped9, $targettribe, $from, $to, $toF, $owntribe, $varray, $varray1, $battlepart, $isoasis, $village_destroyed, $chief_pic);
                    $info_chief        = $chiefResult['info_chief'];
                    $chiefing_village  = $chiefResult['chiefing_village'];
                    $village_destroyed = $chiefResult['village_destroyed'];

                    // attacker hero outcome (oasis conquest/loyalty, artifact claim, report line) — extracted to handleHeroPostBattle() [#155]
                    $this->handleHeroPostBattle($data, $from, $to, $dead11, $traped11, $heroxp, $hero_pic, $isoasis, $type, $targettribe, $info_cat, $info_hero, $can_destroy, $village_destroyed);
                    
                    if ($DefenderID == 0) $natar = 0;

                    if (!empty($scout)) {
                        $info_spy = $this->buildScoutReport($data, $spy_pic, $isoasis, $targettribe, $crannySpy, $totwood, $totclay, $totiron, $totcrop);
                    }

                    // prisoners freed during conquest attacks — extracted to handlePrisoners() [#155]
                    $info_trap  = empty($scout) ? $this->handlePrisoners($data, $from, $to, $ownally, $type, $totalsend_att, $totaldead_att, $totaltraped_att) : '';
                    $info_troop = ($totalsend_att - ($totaldead_att + (isset($totaltraped_att) ? $totaltraped_att : 0)) <= 0) ? rc_tok('RC_NONE_RETURNED') : "";

                    // assemble data2 + data_fail — extracted to buildCombatReport() [#155]
                    $report    = $this->buildCombatReport(
                        $scout, $from, $to, $owntribe, $targettribe,
                        $unitssend_att, $unitsdead_att, $steal, $battlepart,
                        $unitssend_def, $unitsdead_def, $unitssend_deff, $unitsdead_deff,
                        $rom, $ger, $gal, $nat, $natar,
                        $hun, $egy, $spa, $vik,
                        $DefenderHeroesTot, $DefenderHeroesDead,
                        $info_ram, $info_cat, $info_chief, isset($info_spy) ? $info_spy : '',
                        $info_trap, $info_hero, $info_troop,
                        $data, $dead11, $herosend_def, $deadhero, $unitstraped_att,
                        $village_destroyed, $can_destroy, $catp_pic
                    );
                    $data2     = $report['data2'];
                    $data_fail = $report['data_fail'];

                    // notify the defender of the incoming battle/scout — extracted to sendBattleNotifications() [#155]
                    $this->sendBattleNotifications($scout, $battlepart, $from, $to, $targetally, $data2, $AttackArrivalTime, $unitsdead_att, $unitssend_att, $defspy, $info_troop, $totalsend_alldef, $totalsend_att, $totaldead_att, $totaltraped_att, $totaldead_alldef);
                    // surviving attacker troops return (report + movement + loot/RR/enforce) or die — extracted to finalizeReturnOrDeath() [#155]
                    $retDead = $retTraped = $retTroopsdead = [];
                    for ($i = 1; $i <= 11; $i++) { $retDead[$i] = ${'dead'.$i}; $retTraped[$i] = ${'traped'.$i}; $retTroopsdead[$i] = ${'troopsdead'.$i}; }
                    $this->finalizeReturnOrDeath($data, $from, $to, $owntribe, $type, $isoasis, $conqureby, $AttackArrivalTime, $targetally, $ownally, $data2, $data_fail, $chiefing_village, $DefenderWref, $AttackerWref, $steal, $retDead, $retTraped, $retTroopsdead, $totalsend_att, $totaldead_att, $totaltraped_att, $totalstolentaken);
                    if($type == 3 || $type == 4) $database->addGeneralAttack($totalattackdead);

                    if (!isset($village_destroyed)) $village_destroyed = 0;

                    // delete the target village if it was destroyed — extracted to handleVillageDestruction() [#155]
                    $this->handleVillageDestruction($village_destroyed, $can_destroy, $data, $to, $varray);

                    // remember the razed tile so any follow-up wave still queued in
                    // this same batch bounces home instead of fighting a phantom
                    // (now-deleted) village. Needed on top of the $to['wref'] check
                    // because getMInfo() is cached: a same-batch wave would still see
                    // the stale "alive" village row — see the guard right after
                    // resolveVillageTarget() (issue #298).
                    if ($village_destroyed == 1 && $can_destroy == 1) {
                        $razedTargets[$data['to']] = true;
                    }
                }else{
                    //units attack string for battleraport
                    $unitssend_att1 = ''.$data['t1'].','.$data['t2'].','.$data['t3'].','.$data['t4'].','.$data['t5'].','.$data['t6'].','.$data['t7'].','.$data['t8'].','.$data['t9'].','.$data['t10'].'';
                    $herosend_att = $data['t11'];
                    $unitssend_att= $unitssend_att1.','.$herosend_att;
                    
                    $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);
                    $peace = PEACE;
                    $data2 = $from['owner'].','.$from['wref'].','.$to['owner'].','.$owntribe.','.$unitssend_att.','.$peace;
                    $time = time();
                    $database->addNotice($from['owner'], $to['wref'], $ownally, 22,''.addslashes($from['name']).' attacks '.addslashes($to['name']).'', $data2, $time);
                    $database->addNotice($to['owner'], $to['wref'], $targetally, 23,''.addslashes($from['name']).' attacks '.addslashes($to['name']).'', $data2, $time);
                }

                //Update starvation data
                $database->addStarvationData($to['wref']);
                
                $data_num++;

                unset(
                    $Attacker
                    ,$Defender
                    ,$DefenderHeroesTotArray
                    ,$DefenderHeroesDeadArray
                    ,$DefenderHeroesTot
                    ,$DefenderHeroesDead
                    ,$unitssend_att
                    ,$unitssend_def
                    ,$battlepart
                    ,$unitsdead_def
                    ,$dead
                    ,$steal
                    ,$from
                    ,$data
                    ,$data2
                    ,$to
                    ,$data_fail
                    ,$owntribe
                    ,$unitsdead_att
                    ,$herosend_def
                    ,$deadhero
                    ,$heroxp
                    ,$AttackerID
                    ,$DefenderID
                    ,$totalsend_att
                    ,$totalsend_alldef
                    ,$totaldead_att
                    ,$totaltraped_att
                    ,$totaldead_def
                    ,$totalattackdead
                    ,$defheroxp
                    ,$AttackerWref
                    ,$DefenderWref
                    ,$troopsdead1
                    ,$troopsdead2
                    ,$troopsdead3
                    ,$troopsdead4
                    ,$troopsdead5
                    ,$troopsdead6
                    ,$troopsdead7
                    ,$troopsdead8
                    ,$troopsdead9
                    ,$troopsdead10
                    ,$troopsdead11
                    ,$DefenderUnit
                    ,$info_trap
                    ,$report);
            }
        }
    }

    private function bountysortOasis($oasisArray) {
        $crop = $clay = $wood = $iron = 0;
        foreach ($oasisArray as $oasis) {
            switch($oasis['type']) {
                case 1:
                case 2:
                    $wood++;
                    break;
                case 3:
                    $wood++;
                    $crop++;
                    break;
                case 4:
                case 5:
                    $clay++;
                    break;
                case 6:
                    $clay++;
                    $crop++;
                    break;
                case 7:
                case 8:
                    $iron++;
                    break;
                case 9:
                    $iron++;
                    $crop++;
                    break;
                case 10:
                case 11:
                    $crop++;
                    break;
                case 12:
                    $crop += 2;
                    break;
            }
        }
        return [$wood, $clay, $iron, $crop];
    }

    function getAllUnits($base, $use_cache = true) {
        global $database;

        $ownunit = $database->getUnit($base, $use_cache);
		$enforcementarray = $database->getEnforceVillage($base, 0);
		
		if(count($enforcementarray) > 0){
			foreach($enforcementarray as $enforce){
				for($i = 1; $i <= 90; $i++){
					$ownunit['u'.$i] += $enforce['u'.$i];
				}
			}
		}
		
		$enforceoasis = $database->getOasisEnforce($base, 0, $use_cache);
		if(count($enforceoasis) > 0){
			foreach($enforceoasis as $enforce){
				for($i = 1; $i <= 90; $i++){
					$ownunit['u'.$i] += $enforce['u'.$i];
				}
			}
		}
		
		$enforceoasis1 = $database->getOasisEnforce($base, 1, $use_cache);
		if(count($enforceoasis1) > 0){
			foreach($enforceoasis1 as $enforce){
				for($i = 1; $i <= 90; $i++){
					$ownunit['u'.$i] += $enforce['u'.$i];
				}
			}
		}
		
		$movement = $database->getVillageMovement($base);
		if(!empty($movement)){
			for($i = 1; $i <= 90; $i++){
				if(!isset($ownunit['u' . $i])){
					$ownunit['u'.$i] = 0;
				}
				
				$ownunit['u'.$i] += (isset($movement['u'.$i]) ? $movement['u'.$i] : 0);
			}
		}
		
		$prisoners = $database->getPrisoners($base, 1);
		if(!empty($prisoners)){
			foreach($prisoners as $prisoner){
				$owner = $database->getVillageField($base, "owner");
				$ownertribe = $database->getUserField($owner, "tribe", 0);
				$start = ($ownertribe - 1) * 10 + 1;
				$end = ($ownertribe * 10);
				for($i = $start; $i <= $end; $i++){
					$j = $i - $start + 1;
					$ownunit['u'.$i] += $prisoner['t'.$j];
				}
				$ownunit['hero'] += $prisoner['t11'];
			}
		}
		return $ownunit;
    }
    
    // Production of a single resource (1=wood, 2=clay, 3=iron, 4=crop) for the
    // given village field layout, used by the bounty/production catch-up. The
    // four resources only differ by: the production-building data global, the
    // booster building field-type(s) whose 'attri' adds a % bonus, and the oasis
    // bonus slot (resourceType - 1). Crop additionally has two boosters
    // (grain mill type 8, then bakery type 9 applied on the running total) and
    // the gold-club +25% crop bonus (b4) keyed on the village owner.
    private function bountyGetResourceProd($resArray, $oasisNumber, $resourceType) {
        global $bid1, $bid2, $bid3, $bid4, $bid5, $bid6, $bid7, $bid8, $bid9, $database;

        // BUG FIXED: defensive fallback in case a caller ever passes a razed
        // village's null resource array (updateRes() now guards its own call
        // path, but this closes the loop here too) - treat as "no fields" so
        // production is simply 0, instead of "Trying to access array offset
        // on null" (+ the resulting null used as an array key below, which
        // additionally raised "Deprecated: Using null as an array offset").
        if (!is_array($resArray)) $resArray = [];

        $prodBid = [1 => $bid1, 2 => $bid2, 3 => $bid3, 4 => $bid4][$resourceType];
        $boosterBid = [5 => $bid5, 6 => $bid6, 7 => $bid7, 8 => $bid8, 9 => $bid9];
        // Booster field-types per resource, in application order (crop: mill then bakery).
        $boosterTypes = [1 => [5], 2 => [6], 3 => [7], 4 => [8, 9]][$resourceType];

        $prod = 0;
        $holders = [];
        $boosterLevels = array_fill_keys($boosterTypes, 0);
        for($i = 1; $i <= 38; $i++) {
            $fieldType = $resArray['f'.$i.'t'] ?? null;
            if($fieldType == $resourceType) $holders[] = 'f'.$i;
            if($fieldType !== null && isset($boosterLevels[$fieldType])) $boosterLevels[$fieldType] = $resArray['f'.$i] ?? 0;
        }

        foreach($holders as $holder) $prod += $prodBid[$resArray[$holder]]['prod'];

        foreach($boosterTypes as $bt) {
            $level = $boosterLevels[$bt];
            if($level >= 1) $prod += $prod / 100 * (isset($boosterBid[$bt][$level]['attri']) ? $boosterBid[$bt][$level]['attri'] : 0);
        }

        $oasisIndex = $resourceType - 1;
        if($oasisNumber[$oasisIndex] > 0) $prod += $prod * 0.25 * $oasisNumber[$oasisIndex];

        if($resourceType == 4 && !empty($resArray['vref']) && is_numeric($resArray['vref'])) {
            $who = $database->getVillageField($resArray['vref'], "owner");
            $croptrue = $database->getUserField($who, "b4", 0);
            if($croptrue > time()) $prod *= 1.25;
        }

        return round($prod * SPEED);
    }
}
