Build powers, cast prereqs and power fail conditions from Powers.cfg

PowersBase(PowerTemplate, id) and PowerPrereq template constructors;
PowersManager.loadPowers fills both maps on the world and login servers
(ids 1..1465 in file order, identical to the DB IDs). The 3.10 target
type exceptions and applyClientTargetTypes are gone; the dead
BLEED-DOT-10.5-RANGE token override is removed (the power is
BLEED-DOT-10.5-RANGED and its DB token is already its hash).

Parity (JSON-Refactor-Plan/scripts/p48.py, every column): 1465/1465 ids,
tokens = SBStringHash(id), flags 1465/1465, prereqs 720/720 rows, fail
conditions 103/105 powers. Absent MAXNUM*TARGETS stay 7 (patch notes).

Value changes (cfg wins, user 2026-10-26):
- target types HLR-025 PC->CORPSE, HNT-023 PC->SELF, RNG-033 GROUP->SELF
- HNT-023 SPELL->SKILL, target select CLICK->NONE
- cast time RNG-033 3->0.2, ROG-002 10->12, WIZ-067 6->7
- WIZ-067 release 1->0, init ramp 0->-0.13; SCT-004 cost ramp 0.5->0
- recycle ALF-001 300->180, FGM-002/003 600->900
- WLR-018 category HEAL->none
- BRD-016, PRS-031 lose the power CastSpell fail condition
- 4 name typos and 167 descriptions from the cfg; 2 effect prereq
  messages (SAB-001, SAP-004) untruncated (unread)
- canCastWhileFlying true by default (client ctor; no live reader)
- category BOON-CLASS/BOON-RACE -> BOONCLASS/BOONRACE: GetSourceType now
  resolves them instead of logging an error (no effect blocks them)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MagicBotandClaude Opus 5.5 committed 2026-10-26 09:57:46 -04:00
1 parent 3cf966d0eb
commit ac72d2c8d2
4 files changed
+240 -50

No files matched your search

