1. Spies — real bug, found. In calculateBattle, the block that calculates the attack power of spies had the hardcoded condition if ($i == 4 || $i == 14 || $i == 23 || $i == 44). Scouts of new tribes (52/64/74/82) did not enter the branch → attack power 0, so they all died no matter how many there were. Now the condition is in_array($i, $unitsbytype['scout']). With this, all four lists in Battle (cavalry, catapults, rams, scouts on attack + scouts on defense) are derived from unitsbytype.
2. Defender hero. I rebuilt the report CSV with dummy values and confirmed that the indexes in Notice/1.tpl are correct (272 = hero present, 273 = hero dead) — so the problem was that $deadhero was ending up as 0 in the report, even though deadherodef was 1 in the battle result (that's why the hero actually died in the DB). Now $deadhero is taken directly from the battle result ($battlepart['deadherodef']), with $owndead['hero'] only as a fallback.
3. "Free" healing. The code called modifyResource(..., mode 0) exactly like training, so the resources were low — but at 100x speed and full storage (8M/8M in your captures) the difference is instantly invisible. I did find a real bug in this area though: modifyResource caps at 0 instead of failing, so if you don't have resources you heal for free. Now procHeal checks the stock and, if there are insufficient resources, automatically reduces the quantity to what you can afford (or cancels).
NEW TRIBES ENHANCEMENT (Huns, Egipteans, Spartans & Vikings) #327
Please be advised, is not fully tested so if you activate on install maybe is not fully working
The Brewery (building 35, Teuton-only) grants a +1% attack bonus per level
to the whole empire, but only while a Mead-Festival is running (72h), and the
building is capital-only. The attack-bonus block in calculateBattle() ignored
both rules: it read the Brewery level from $AttackerWref (the *launching*
village, which usually has no Brewery) and applied the bonus purely on the
building level, with no festival check.
As a result the bonus never reacted to the festival being started or expired:
attacks with and without an active festival produced identical losses, so the
army appeared unaffected by the festival (issue #294). Depending on whether the
attack was launched from the capital, the bonus was either permanently on or
permanently off, but never gated by the festival.
Read the Brewery level from the attacker's capital and gate the bonus on the
festival being active, mirroring the catapult-randomization gate in Units.php
and the chief-penalty gate in Automation.php. The simulator passes
AttackerID = 0, so it keeps a 0 bonus exactly as before.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
procDistanceTime() multiplied the whole travel distance by the Tournament
Square speed factor as soon as the distance reached TS_THRESHOLD. That
made the trip time jump down at the threshold, so a target just past it
arrived dramatically sooner than a nearer one (e.g. a village 41 tiles
away raided faster than one 18 tiles away).
In T3.6 the Tournament Square only speeds up the part of the journey
beyond the threshold: the first TS_THRESHOLD tiles are walked at base
speed and the remainder at the boosted speed. Split the computation
accordingly so travel time stays monotonic with distance while still
rewarding a high-level square.
This is a long-standing bug, unrelated to the Generator refactor (which
only reformatted the same whole-distance multiplication). The same fix is
applied to the duplicate procDistanceTime() in Admin/database.php used by
the admin troop-return helper.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to #309. That fix only skipped follow-up waves whose target
was razed within the same resolution batch (an in-memory set). But waves
timed a second apart land in separate ticks: the first razes the village,
the next tick re-resolves the follow-up wave against a village that no
longer exists. getMInfo() (wdata LEFT JOIN vdata) then returns NULL vdata
columns, so the return trip is computed from a NULL wref (NULL coords) ->
a bogus arrival time that strands the troops forever (report against
"[?]"); DelVillage() does not reliably bounce every in-flight wave
either, leaving the attack at proc=0 and re-fetched every tick.
Detect the razed target from DB truth: after resolveVillageTarget(), if
the village is gone ($to['wref'] is NULL) or it was razed earlier in this
same batch ($razedTargets, still needed because getMInfo() is cached and
a same-batch wave would otherwise see the stale "alive" row), bounce the
whole army straight home and mark the movement processed instead of
fighting a phantom. setMovementProc() returns true only when it flips
proc 0->1, so we never duplicate a return DelVillage() already created.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
When several catapult waves land in the same resolution tick,
sendunitsComplete() fetches them all into one in-memory batch. The first
wave can raze the target village: handleVillageDestruction() -> DelVillage()
already bounces every still-in-flight attack straight home (symmetric return
time) and deletes the vdata row, leaving the wdata tile as an empty valley.
The loop then kept resolving the remaining waves from the in-memory batch
against the now-deleted village. getMInfo() LEFT-JOINs wdata onto the missing
vdata, so every vdata column (including wref) comes back NULL. The return
trip in finalizeReturnOrDeath() is then computed from a NULL wref (NULL
coordinates), producing a bogus arrival time that strands the troops, and a
duplicate of the bounce DelVillage() had already issued. The attacker sees
the waves "coming to nowhere and never coming back" (issue #298).
Track the tiles razed during the current batch and skip any follow-up wave
that targets one of them: those troops were already bounced home by
DelVillage(). No change to the battle maths; for a normal (non-razed) target
the set stays empty, so behaviour is preserved.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>