Extract the per-branch defender target resolution and battle-environment
setup into two private helpers: resolveVillageTarget() and
resolveOasisTarget(). Each returns the target owner (tribe/alliance), map
info, conquest flag and the battle parameters (wall, armory/blacksmith
tech, residence, siege masonry); the village helper also returns the
evasion inputs. Both are read-only (no DB writes).
The foreach body keeps handleEvasion(), buildDefenderUnits() and
buildAttackerUnits() as explicit, ordered calls, so the village and oasis
branches are now symmetric orchestration.
Behaviour-preserving. The building/tech reads now run inside the helper
before handleEvasion(); they read buildings and technology only (never the
troops handleEvasion() may move), so the result is unchanged. A few
dead locals are dropped (playerunit, wallgid, w; the redundant
DefenderUnit/def_ab re-inits).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the per-attack, target-independent context resolution (attacker
village/owner tribe and alliance, war references, base flags) into a
private helper. Read-only, behaviour-preserving: the three repeated
getCachedUser() lookups on the attacker owner are collapsed into one
(the user cache makes them idempotent).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The defender's units were gathered by two near-identical inline blocks
(village and oasis targets). Extract them into a single private method
buildDefenderUnits() returning the defender's own troops (normalised to
non-negative ints), the aggregated reinforcement totals (enforDefender) and
the raw reinforcement rows (enforcementarray).
Pure behaviour-preserving extraction:
- Both call sites assign the returned bundle; all downstream usages unchanged.
- The oasis reinforcement aggregation now uses the same isset-guarded loop as
the village one: identical numeric result, minus a latent PHP 8.3
"undefined array key" notice.
- The dead `$def_ab[$i] = 0` init that lived in the village normalisation loop
is dropped: it was unconditionally wiped by the later `$def_ab = []` before
any use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The marketplace send tab (17.tpl) was refactored with an empty <script>
block, which dropped the `haendler` (available merchants) and `carry`
(per-merchant capacity) globals that add_res()/upd_res() in unx.js rely on.
Without them `ic = haendler * carry` evaluates to NaN, so clicking the
"(capacity)" link next to a resource (or the resource icon) no longer fills
the input. Restore the two globals so the max-per-merchant fill works again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The attacking army was built by two near-identical inline blocks (village
and oasis targets). Extract them into a single private method
buildAttackerUnits() that returns the Attacker unit array (u<start..end> +
uhero) together with the catapult / ram / chief / scout unit ids used in the
report. The oasis target keeps its Nature siege/chief slots (37/38/39) via
the $isoasis flag.
Pure behaviour-preserving extraction: both call sites now assign the returned
bundle, so all downstream usages remain unchanged. The unit-id picks are
initialised to null (they are always set for the real attacker tribes 1/2/3/5;
only the unreachable Nature-attacker case differs, which silences a latent
PHP 8.3 undefined-variable notice).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the ram-damage handling out of sendunitsComplete() into a dedicated
private method applyRamDamage(). For a normal attack (type 3) with rams, it
computes the new wall level, updates it in the database (recounting the
village population when the wall is destroyed), builds the report fragment,
and recalculates the battle when the wall level changed.
Pure behaviour-preserving extraction: the battle-recalc context is passed in
a single $ctx array; the call site keeps the t7 guard and assigns the
returned battlepart / info_ram, so all downstream usages remain unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the trapper resolution block out of sendunitsComplete() into a
dedicated private method calculateTrappedUnits(). It computes how many
incoming attacker units are caught in the defender's traps (Gaul trapper
or Natar capital), updates the trap counters and the prisoners table, and
subtracts the trapped troops from the attacking army.
Pure behaviour-preserving extraction: the inline `${'traped'.$i}`
variables are rehydrated at the call site from the returned bundle, so all
downstream usages remain unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the attacker/defender total-population computation (and the two
getProfileVillages() lookups that feed it) out of the per-attack loop into a
dedicated private method. Behaviour-preserving: the method takes the initial
$defpop/$attpop (0 for villages, 500 for the oasis branch) and accumulates onto
them exactly as before, and returns the village lists ($varray/$varray1) used
later for the can-destroy check and handleConquest().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the unit/tribe rename: a few strings still used the old names.
- MANUAL_UDESC_11: "Les Massues" -> "Les Combattants au gourdin" (U11),
so the in-game manual description matches the unit name.
- TZ_MACEMAN: "Massue" -> "Combattant au gourdin" (used as the U11 label in
the alliance forum troop picker).
- Conquest help text: the loyalty-reduction units listed "COMMANDANTS, CHEFS"
(old Teuton name) -> "CHEFS, CHEFS DE TRIBU", matching Senator/Chief/
Chieftain (Romain/Germain/Gaulois) like the English version.
- Wonder-of-the-World lore: anglicism "Natarian" -> "natarien/natarienne"
(empire natarien, capitale natarienne, menace natarienne), consistent with
the Natar unit names.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The five troop-overview pages hardcoded the English title "Troops (X)",
so the in-game manual kept showing e.g. "Troops (Teutons)" even in French,
inconsistent with the now-corrected unit/tribe names. Replace the hardcoded
titles with the existing TROOPS and TRIBE1-5 constants (consistent with the
#186 manual i18n migration) so the title follows the active language,
e.g. "Troupes (Germains)" in French. en.php already defines all of these
constants as the idempotent fallback, so other languages are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Tribe 2 renamed Teutons -> Germains (the French name for this tribe),
including all manual/tooltip/description texts and adjective forms
(heros germain, armee germaine, troupes germaines).
- Teuton units: Massueur -> Combattant au gourdin (U11),
Hallebardier -> Combattant a la lance (U12),
Chevalier teutonique -> Cavalier teuton (U16), Commandant -> Chef (U19).
- Roman units: Imperial -> Imperian (U3) and official Latin cavalry names
Equites Legati/Imperatoris/Caesaris (U4-U6).
- Gaul units: Foudre de Teutates -> Teutates accent (U24),
Haeduen -> Hedui (U26).
- Natar units: anglicism natarian -> natarien (U46/U49/U50) and
"artefact natarian" -> "artefact natarien" in building descriptions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the pre-casualty defender report block into a new private method
collectReinforcementReport(): reset and rebuild the reinforcement unit totals
($DefenderEnf) and 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 and their masked variants), and
(re)initialise the per-tribe dead-hero accumulator. The bundle is returned to
the caller and the defender's folded hero count is reassigned at the call site.
Pure behaviour-preserving extraction. Method name matches the maintainer's
roadmap for #155.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the "kill other defence in village" block (reinforcement casualties)
into a new private method applyReinforcementCasualties(): compute each
reinforcement's dead units/hero, persist them (modifyEnforce), notify the
reinforcing players, and delete fully-wiped reinforcements. Dead counts are
accumulated into $alldead and dead heroes into $DefenderHeroesDeadArray (both
passed by reference); the tribe-presence flags (rom/ger/gal/nat/natar) are
returned to the caller.
Pure behaviour-preserving extraction. Also drops two dead recomputations of
$totalsend_att/$totaldead_att that sat inside the loop and were immediately
overwritten right after it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the in-village defender casualty handling (kill ratio applied to the
defender's own troops + hero, persisted via modifyUnit) out of the giant
sendunitsComplete() loop into a dedicated private method returning the
$owndead map. Pure behaviour-preserving extraction; also drops a dead $u
variable that was assigned but never read.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On some servers the NPC ("merchant") distribution screen could lock on
"Rest: NaN", hiding the trade button and making the NPC unusable. The "Rest"
value never returned to 0, so the player could not complete the trade.
Two layers of hardening:
* Client (Templates/Build/17_3.tpl):
- add a safe npcDiv() helper (returns 0 on a 0 divisor or non-finite value)
and route every division in portionOut() through it, so Infinity/NaN can no
longer leak into the inputs;
- in calculateRest(), coerce "Rest"/overall to a finite value (fall back to
the total when org4 cannot be parsed) so the submit button can always
reappear once the distribution is balanced;
- coerce each resource, $totalRes, $maxstore and $maxcrop to a valid,
non-negative integer before they reach the page/JS, so a resource computed
as NaN/INF cannot poison org4/summe in the first place.
* Server (Market::tradeResource): clamp each requested amount to [0, maxstore]
(crop to maxcrop) before persisting, so a forged or corrupted POST can no
longer write out-of-range resources.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The send/trade-route resource fields were capped at maxlength="5", limiting
input to 99,999 per resource. On high-speed servers a single convoy carries far
more (e.g. Teutons: 1000 x speed), so players could not fill their merchants.
Raise the cap to maxlength="9" (~1 billion), which covers any server speed.
The server already validates against available resources and merchant capacity,
so no server-side change is needed and no over-send is possible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an admin-controlled debug mode that captures PHP errors of all
players into var/log/debug-players.log, to hunt remaining PHP 8.3 bugs
from real play sessions. Fully transparent to players: no redirect, no
gameplay change, errors are never displayed.
- DB: new debug_log table (one row), mirroring the maintenance pattern.
- Database: getDebugMode()/setDebugMode()/setDebugSettings(), defensive
when the table is absent (no blank page).
- Session: register a custom error + shutdown handler when enabled; the
handler runs even when php.ini error_reporting masks warnings/notices,
so capture is complete without a Docker rebuild. Auto-disables after a
configurable window.
- DebugErrorLogger: size-capped file with a single .log.1 rotation,
honours the @ operator, never throws.
- Admin: new "Debug Error Log" page (levels, size cap, auto-off, on-page
viewer, clear, download) + debugLog action mod.
- Menu: admin-only quick on/off widget (TZ_DEBUG_ON/OFF, EN/FR/RO).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement the "Time Preference" section of the player preferences (issue
#198), which until now was stored in DB but never applied in-game.
procMtime() — the central date/time formatter used across reports, messages,
notices and troop movements — now honours the per-user settings:
- timezone: the stored value is mapped to a DateTimeZone, either a named
DST-aware region (Europe, UK, Turkey, Kolkata, Bangkok, New York,
Chicago, New Zealand) or a fixed UTC offset (general zones); timestamps
are converted before formatting.
- tformat: drives the date layout and 12h/24h clock (0=EU dd.mm.yy 24h,
1=US mm/dd/yy 12h, 2=UK dd/mm/yy 12h, 3=ISO yy/mm/dd 24h). tformat 0
keeps the previous EU output unchanged.
The today/yesterday sentinels are preserved (callers compare them) and are
computed in the player's timezone. The time-only path (mode 9) stays 24h
H:i:s because it feeds the live JS clock counters (unx.js), which parse
"H:i:s" by splitting on ":". A few alliance/report/farm-list rows that
combined procMtime's day part with a raw date('H:i') now take the time from
the same procMtime result, to avoid mixing timezones.
A second "local time" clock is shown next to the server-time one: a small
vanilla-JS snippet emitted once from menu.tpl finds the visible #tp1 clock
and appends a row ticking in the player's timezone (Date.now() + UTC offset),
independent of the browser timezone and of the unx.js counters. It aligns the
value under the server time, is skipped when the player's timezone matches
the server's, and lets the clock box grow so the extra row fits the frame.
Adds the LOCAL_TIME string (EN/FR/RO). When no player session is available
(admin / pre-login) procMtime falls back to the server timezone and EU
layout, i.e. the previous behaviour. Removes the last "not coded yet" tag
from the preferences form.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The #155 refactor that extracted buildCombatReport() turned one comma
into a literal dot in the non-scout data2 string:
...$info_chief.'.'.(isset($info_spy) ? ... // was ','
That dropped one CSV field, shifting every following field left by one
(hero troops t11, casualties, prisoners 182-192) and injecting a stray
"." into a numeric cell. Visible attack reports rendered with misplaced
hero/prisoner data, and the shifted text in the prisoner slots is what
made Templates/Notice/1.tpl array_sum() warn under PHP 8.
Restore the comma so the layout matches what the report template
(Notice/1.tpl) and the pre-refactor code expect. Reports stored before
this fix keep their corrupted string; the int-cast guard added in the
previous commit keeps them from re-emitting the warning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PHP 8.x turned several long-standing implicit accesses into runtime
warnings that flooded the error log during attacks and report viewing:
- Battle.report (Templates/Notice/1.tpl): the prisoner check ran
array_sum() over slots that may hold non-numeric text (the
"Information" line can shift indices), raising
"Addition is not supported on type string". Sum int-cast values.
- Database::getUnitsNumber(): movement/reinforcement arrays can be null
or miss tribe-specific unit keys, raising "Undefined array key" and
"Trying to access array offset on null". Null-coalesce to 0.
- Automation (combat resolver): $scout was only defined for scouting
attacks (type 1) yet always passed to buildCombatReport(), raising
"Undefined variable $scout". Initialize it for every attack type.
- Automation: the destroy-village check read $to['natar'] which is not
always set, raising "Undefined array key". Use empty().
Pure null-safety / behaviour-preserving changes; no gameplay logic
altered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Honour the per-user auto-completion checkboxes on the rally point and the
marketplace, where the target village is typed by name (dname field):
- v1: own villages
- v2: villages in the surroundings of the active village
- v3: villages of the player's alliance members
A new Database::getAutoCompleteVillages() builds the bounded, de-duplicated
name list from the enabled categories (system accounts excluded). A shared
Templates/villageAutocomplete.tpl renders a native <datalist> consumed by
the dname input via list="dnameSuggest"; nothing is emitted when every box
is unchecked, keeping the previous behaviour. Removes the "not coded yet"
tag from the auto-completion section in the preferences form.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Honour the per-user report-filter checkboxes in marketComplete():
- v4: recipient gets no report for transfers between own villages
- v6: recipient gets no report for transfers from foreign villages
- v5: sender gets no report for transfers to foreign villages
The user row is already fully loaded by getUserFields(), so the prefs
are read without extra queries. Removes the "not coded yet" tag from
the report-filter section in the preferences form.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
userinfo is served from a 30s session cache (Session::PopulateVar), so a
freshly saved preference (e.g. the large-map checkbox) only reflected after
the cache expired or a re-login — the box appeared unchanged on OK.
Invalidate the per-user session cache after saving so the redirected page
reloads the values from the DB right away.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ft=p2 handler (Profile::updatePreferences) only saved the language
column, so the large-map checkbox — and v1-v6 / timezone / tformat — never
persisted: the box reverted to unchecked on OK. The full save block in
preference.tpl was dead code (the handler header()+exit before it runs).
Persist the whole preferences form in the handler (checkboxes as 0/1,
timezone/tformat as validated ints) so the Large map preference actually
sticks. The other fields are stored now and applied in-game in later
steps of #198.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `map` user preference ("Show the large map in an extra window") was
saved to users.map but never applied in-game. When enabled, the large-map
button on the map now opens karte2.php in a separate browser window
instead of the in-page iframe overlay (default behaviour kept when the
preference is off).
Also drop the "(not coded yet)" marker from the Large map section of the
preferences page and wire its title to the existing LARGE_MAP constant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On a fresh install several strings were still hardcoded in English
(reported on the live server, RO locale):
- statistiken.php: the page <title>, the <h1> and the tab bar
(Player / Alliances / Villages / Heroes / General) were literal English.
- Templates/Ranking/ranksearch.tpl: the "back | forward" pagination links.
- Templates/troops.tpl: the "Hero" troop-row label.
Wire them to language constants. Reuse existing ones
(STATISTICS, PLAYER, PLAYERS, VILLAGES, BACK, FORWARD, U0) and add three
new constants (ALLIANCES, HEROES, GENERAL) to en/fr/ro. WW is kept as-is
(universal acronym).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase C of the sendunitsComplete() refactor (issue #155).
- handlePrisoners(): extracts the ~100-line block that releases troops
trapped in the defender's traps during a conquest attack (type 3),
sends them home with 25% casualties, repairs destroyed traps, and
returns the HTML fragment for the battle-report trap line.
- buildCombatReport(): extracts the assembly of the $data2 (main report)
and $data_fail (all-attacker-dead fallback) CSV strings, including the
"village destroyed" annotation in $info_cat and the final appending of
$info_trap / $info_troop / $info_hero.
Pure behaviour-preserving extractions — no logic change.
Validated by deterministic A/B harness (infantry, evasion, conquest).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The "no tasks" quest mode delivers a Plus + gold shipment on claim
(case '91', and the final case '97'). The reward was granted with a
non-atomic read-modify-write:
$gold = getUserField('gold'); $gold += 15; updateUserField('gold', $gold);
and the quest pointer / timer were advanced unconditionally. Under the
many concurrent ajax requests the game fires, a request that read gold/
plus before the claim and wrote the user row back after it would clobber
the freshly granted reward. The quest_time write (a literal value) still
survived, so the UI advanced to the next shipment countdown while the
player received neither Plus nor gold -- exactly the sporadic symptom in
the report.
Fix: gate the grant on an atomic conditional advance
(UPDATE ... SET quest = 91 WHERE quest = 90) and apply the reward with
in-place SQL increments (gold = gold + 15, plus = IF(plus > now, ...)),
matching the idiom already used in Templates/Plus/*.tpl. This makes the
claim idempotent (duplicate/concurrent requests grant nothing extra) and
immune to the lost-update race. Also fixes a latent case where an expired
Plus timestamp was extended in the past instead of reset to now.
Applied to both quest_core.tpl and quest_core25.tpl.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Localize the 17 "new features" manual pages (Templates/Manual/1111-1126.tpl
and the 1331.tpl index, typ=11/13) so they render in the player's language
instead of hardcoded English.
- Replace hardcoded page titles, descriptions and nav tooltips with constants.
- The 1331 index reuses the per-page title constants and gains
MANUAL_NF_ENABLED / MANUAL_NF_DISABLED for the on/off status labels.
- Add 33 constants each to GameEngine/Lang/{en,fr,ro}.php
(2 status labels + 15 page titles + 16 descriptions).
- en.php uses tz_def() as the English fallback; fr/ro use define().
- Reuse existing constants where they already exist: VACATION_MODE (page 19),
NEW_FEATURES, FORWARD, BACK, OVERVIEW.
it/zh fall back to English. RO strings pending native review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Localize the 51 in-game manual unit detail pages (Templates/Manual/1*.tpl,
typ=1) so they render in the player's language instead of English, continuing
the manual l10n started in the building pages PR.
What changed:
- 51 unit pages (Romans/Teutons/Gauls/Nature/Natars) now use translation
constants for every hardcoded string: tribe tags -> TRIBE1..5, the stat
labels (attack value, defence vs infantry/cavalry, Velocity, fields/hour,
Can carry, Duration of training), the unit descriptions, the NPC reference
notes and the "forward" nav.
- 48 new constants added to en/fr/ro:
* 7 shared stat labels (MANUAL_ATTACK_VALUE, MANUAL_DEF_INFANTRY,
MANUAL_DEF_CAVALRY, MANUAL_VELOCITY, MANUAL_FIELDS_HOUR, MANUAL_CAN_CARRY,
MANUAL_TRAINING_DURATION)
* 2 NPC reference notes (MANUAL_NPC_NATARS, MANUAL_NPC_NATURE)
* 1 templated Nature-animal blurb (MANUAL_UDESC_ANIMAL_EXP, used via
printf with the animal name + xp value, so the 10 animal pages share it)
* 38 unit descriptions (MANUAL_UDESC_*). Identical descriptions are shared:
settlers (110/120/130) and rams (117/127) reuse one constant each.
- Reuses constants already present upstream and in the building pages branch:
TRIBE1..5, LEVEL, FORWARD, CROP_CONSUMPTION, PREREQUISITES, UPKEEP, DURATION,
RESOURCES, U1..U50.
English render is unchanged: the English constant values are the existing page
text extracted verbatim (whitespace-collapsed). it/zh are intentionally left to
fall back to English via the en.php fallback wired up earlier.
Scope note: this builds on the manual building-pages l10n branch (reuses
CROP_CONSUMPTION). The 17 "new features" pages (typ=11/13) are out of scope and
will be a follow-up PR. RO strings are functional but would benefit from a
native review.