ValidRace/ValidClass/ValidDiscipline use the single req set plus the
restrict flag (true = blacklist, false = whitelist) instead of the legacy
req/res split. Offline parity (p15c.py, 7586 rows): 0 differences in
restrict or set for race, class and disc.
Value changes: none for equip checks. The legacy disc blacklist branch was
inverted (passed only when a listed disc was held); the new check passes
when none is held. No ITEM has a disc blacklist (only 19 RUNEs), and
ValidDiscipline has no callers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EnumSet.contains(EnumSet) is never true; use containsAll so the 350
weapons with item_eq_slots_and {LHELD, RHELD} block the other hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Range, speed, damage list and swing anims now come from
engine.parsers.objects.ItemTemplate.getWeapon(templateID). Non-weapons and
missing templates get an empty block, matching the legacy zero defaults.
Offline parity (scripts/p15b.py, 811 WEAPON rows vs COBJECTS/ITEM): 0
differences in speed, range, projectile, idle anim, damage list or either
anim list. No duplicate damage types exist, so sum vs first is moot; first
entry is used as before. 26 weapons (ammo, lutes, no-stats crossbows) have an
empty damage list, same as the DB. No value changes. No comparator removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resists.getArmorResists and NPCManager.applyEquipmentResists now read
cacheTemplate.getCombatAttackResist() with DamageType keys instead of the
legacy String-keyed map, and default a missing key to 0 instead of an
unboxing NPE.
Offline parity (scripts/p15a.py, all 7586 static_item_templates rows vs
the template CObjectIndex.lookup picks): 0 differences in the full
combat_attack_resist map. Every ITEM/DEED/RUNE template carries all 16
keys, so no value changes. No comparator added or removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shrine boons read getItemOfferingInfo() and the banestone and bane
production paths read getItemBaneRank() on template.cacheTemplate.
The legacy parser only kept item_bane_rank when ACTIONRESPONSE is
4250517122; the cache parsers always read it. Parity shows this makes
no difference: every template outside that gate has bane rank 0.
Parity (offline, scripts/p14.py, 7586 templates): 0 differences in
item_offering_info and item_bane_rank. No value changes expected.
JSON-Refactor-Plan Task 1.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
item_defense_rating, item_bulk_factor and item_primary_attr are
Item-only fields, not on CObjectTemplate, so their consumers read
engine.parsers.objects.ItemTemplate._items by templateID, like the
RuneTemplate and DeedTemplate consumers. The primary attr checks
compare with == because the cache template leaves the attr null
(legacy: None) on non-weapons.
Parity (offline, scripts/p14.py, 7586 templates): 0 differences in
item_defense_rating, item_bulk_factor and item_primary_attr. Every DB
ARMOR and WEAPON template is in the ITEM folder, so both sides gate
these fields on the same templates. No value changes expected.
JSON-Refactor-Plan Task 1.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consumers of the legacy ItemTemplate item_wt, item_value and
item_base_name now read getItemWt(), getItemValue() and
getItemBaseName() on template.cacheTemplate.
Parity (offline, scripts/p14.py, 7586 templates): 0 differences in
item_wt, item_value and item_base_name. No value changes expected.
JSON-Refactor-Plan Task 1.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every consumer of the legacy ItemTemplate item_type now reads
template.cacheTemplate.getItemType(). Left on the legacy field: the
type count in dbItemHandler.LOAD_ITEM_TEMPLATES, which runs before the
bridge is linked. The charter checks already read DeedTemplate.
Parity (offline, static_item_templates vs the template
CObjectIndex.lookup picks, scripts/p14.py, 7586 templates: ITEM 4011,
DEED 880, RUNE 2695): 0 differences in item_type. No value changes
expected.
JSON-Refactor-Plan Task 1.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbstractCharacter mob damage/ATR and CombatManager.getNaturalAttack now
read RuneTemplate._runes instead of the legacy ItemTemplate map.
Parity (offline, static_item_templates vs COBJECTS/RUNE, 2695 runes):
0 differences in rune_min_damage, rune_max_damage, rune_attack and
rune_naturalattacks. All 2366 runes with natural attacks use the
10-value layout. No value changes expected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ResourceType.template was filled in the enum constructor, which can run
before ItemTemplate.templates is loaded, leaving it null. lootBuilding was
its only reader and threw on the first non-gold resource.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
destroyOrDerank nulled city.warehouse on destruction, so the loot click
found no warehouse and returned silently. The destroyed Warehouse is now
held in Warehouse.destroyedWarehouses keyed by the rubble building UUID
until it is looted empty, the rubble is destroyed, or the city places a
new warehouse. Contents are not persisted across a reboot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
applyRunesForNPC already adds 252620, and most rune sets repeat it, so
NPCs with a set listed the shopkeeper rune twice after the runeSet fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A siege engine switches to whoever damages it, player or pet, even when
it already has a target. Pre-roll melee retaliation, harmful-power
direct aggro (which also fires on misses) and Taunt no longer change a
siege engine's target. The attack timer is untouched, so a switched
engine still waits for its next cycle before firing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unarmed mobs now add their rune natural attack range in processAttack
and use it in getRange. Most melee runes have 0, so they are unchanged;
ranged plant mobs (Gasser, Spitter, Needler, ...) get 150, Shuggra 50.
Natural range is not subject to the unverified 80 clamp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Siege engine range was hardcoded to 300. Read it from the rune natural
attack instead (350 for Trebuchet, Mangonel and Ballista), matching the
client's RecalcMinWeaponRange. Falls back to 300 if the rune has none.
getNaturalAttack now returns the whole entry so speed and range share
the lookup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extend getNaturalAttackSpeed to resolve the rune from the attacker type:
mobbase rune for mobs, race rune for players. Unarmed players now swing
at their race rune speed (20 = 2.0 s per hand) instead of a flat 5000 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unarmed mobs used a flat 5000 ms swing delay. Parse rune_naturalattacks
from the rune template and use its speed (x100 ms) instead, with the
AttackDelay modifier and 1.0 s floor applied as for weapons. Siege
engines now fire every 17.5 s (speed 175); most creatures swing every 3.5 s.
Falls back to 5000 ms when the rune has no natural attack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>