The 14 Magister Captain contracts' powers have no client rune or deed
(Task 3.9), so they live on the MinionType mage entries instead of the
DB fallback in loadAllRunePowers. canCast, mobCast and aiInfoCmd read
them through MinionType.getCaptainPowers.
No value change: same 56 power ids, rank 40, and all 56 DB tokens equal
the powerbase token of their power id (checked on mbdb_108).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client ArcCharacter_RecalcMaxAttributes (0x470440) and RecalcAttributes
(0x470610) treat the stats in the mob message as spent points and add
rune_attr_adj from all of the character's runes, clamped to
[1, max(1, sum of rune_max_attr_adj)]. The server summed only the race
rune and sent the result, so the client counted it twice.
Mob.getClientRuneIDs lists the runes serializeForClientMsgOtherPlayer
sends (race, plus Pet 2522 for pets, Warrior 2518 + Guard 252621 for
contract and guard mobs); getRuneAttribute applies the client rule.
calculateModifiedStats and the mob branch of CharacterSkill use it,
and the message now sends 0 (no spent points), as NPCs already do.
MobBaseStats (race rune only) has no users left and is removed.
Value changes (server side):
- Contract mobs and player guards: +100 to all five attributes
(Guard rune 252621; its max adj is also +100, so no new clamp).
- Pets: + Pet rune 2522 (Str 76, Dex 76, Con 120, Int 40, Spi 40;
max Str 500, Dex 500, Con 2000, Int 40, Spi 40).
- The new max clamp changes no loaded mobbase on its own (checked all
2375 against their race runes).
These feed mob skill values (CharacterSkill) and statXxxCurrent.
Guard ATR/defense/damage/health still come from the NPCManager guard
formulas.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mob message paired Dex with the Spirit hash (0xACB82E33), Int with
the Dexterity hash (0xE07B3336) and Spi with the Intelligence hash
(0xFF665EC3); the inline comments were wrong. Verified with
Hasher.SBStringHash. Mob and NPC now use MBServerStatics.STAT_*_ID.
Value change: the client now gets a mob's Dex, Int and Spi in the right
slots. NPCs send 0 for all five, so no change there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client builds a mob's scale from its race rune SCALEFACTOR
(ArcCharacter_BuildCharFromRune 0x458160) and overwrites the three floats
the server sends, so the DB column (always 1) never had a visible effect.
MobBase now holds rune_scale_factor (1,1,1 when absent) and the mob message
sends it. No client-visible change. The column drop is
sql/3.7-mobbase-scale-drop.sql, to run after this build is deployed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every static_npc_mobbase row has ID = loadID since 3.6 dropped the
server-made rows, so the race rune and rune effects are looked up by ID and
the loaders no longer filter on loadID. No value change (dev: 2375 of 2375
rows have ID = loadID). Mob.loadID and NPC.loadID are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LOAD_ALL_MOBBASE_SPEEDS and MobBase.updateSpeeds have had no caller since
3.3 moved mob speeds to rune_speed. No value change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AbstractCharacter.getResists fell back to Resists.getResists(0), but the
table has no ID 0, so the fallback always returned null. It now returns
the field directly. Removes dbResistHandler, the cache, the ResultSet
constructor and the empty setMobResists stub. No value change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MobBase builds its stats from the race rune's rune_attr_adj since 3.2, so
LOAD_STATS and the MobBaseStats ResultSet constructor have no callers.
No value change. The table is dropped by sql/3.6-mob-stats-resists-drop.sql.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each line now has the effects map key (effect UUID or stack type such as
PetBuff), the effect id and name, rank, time left or static, and the
power that applied it. Help and usage strings no longer describe
/printstats.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PowersBase.monsterTypeRestrictions is now an EnumSet<MonsterType>
(DB values Siege/Centaur/Dwarf/Vampire read with CacheParserUtil.readEnum).
MobBase gets monsterType from its race rune's rune_sub_type (all 2380
RACE runes convert); players use RaceType.getMonsterType(). No string
compares and no rune lookup per cast. Same results as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Players use their race rune's rune_sub_type, like mobs. Only GOR-003
Crimson Pact (single-target ghoul transformation) restricts player
types: it no longer lands on Centaur 2004/2005, Dwarf 2006 or Vampire
2028/2029, matching its description ("Cannot be cast on a vampire,
centaur, or dwarf"). No player race rune has sub-type Siege, so the
Siege-restricted PC/GROUP powers are unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
static_power_powerbase monsterTypeRestrict1-3 were loaded into
PowersBase.monsterTypeRestrictions but never read. getAllTargets now
drops a Mob whose race rune rune_sub_type is restricted (the rune
sub-type is the client's monster type, g_MonsterTypeEnum at rune +0xC).
86 powers restrict Siege (DB and Powers.cfg agree). Value changes:
- Imbue Golem, Dark Lord's Boon, Strengthen Beast no longer land on the
Siege-type summons Siege Golem 11999 (ANM-004) and Ice Fiend 11998
(SUM-005); they still buff the other pets.
- The 83 Siege-restricted harmful powers (bleeds, DOTs, procs, debuffs,
Challenge, The Lesser Kiss, ...) no longer affect Trebuchet 13171,
Mangonel 13757, Ballista 13758 or the two Siege summons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
validateTarget tested MASK_MOB before MASK_PET, and summoned pets carry
both, so every single-target PET power (pet heals and buffs such as
Repair Golem, Dark Lord's Favor, Strengthen Beast, Vashteera's Favor)
was rejected on any pet. Pets are now tested first and accept PET or
MOBILE powers, matching the client, where Mobile covers every ArcMobile
and Pet is left to the server (0x49E440). Non-pet mobs are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No power uses it after DRU-066 moved to the Powers.cfg Pet type, and its
area query (WorldGrid.getObjectsInRangePartialNecroPets) was an empty
stub. The DB rows are aligned with sql/necro-pet-targeting.sql.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DRU-066 Braialla's Judgement targets Pet and requires NECROPETSTATS, so
necro pets must be found by the Pet AoE query; the client leaves Pet
targeting to the server (0x49E440 has no Pet case). Reverts the necro
pet exclusion from 40799f96.
Other MASK_PET readers: MobAI pet aggro scan is still off
(PET_AGGRO_ENABLED = false); InterestManager and validateTarget test
MASK_MOB first, so they don't change. DRU-066 is the only AoE PET power.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PowersBase gets setTargetType (the old inline parse, now resetting the
flags first) and the world server re-applies every power's target type
from PowerTemplate after the wpak load. The login server only uses power
tokens and keeps the DB value.
Parity vs static_power_powerbase (1465 powers): 1460 identical, area,
radius type, exclude and target effect prereqs identical for all.
Value changes:
- NEC-018 Sepulchre's Bounty: PET -> MOBILE. Its AoE now collects mobs
and pets; the NECROPETSTATS target prereq keeps only necro pets.
- DRU-066 Braialla's Judgement: NECROPET -> PET.
Kept on the DB (user, behaviour change, for the Phase 4 power task):
HLR-025 Succor PC (cfg CORPSE), HNT-023 Phaedra's Fury PC (cfg SELF),
RNG-033 Fleet of Foot GROUP (cfg SELF).
Client: ArcPower_IsValidTargetForType 0x49E440 accepts any live
ArcMobile for Mobile and has no case for Pet (server decides).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mob effects come from the race rune (2.10b builder) and necro pets get
NECROPETSTATS in code, so GET_RUNEBASE_EFFECTS, the MobBaseEffects
ResultSet constructor, its unused mobBaseID and the equals/hashCode used
for the merge are gone. The table is now unread; drop it with
sql/3.4-mobbase-effects-drop.sql.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No client data grants NECROPETSTATS (no rune, summon power or item; no
reference in sb.exe), but NEC-018 Sepulchre's Bounty and DRU-066
Braialla's Judgement require it on the target, so the server adds it to
Grobones 12021 and Wight 12022 in applyMobbaseEffects.
Rank is the highest rank of the pet's own ladder (PET_NECRO /
PET_NECRO_HARDHIT) at or below its level, which is exactly what the 100
server-made static_npc_mobbase_effects rows held (same 50 rank/reqLvl
pairs). Tying it to that ladder is a guess at the official behaviour.
No value changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
static_npc_mobbase_effects is shrunk to the 100 NECROPETSTATS rows
(sql/3.4-mobbase-effects-shrink.sql); the Wild Boar 13236 PEACE row is
dropped as unused (no spawns, contracts or summon power).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MobBase now takes its effects from MobBaseEffects.getEffectsForRune (the
2.10b builder) and adds the DB rows the rune doesn't have on top.
Parity (scripts/p34.py, 2375 mobbases): 1028 identical, 1344 empty on
both sides, 3 with server-only DB rows that stay in the DB:
- 12021 Grobones, 12022 Wight: 50 NECROPETSTATS rows each
- 13236 Wild Boar: PEACE rank 40, reqLvl 1
No value changes. Effect order does not matter: applyMobbaseEffects keeps
the highest eligible rank per effect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MobBase walk/walkCombat/run/runCombat now come from rune_speed
WALK/COMBATWALK/RUN/COMBATRUN instead of static_npc_mobbase_race.
Parity (JSON-Refactor-Plan/scripts/p33.py, compared as Java floats):
2375/2375 loaded mobbases identical, every one has a race row on dev.
No value changes. The 253 unused race rows (no mobbase) and the
fly/swim columns had no reader.
LOAD_ALL_MOBBASE_SPEEDS and static_npc_mobbase_race are now unread;
they go in Task 3.6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
static_npc_mobbase_stats.baseSkills never matches a static_skill_skillsbase
token on dev (2007496699 on 2208 rows is no known skill hash and is absent
from the client), so applyMobbaseSkill never added a skill and only cost a
skillsbase query per mob spawn. No value changes. Nothing reads the
baseSkills/skillAmount columns now; the table goes in 3.6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parity (scripts/p32.py, static_npc_mobbase_stats by loadID vs rune_attr_adj):
2301 of 2375 mobbases identical. 74 have no stats row and went from all-zero
stats to the rune values: CSR 2020/2021, summoner pets 12024-12035 and
13266-13301, clay constructs 12200-12202/12375/12382, Guard 13170 (not spawned
or contracted), necro undead 13339-13348, and 13961, 14167, 14168, 14260-14262,
14285, 14292. On dev 47 obj_mob spawns use these (13343, 14167, 14168, 14285,
14292). Effect: higher rune skill values for these mobs via CharacterSkill.
LOAD_STATS stays until the table is dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 406 ID <> loadID mobbases (15000-17266) were junk: no RUNE file, nothing
on dev references them. MobBase now always reads its core fields from the
race rune and logs an error if the rune is missing; the loader skips any
ID <> loadID rows still present. GetClassType only matched those IDs, so it
always returned 2518 and is replaced by the constant.
DB: MBDATA/JSON-Refactor-Plan/sql/3.6-mobbase-cleanup.sql deletes the rows,
the 10 orphan static_npc_mobbase_effects rows (17061/17122/17154) and drops
13 columns no longer read: lootTableID, name, level, health, atr, defense,
minDmg, maxDmg, noaggro, raceType, fsm, notEnemy, enemy.
Value changes: none for the 2375 loaded mobbases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mobbases whose ID is their own race rune (2375 of 2781) now take name,
level, health, atr, defense, min/max damage, fsm and enemy/notEnemy from
the rune; the 406 server-made mobbases keep their DB row. Server-only
columns (bootySet, flags, seeInvis, spawnTime, goldMod, scale,
attackRange) stay DB.
Parity (MBDATA scripts/p31.py vs magicbane.sql), JSON for all (D2,
user decision 2026-10-23):
- fsm 84: DB None where the rune has no ROOTFSMID; no change
- enemy/notEnemy: no change
- level 95: rune 0 (player races, guards, pets, golems, necro pets)
- level 8: 13314 53->42, 13581 53->40, 13758 78->40, 14034 54->40,
14172 54->40, 14174 55->40, 14177 54->40, 13171 10->1
- health/atr/def/dmg 61: DB defaults 100/10/20 -> 0 (tombstones
2030-2040, pets 12024-12035 and 13266-13301, CSR 2020/2021 atr/def/dmg)
- defense 27: DB 100 -> rune 140-780 (clay, risen/doom, skeletal lines,
12011, 13961, 14167, 14168, 14285, 14292); damage/atr there and on
14037/14087/14296/14303 already came from the rune at runtime
- name 33: DB names -> obj_name (e.g. Sharduk -> Hell Hound)
No ParityCheck comparator: p31.py covers it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client ArcCharacter_ExpForLevelUp (0x4715D0): XP to reach level L is
(L-1)^3 * max(1, 1 + (L-40) * 0.0512f) * class * race rune_exp_req_to_level,
rounded half up. getLevel steps levels with it up to LEVELCAP. Both methods
now take the PlayerCharacter so the factors come from its runes; the CSR
promotions 2901+ have no rune and use the base class factor.
Same values: every player race is 10 and every class rune 15, and the formula
equals the removed table for all 76 levels. Levels above the table no longer
use the old approximate fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DB views char_perAccount and dyn_trash_detail lost their race and class
names when 2.11 dropped the Phase 2 tables. The world server now rewrites the
generated static_rune_lookup table (ID, type, name) after the second
Race.loadAllRaces(): 26 player races, 4 base classes, the promotion runes and
the CSR promotion rows 2901-2911. One transaction (delete, batch insert,
commit); on any failure it rolls back and logs a warning, the boot continues.
Names come from the runes: races use obj_name (as in 2.3), classes the rune
names, so DB typos such as "Theif" become "Thief" in the views. No gameplay
change. Needs JSON-Refactor-Plan/sql/2.12-rune-lookup.sql (table, level
functions, views) run before boot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deletes the uncalled static_rune_skill_adjusts, static_rune_maxskills,
static_skill_skillsgranted, static_skill_skillreq and static_power_powergrant
loaders and their ResultSet constructors. LOAD_RUNE_POWERS stays for the 14
Magister Captain guard contract ids. No value changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
static_rune_race is read only for the race ids and the server-only heights;
the hair/beard style, racebaseclass, baseclass and promotionrunereq queries
are gone. GET_BASE_CLASS is cache-only, which also drops the obj_account
cache-miss query. CSR/admin promotions 2901+ (no promotionrunereq rows) get an
empty allowed list. No value changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RuneBaseEffect, its handler, every effectsGranted field and getter (including
the PromotionClass promoID*10+2500..2503 lookups) and the empty
PlayerBonuses.grantEffect had no reader (2.10a). The GameObjectType constant
stays so later ordinals are unchanged. No value changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 315 static_rune_runebase rows are exactly the non-RACE runes, so the loader
selects by rune_type. Removes the server type/subtype columns (no live reader),
the runereq/overwrite path, the GET_RUNEBASE cache-miss query, the runebase
attribute handler, the racerune/baseclassrune loaders and the 111111 checks.
No value changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MobBaseEffects.getEffectsForRune builds each rune's list from the RUNE JSON
on first use and caches it: exact repeats are dropped (54 rune files list
their effects twice), the action id resolves to its effect through
PowerActions.cfg, token = SBStringHash(effect id), rank = arg 0,
reqLvl = arg 1. Used by Race, BaseClass, PromotionClass and RuneBase;
MobBase stays on GET_RUNEBASE_EFFECTS until Task 3.4.
Parity (scripts/p210b.py, token level): 1074 runes match. Differences:
- Tireless, Tough as Nails, Taint of Madness: JSON action TRT-* resolves
to the same effect the DB stores; no change
- CSR 2901-2904, 2906, 2908, 2910, 2911, Non-Aggro Rune 252624: DB token
was the NoNPCAggro action (no effect found, skipped); now NONPCAGGRO,
which has no mods; no gameplay change
- Mob-only 12021/12022 NECROPETSTATS and 13236 PEACE are server rows on
the MobBase path, untouched
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Files now load from mb.data/wpak/<archive>/<file>, matching MBDATA's
data/wpak layout and the client's Config/Config.wpak naming, so more
archives can sit beside Config/ later. All nine current files are in
wpak/Config/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaced by engine.parsers.wpak. Nothing referenced it except the
commented-out calls and the "Loading WPAK data" log line in
ConfigManager, which are removed too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SkillParser: uid, name, icon, attribute weights and description per
Skills.cfg line (94 skills; the server's ids 1-63 stay in the DB).
StartingKitParser: each kit line is a list of up to 10 item ids under
its RACE= line, as the client reads it; the file's tab columns are not
used (451 kits, same count as static_rune_validkit).
CharCreateRuneParser: race (with sex), class, promotion and attribute
creation rune lists. PowActionCostParser: resource costs per id, the
source of static_effect_costmaps (524 entries). No consumers change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PowerParser loads Powers.cfg into PowerTemplate._powers. The 22 header
fields are named after the client loader's error messages (0x56E410);
ACTION= lines keep trains, duration, curve and stack fields; prereqs,
curves, flags, messages and CONDITION blocks are kept. Keys compare
case-insensitively, as in the client. Four keys the client ignores
(APPLYDAMAGESELF=, APPLYEFFECTCASTER=, APPLYEFFECTTARGET= and a
POWEREND= typo) are skipped; any other unknown key fails the load.
Numbers are read like the client's atoi/atof with a warning when a
token isn't clean: PRL-033 init time "2.9.0" -> 2.9 and SGE-001
precision "10.0" -> 10, which matches the DB. Ten EQPREREQ lines end
without a required flag, which the client reads as 0.
Needs the fixed Powers.cfg (SRDX-DB header had "SPELL 15 Unknown"
pasted twice; fixed in MBDATA, findings section 6).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CompoundCurveParser keeps every curve segment (the CompoundCurveType enum
holds one value per curve). ModTypeParser keeps each mod type's Behavior,
which decides the argument layout of a MODS line. EffectParser loads
Effects.cfg into EffectTemplate._effects: header, the four flag keys
(case-insensitive, as in the client), SOURCE lines, every MODS line in
order, and CONDITION lines. Mod layouts follow the client's ArcMod*_Parse
functions; curve names in effects and power actions must exist in
CompoundCurves.cfg. Boot order: curves, mod types, power actions, effects.
No consumers change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New engine.parsers.wpak importer (replaces engine.wpak, which stays unused).
WpakFileReader tokenizes cfg lines (quotes, '#'/'*' comments, trailing '#')
and builds XxxBEGIN/XxxEND blocks; every error names file:line.
PowerActionParser loads PowerActions.cfg into PowerActionTemplate._powerActions,
keyed by action id, at world boot. Header layouts per action type come from
the client's ArcPowerAction*_ParseHeader functions: the trailing 0/1 on
ApplyEffect and friends is the aggressive flag, not a level. effect_ids holds
the action -> effect map needed by Task 2.10b. Three ids (PRE-235..237)
repeat identically; the later entry wins, as in the client. No consumers change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SkillReq.getSkillReqsForRune builds each rune's list from the RUNE JSON
on first use and caches it: skill_value is the grant level and
skill_granted_skills are the prereqs. The prereq amounts (80, Warrior 50,
Sun Dancer 60) are loaded but not yet read; CharacterSkill keeps its
hard-coded 80 and validForWarrior.
Parity (scripts/p29.py): 47 of 79 DB runes match. Value changes (JSON
wins, Task 2.9 decisions):
- Thief Dagger Mastery, Throwing, Unarmed Combat Mastery: level 15 -> 20
- Sentinel Axe/Great Axe/Hammer/Great Hammer/Sword Mastery: 20 -> 15;
gains Great Sword Mastery (15, Sword); loses Wear Armor, Medium
- Channeler loses Sword; Storm Lord loses Stormcalling
- Blood Horn gains Hammer Mastery (20, Hammer)
- No weapon prereq any more on Archer Archery, Blade Weaver Blade Weaving,
Sun Dancer Sun Dancing and the 11 Master of X talents
- No effect: Druid/Prelate Sword and Fury Spear 10 -> 1 (promotion is at
10), discipline/talent level changes (they need level 20), Throwing
prereqs (always granted), Blade Master Sword prereq (granted at 1)
- JSON-only runes (Archon, Duelist, War Correspondent, Pet, mob/guard
races) gain lists; Archon resistances and Duelist Dueling are not
skills and are skipped with a warning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Their item_power_action entries are mob power ranks by level, not trains,
and getGrantedTrains only looks up a player's race, class, promotion and
runes. RACE runes load only when they are a player Race (static_rune_race).
Cuts the table from 379384 entries / 969 powers to 2596 / 925; no value a
player can read changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Amount = power_arguments[0] of each power type's first action, keyed by
SBStringHash(power_type). Parity (scripts/p28.py): 69 of the 70 DB runes
match; Wizard 2521 WIZ-064 Conjure Familiar goes from 1 to 20 (decided JSON).
The JSON adds grants on 2189 runes: mob and guard races (2100+), Archon 2018
(ARCHON-001..005 40, CSR-UNINVIS 30), and NPC trainer/guard talents
(252569-252655, 253002). Only Archon is a player race; the talents are not
player runes. The DB loader stays until 2.11.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ends the name clash with the parser's engine.parsers.objects.PowerGrant.
It no longer extends AbstractGameObject: it was never cached or sent as a
game object, and the renamed class has no GameObjectType entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parity (scripts/p28.py): all 1066 DB runes with a JSON file match in value
and order; token = SBStringHash(power_type), rank = power_value, name = obj_name.
The 14 Magister Captain guard contract ids (848, 952, 1052, 1182, 1252, 1352,
1452, 1502, 1527, 1552, 1557, 1602, 1652, 980102) have no rune file; guard
spells are server-made, so they keep their DB rows.
The JSON adds 170 class/discipline/talent runes; none is a mobbase, and the
trainer contracts among them never reach the guard contract path in MobAI,
so no reachable value change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
applyRuneSkills checked mob.skills for the rune name rather than the skill
type, so every qualifying [rank, level] pair replaced the skill and only
the last one counted. Pairs at or below the mob's level now add up, the
same way trainer caps sum their pairs.
Value changes: only mobbases with several pairs for one skill, Guard Dog
12017 and Guard 13170. Only Guard Dog is in use (contract 910 Personal
Guard Dog); Guard 13170 has no spawns or contracts on dev. Example at
level 50: Dodge and Unarmed Combat 30 -> 110, Unarmed Combat Mastery
20 -> 80. Guard ATR/defense/damage formulas don't read these skills.
No mobbase has a base skill matching a rune skill, so applyMobbaseSkill
values are unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Load moved after the rune templates are parsed.
Parity (scripts/p27.py): 76 of 107 DB runes match. Value changes (JSON
wins, Task 2.7 decisions):
- Blade Master 3005: Sword +10 removed (no rune_skill_adj in the JSON;
Blade Weaver 3006 keeps Sword +10). Reaches players via PlayerBonuses.
- Pet 2522: Dodge and Unarmed Combat 1 -> 22. Not reachable; players
never hold the Pet rune.
- 29 Master X / trainer runes lose their rows (no level-0 pairs in the
JSON). Not reachable; players never hold trainer runes. Their caps
still load into MaxSkills.
- 2243 mob/guard race runes gain rows. Not reachable; PlayerBonuses
only reads player race/class/promotion/held runes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parity (scripts/p27.py): all 78 DB runes match (token, skillLevel = pair
level, maxSkillPercent = pair rank). Talents only, as decided in Task 2.7:
class/discipline/race adjusts are skill grants, and contracts whose class
ID is such a rune (Thief, Huntress, Warlock, Fighter, 8 disciplines,
Vampire) would otherwise gain trainer caps.
The selector adds Brawler 250054 and guard talents 252621-252623; no
contract keys on them, so no reachable value change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parity (scripts/p27.py): all 2259 DB runes and 69290 rows match in value
and order; token = SBStringHash(skill_type), name = obj_name.
The JSON adds 95 class/discipline/talent runes; applyRuneSkills reads by
mobbase ID and none of them is a mobbase, so no reachable value change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>