+196 -2
View File
@@ -9,10 +9,20 @@
package engine.powers;
import engine.mbEnums.AreaType;
import engine.mbEnums.CastingModeType;
import engine.mbEnums.CostType;
import engine.mbEnums.ExcludeType;
import engine.mbEnums.MonsterType;
import engine.mbEnums.PowerCategoryType;
import engine.mbEnums.PowerTargetType;
import engine.mbEnums.PowerType;
import engine.mbEnums.TargetSelectType;
import engine.parsers.cache.CacheParserUtil;
import engine.parsers.objects.wpak.CurveRamp;
import engine.parsers.objects.wpak.EffectTemplate;
import engine.parsers.objects.wpak.PowerTemplate;
import engine.util.Hasher;
import org.pmw.tinylog.Logger;
import java.sql.ResultSet;
@@ -22,6 +32,9 @@ import java.util.EnumSet;
public class PowersBase {
// Patch notes: "The Fury's persistent AEs now hit 10 targets instead of 7"; the client ctor leaves these unset
private static final int DEFAULT_MAX_TARGETS = 7;
public final EnumSet<MonsterType> monsterTypeRestrictions = EnumSet.noneOf(MonsterType.class);
public final ArrayList<ActionsBase> actions = new ArrayList<>();
public final ArrayList<PowerPrereq> effectPrereqs = new ArrayList<>();
@@ -285,6 +298,189 @@ public class PowersBase {
this.monsterTypeRestrictions.add(CacheParserUtil.readEnum(MonsterType.class, ct, this.UUID, "monsterTypeRestrict3"));
}
public PowersBase(PowerTemplate powerTemplate, int id) {
CurveRamp hateRamp;
CurveRamp costRamp;
CurveRamp recycleRamp;
CurveRamp initRamp;
this.UUID = id;
this.IDString = powerTemplate.power_id;
hateRamp = rampOf(powerTemplate.hatevalue_curve, "HATEVALUE");
costRamp = rampOf(powerTemplate.curves.get("COSTAMT"), "COSTAMT");
recycleRamp = rampOf(powerTemplate.curves.get("RECYCLETIME"), "RECYCLETIME");
initRamp = rampOf(powerTemplate.curves.get("INITTIME"), "INITTIME");
this.name = powerTemplate.power_name;
this.token = Hasher.SBStringHash(this.IDString);
this.isSpell = powerTemplate.power_type == PowerType.SPELL;
this.skillID = powerTemplate.icon_number; // the DB skillID column held the icon number; unread
this.skillName = powerTemplate.focus;
this.range = powerTemplate.range;
this.description = String.join(" ", powerTemplate.descriptions);
this.useHealth = powerTemplate.cost_type == CostType.HEALTH;
this.useMana = powerTemplate.cost_type == CostType.MANA;
this.useStamina = powerTemplate.cost_type == CostType.STAMINA;
this.setTargetType(powerTemplate.target_type.name());
this.setCategory(powerTemplate.category);
this.cost = powerTemplate.cost_amt;
this.costRamp = costRamp.ramp;
this.costRampAdd = costRamp.useAdd;
this.castTime = powerTemplate.init_time;
this.castTimeRamp = initRamp.ramp;
this.initRampAdd = initRamp.useAdd;
this.cooldown = powerTemplate.release_time;
this.recycleTime = powerTemplate.recycle_time;
this.recycleRamp = recycleRamp.ramp;
this.recycleRampAdd = recycleRamp.useAdd;
this.maxTrains = powerTemplate.maxlevel;
this.hateValue = powerTemplate.hatevalue;
this.hateRamp = hateRamp.ramp;
this.hateRampAdd = hateRamp.useAdd;
// Only CLICK sets a source: the old loader's NAME and NEARBYMOBS branches were unreachable
this.targetFromLastTarget = !this.targetSelf && !this.targetGroup
&& powerTemplate.target_sel == TargetSelectType.CLICK;
this.monsterTypePrereq = powerTemplate.monstertypeprereqs.isEmpty() ? "" : powerTemplate.monstertypeprereqs.get(0).name();
this.monsterTypeRestrictions.addAll(powerTemplate.monstertyperestricts);
this.isAOE = powerTemplate.area_type == AreaType.CONE || powerTemplate.area_type == AreaType.POINTBLANK
|| powerTemplate.area_type == AreaType.SPHERE;
this.useCone = powerTemplate.area_type == AreaType.CONE;
this.usePointBlank = powerTemplate.area_type == AreaType.POINTBLANK;
this.useSphere = powerTemplate.area_type == AreaType.SPHERE;
this.radius = powerTemplate.area_range;
this.setExcludeType(powerTemplate.exclude_type);
this.maxNumMobTargets = (powerTemplate.maxnummobtargets == 0) ? DEFAULT_MAX_TARGETS : powerTemplate.maxnummobtargets;
this.maxNumPlayerTargets = (powerTemplate.maxnumplayertargets == 0) ? DEFAULT_MAX_TARGETS : powerTemplate.maxnumplayertargets;
this.chantDuration = powerTemplate.pulseinfo_duration;
this.chantIterations = powerTemplate.pulseinfo_iterations;
this.isChant = powerTemplate.pulseinfo_iterations > 0;
this.allowedInCombat = powerTemplate.casting_mode == CastingModeType.COMBAT || powerTemplate.casting_mode == CastingModeType.BOTH;
this.allowedOutOfCombat = powerTemplate.casting_mode == CastingModeType.NONCOMBAT || powerTemplate.casting_mode == CastingModeType.BOTH;
this.regularPlayerCanCast = "PGOV_PLAYER".equals(powerTemplate.grantoverridevar);
this.canCastWhileMoving = powerTemplate.cancastwhilemoving;
this.canCastWhileFlying = powerTemplate.cancastwhileflying;
this.losCheck = powerTemplate.shouldcheckpath;
this.sticky = powerTemplate.sticky;
this.isAdminPower = powerTemplate.isadminpower;
this.requiresHitRoll = powerTemplate.hit_roll_yn;
for (PowerTemplate.EffectPrereq effectPrereq : powerTemplate.effectprereqs)
this.effectPrereqs.add(new PowerPrereq(effectPrereq));
for (PowerTemplate.EquipPrereq equipPrereq : powerTemplate.eqprereqs)
this.equipPrereqs.add(new PowerPrereq(equipPrereq));
for (PowerTemplate.EffectPrereq targetPrereq : powerTemplate.targeteffectprereqs_anded)
this.targetEffectPrereqs.add(new PowerPrereq(targetPrereq, true));
for (PowerTemplate.EffectPrereq targetPrereq : powerTemplate.targeteffectprereqs_ored)
this.targetEffectPrereqs.add(new PowerPrereq(targetPrereq, false));
for (EffectTemplate.ConditionEntry condition : powerTemplate.conditions)
switch (condition.condition) {
case CastSpell:
this.cancelOnCastSpell = true;
break;
case TakeDamage:
this.cancelOnTakeDamage = true;
break;
}
}
private CurveRamp rampOf(String curveName, String field) {
CurveRamp curveRamp;
if (curveName == null)
return CurveRamp.NONE;
curveRamp = CurveRamp.fromCurve(curveName);
if (curveRamp == null) {
Logger.warn("Power " + this.IDString + " " + field + " curve " + curveName + " has no ramp form");
return CurveRamp.NONE;
}
return curveRamp;
}
// CHANT and uncategorised powers are never harmful: the old loader tested the cost type against target type names
private void setCategory(PowerCategoryType categoryType) {
this.powerCategory = (categoryType == null) ? PowerCategoryType.NONE : categoryType;
this.category = (categoryType == null) ? "" : categoryType.name();
switch (this.powerCategory) {
case WEAPON:
this.isWeaponPower = true;
this.isHarmful = false;
if (this.skillName.equals("Bow") || this.skillName.equals("Crossbow") || this.skillName.equals("Archery"))
this.weaponRange = 1000f;
else if (this.skillName.equals("Throwing"))
this.weaponRange = 60f;
else
this.weaponRange = 15f;
break;
case HEAL:
case GROUPHEAL:
this.isHeal = true;
this.isHarmful = false;
break;
case TRACK:
this.isTrack = true;
this.isHarmful = false;
break;
case AE:
case AEDAMAGE:
case BREAKFLY:
case DAMAGE:
case DEBUFF:
case MOVE:
case SPECIAL:
case SPIREDISABLE:
this.isHarmful = true;
break;
case VAMPDRAIN:
this.vampDrain = true;
this.isHarmful = true;
break;
default:
this.isHarmful = false;
}
}
private void setExcludeType(ExcludeType excludeType) {
switch (excludeType) {
case CASTER:
this.isCasterFriendly = true;
break;
case GROUP:
this.isGroupFriendly = true;
this.isCasterFriendly = true;
break;
case ALLBUTGROUP:
this.isGroupOnly = true;
break;
case ALLBUTPETS:
this.mustHitPets = true;
break;
case NATION:
this.isNationFriendly = true;
this.isCasterFriendly = true;
break;
}
}
public void setTargetType(String targetType) {
this.targetPlayer = false;
@@ -362,8 +558,6 @@ public class PowersBase {
}
public int getToken() {
if (this.IDString.equals("BLEED-DOT-10.5-RANGE"))
return -369682965;
return this.token;
}