// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // Magicbane Emulator Project © 2013 - 2024 // www.magicbane.com package engine.wpakpowers; import engine.InterestManagement.WorldGrid; import engine.gameManager.ChatManager; import engine.gameManager.DbManager; import engine.gameManager.DispatchManager; import engine.gameManager.SessionManager; import engine.job.JobContainer; import engine.job.JobScheduler; import engine.jobs.FinishRecycleTimeJob; import engine.jobs.WpakUsePowerJob; import engine.math.Vector3fImmutable; import engine.mbEnums; import engine.net.Dispatch; import engine.net.client.ClientConnection; import engine.net.client.msg.ModifyHealthMsg; import engine.net.client.msg.PerformActionMsg; import engine.net.client.msg.RecyclePowerMsg; import engine.net.client.msg.UpdateStateMsg; import engine.objects.*; import engine.server.MBServerStatics; import engine.util.Hasher; import engine.util.Pair; import engine.wpak.EffectsParser; import engine.wpak.PowerActionParser; import engine.wpak.PowersParser; import engine.wpak.data.Effect; import engine.wpak.data.*; import org.pmw.tinylog.Logger; import java.util.HashMap; import java.util.HashSet; import static engine.math.FastMath.sqr; public class WpakPowerManager { public static HashMap _effectsLookup = new HashMap<>(); public static HashMap _powerActionLookup = new HashMap<>(); public static HashMap _powersLookup = new HashMap<>(); private static JobScheduler js; public static void init() { EffectsParser.parseWpakFile(); PowersParser.parseWpakFile(); PowerActionParser.parseWpakFile(); } public static void beginCast(final PerformActionMsg msg, ClientConnection origin, boolean sendCastToSelf) { if (executePower(msg, origin, sendCastToSelf)) { // Cast failed for some reason, reset timer RecyclePowerMsg recyclePowerMsg = new RecyclePowerMsg(msg.getPowerUsedID()); Dispatch dispatch = Dispatch.borrow(origin.getPlayerCharacter(), recyclePowerMsg); DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY); // Send Fail to cast message PlayerCharacter pc = SessionManager.getPlayerCharacter(origin); if (pc != null) { sendPowerMsg(pc, 2, msg); if (pc.isCasting()) { pc.update(); } pc.setIsCasting(false); } } } private static boolean executePower(final PerformActionMsg msg, ClientConnection origin, boolean sendCastToSelf) { //check to see if the caster is valid PlayerCharacter playerCharacter = SessionManager.getPlayerCharacter(origin); if (playerCharacter == null) return false; //make sure player is still alive if (!playerCharacter.isAlive() && msg.getPowerUsedID() != 428589216) { //succor RecyclePowerMsg recyclePowerMsg = new RecyclePowerMsg(msg.getPowerUsedID()); Dispatch dispatch = Dispatch.borrow(playerCharacter, recyclePowerMsg); DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY); return false; } //make sure the recycle timer has actually elapsed if (playerCharacter.getRecycleTimers().containsKey(msg.getPowerUsedID())) { Logger.warn("usePowerA(): Cheat attempted? '" + msg.getPowerUsedID() + "' recycle timer not finished " + playerCharacter.getName()); return false; } //lookup the power that was cast Power powerCast = _powersLookup.get(msg.getPowerUsedID()); if (powerCast == null) { ChatManager.chatSayInfo(playerCharacter, "This power is not implemented yet."); return true; } if (playerCharacter.getLastPower() != null) return true; // get numTrains for power int trains = msg.getNumTrains(); if (trains > powerCast.maxLevel) { trains = powerCast.maxLevel; msg.setNumTrains(trains); } //sanity check for amount of trains in spell cast if (playerCharacter.getPowers() != null && playerCharacter.getPowers().containsKey(msg.getPowerUsedID())) { CharacterPower cp = playerCharacter.getPowers().get(msg.getPowerUsedID()); if (cp != null) { int tot = cp.getTotalTrains(); if (tot == 0) return false; if (trains != tot) { trains = tot; msg.setNumTrains(trains); } } } //get casting time int time = getRecycleTime(powerCast, trains); //combat mode sanity check if (playerCharacter.isCombat()) { if (!allowedInCombat(powerCast)) return true; } else if (!allowedOutOfCombat(powerCast)) return true; //stunned check PlayerBonuses bonus = playerCharacter.getBonuses(); // mbEnums.SourceType sourceType = mbEnums.SourceType.GetSourceType(powerCast.category); // if (bonus != null && (bonus.getBool(mbEnums.ModType.Stunned, mbEnums.SourceType.None) || bonus.getBool(mbEnums.ModType.CannotCast, mbEnums.SourceType.None) || bonus.getBool(mbEnums.ModType.BlockedPowerType, sourceType))) // return true; //sanity check for casting while moving Vector3fImmutable endLoc = playerCharacter.getEndLoc(); if (!powerCast.canCastWhileMoving) if (playerCharacter.isMoving()) { float distanceLeftSquared = endLoc.distanceSquared2D(playerCharacter.getLoc()); if (distanceLeftSquared > sqr(playerCharacter.getSpeed())) return true; } //get the actual target form the message int type = msg.getTargetType(); int UUID = msg.getTargetID(); if (type == -1 || type == 0 || UUID == -1 || UUID == 0) return true; AbstractWorldObject target = (AbstractWorldObject) DbManager.getObject(mbEnums.GameObjectType.values()[type], UUID); //check to make sure power can be cast on building if target is a building if (target != null && target.getObjectType() == mbEnums.GameObjectType.Building && !powerCast.target_type.equals(mbEnums.PowerTargetType.BUILDING)) { sendPowerMsg(playerCharacter, 9, new PerformActionMsg(msg)); return true; } //validate casting range if (playerCharacter.getLoc().distanceSquared2D(msg.getTargetLoc()) > (powerCast.range * powerCast.range)) return true; //validate prereqs for power cast //equipment prereqs if (!powerCast.equipmentPreReq.isEmpty()) for (EquipmentPreReq prereq : powerCast.equipmentPreReq) { String requiredSkill = prereq.skill; if (playerCharacter.charItemManager.equipped.get(prereq.slot) != null) { Item equippedItem = playerCharacter.charItemManager.equipped.get(prereq.slot); if (!equippedItem.template.item_skill_mastery_used.equals(requiredSkill) && !equippedItem.template.item_skill_used.equals(requiredSkill)) return true; } else { return true; } } //effect prereqs if (!powerCast.effectPreReqs.isEmpty()) { for (Effect prereq : powerCast.effectPreReqs) { if (!playerCharacter.effects.contains(prereq.effect_id) && !playerCharacter.effects.contains(prereq.effect_name)) return true; } } float cost = getCost(powerCast, trains); if (bonus != null) cost *= (1 + bonus.getFloatPercentAll(mbEnums.ModType.PowerCost, mbEnums.SourceType.None)); if (playerCharacter.getAltitude() > 0) cost *= 1.5f; if (cost > 0) if ((playerCharacter.getObjectTypeMask() & MBServerStatics.MASK_UNDEAD) != 0) if (playerCharacter.getHealth() <= cost) return true; else { playerCharacter.modifyHealth(-cost, playerCharacter, true); ModifyHealthMsg mhm = new ModifyHealthMsg(playerCharacter, playerCharacter, -cost, 0f, 0f, 0, null, 9999, 0); mhm.setOmitFromChat(1); DispatchManager.dispatchMsgToInterestArea(playerCharacter, mhm, mbEnums.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false); } else if (powerCast.costType.name().equals("MANA")) if (playerCharacter.getMana() < cost) return true; else playerCharacter.modifyMana(-cost, playerCharacter, true); else if (powerCast.costType.name().equals("STAMINA")) if (playerCharacter.getStamina() < cost) return true; else playerCharacter.modifyStamina(-cost, playerCharacter, true); else if (playerCharacter.getHealth() <= cost) return true; else playerCharacter.modifyHealth(-cost, playerCharacter, true); if (time > 0) { FinishRecycleTimeJob frtj = new FinishRecycleTimeJob(playerCharacter, msg); playerCharacter.getRecycleTimers().put(msg.getPowerUsedID(), js.scheduleJob(frtj, time)); } else { // else send recycle message to unlock power RecyclePowerMsg recyclePowerMsg = new RecyclePowerMsg(msg.getPowerUsedID()); Dispatch dispatch = Dispatch.borrow(playerCharacter, recyclePowerMsg); DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY); } int tr = msg.getNumTrains(); DispatchManager.dispatchMsgToInterestArea(playerCharacter, msg, mbEnums.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, sendCastToSelf, false); //Make new msg.. PerformActionMsg copyMsg = new PerformActionMsg(msg); copyMsg.setNumTrains(tr); // make person casting stand up if spell (unless they're casting a chant which does not make them stand up) if (powerCast.isSpell() && !powerCast.isChant() && playerCharacter.isSit()) { playerCharacter.update(); playerCharacter.setSit(false); UpdateStateMsg updateStateMsg = new UpdateStateMsg(playerCharacter); DispatchManager.dispatchMsgToInterestArea(playerCharacter, updateStateMsg, mbEnums.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false); } // update cast (use skill) fail condition playerCharacter.cancelOnCast(); // update castSpell (use spell) fail condition if spell if (powerCast.isSpell()) playerCharacter.cancelOnSpell(); // get cast time in ms. time = getCastTime(powerCast, trains); // set player is casting for regens if (time > 100) { playerCharacter.update(); playerCharacter.setIsCasting(true); } playerCharacter.setLastMovementState(playerCharacter.getMovementState()); // run timer job to end cast if (time < 1) // run immediately finishUsePower(copyMsg, playerCharacter, target); else { WpakUsePowerJob upj = new WpakUsePowerJob(playerCharacter, copyMsg, target); JobContainer jc = js.scheduleJob(upj, time); // make lastPower playerCharacter.setLastPower(jc); } return false; } public static void finishUsePower(PerformActionMsg msg, PlayerCharacter caster, AbstractWorldObject target) { Power powerUsed = _powersLookup.get(msg.getPowerUsedID()); if (powerUsed == null) { Logger.error("Invalid power: " + msg.getPowerUsedID()); return; } if (powerUsed.maxMobTargets > 1 || powerUsed.maxPlayerTargets > 1) AoeHandler(caster, target, powerUsed, msg.getNumTrains()); else executeActionsForPower(caster, powerUsed, msg.getNumTrains(), target); } private static void AoeHandler(PlayerCharacter caster, AbstractWorldObject target, Power powerUsed, int rank) { HashSet mobTargets = new HashSet<>(); HashSet pcTargets = new HashSet<>(); int count = 1; if (powerUsed.maxMobTargets > 0) mobTargets = WorldGrid.getObjectsInRangePartial(target.loc, powerUsed.areaRange, MBServerStatics.MASK_MOB); if (powerUsed.maxPlayerTargets > 0) pcTargets = WorldGrid.getObjectsInRangePartial(target.loc, powerUsed.areaRange, MBServerStatics.MASK_PLAYER); for (AbstractWorldObject mob : mobTargets) { if (count < powerUsed.maxMobTargets + 1) { executeActionsForPower(caster, powerUsed, rank, mob); count++; } else { break; } } count = 1; for (AbstractWorldObject pc : pcTargets) { if (count < powerUsed.maxPlayerTargets + 1) { executeActionsForPower(caster, powerUsed, rank, pc); count++; } else { break; } } } public static void executeActionsForPower(AbstractCharacter caster, Power power, int rank, AbstractWorldObject target) { // Iterate through the poweractions for this power // and execute them according to PowerActionType. for (ActionEntry actionEntry : power.actionEntries) { PowerAction powerAction = _powerActionLookup.get(Hasher.SBStringHash(actionEntry.action_id)); if (powerAction == null) { Logger.error("Null PowerAction for " + actionEntry.action_id); continue; } powerAction.action_type.execute(caster, power, rank, target, powerAction); } } public static void sendPowerMsg(PlayerCharacter playerCharacter, int type, PerformActionMsg msg) { if (playerCharacter == null) return; msg.setUnknown05(type); switch (type) { case 3: case 4: msg.setUnknown04(2); DispatchManager.dispatchMsgToInterestArea(playerCharacter, msg, mbEnums.DispatchChannel.PRIMARY, MBServerStatics.CHARACTER_LOAD_RANGE, true, false); break; default: msg.setUnknown04(1); Dispatch dispatch = Dispatch.borrow(playerCharacter, msg); DispatchManager.dispatchMsgDispatch(dispatch, mbEnums.DispatchChannel.PRIMARY); } } public static int getRecycleTime(Power power, int trains) { // returns cast time in ms if (power.curves.get("RECYCLETIME") != null) return (int) (((power.recycle_time + (power.curves.get("RECYCLETIME").getValue() * trains)) * 1000) + getCastTime(power, trains)); else return (int) (((power.recycle_time * (1 + (power.curves.get("RECYCLETIME").getValue() * trains))) * 1000) + getCastTime(power, trains)); } public static int getCastTime(Power power, int trains) { // returns cast time in ms if (power.curves.get("INITTIME") != null) return (int) ((power.init_time + (power.curves.get("INITTIME").getValue() * trains)) * 1000); else return (int) ((power.init_time * (1 + (power.curves.get("INITTIME").getValue() * trains))) * 1000); } public static float getCost(Power power, int trains) { if (power.curves.get("COSTAMT") != null) return power.cost + (power.curves.get("COSTAMT").getValue() * trains); else return power.cost * (1 + (power.curves.get("COSTAMT").getValue() * trains)); } public static boolean allowedInCombat(Power power) { switch (power.castingMode.name()) { case "NONE": case "BOTH": case "COMBAT": return true; } return false; } public static boolean allowedOutOfCombat(Power power) { switch (power.castingMode.name()) { case "NONE": case "BOTH": case "NONCOMBAT": return true; } return false; } public static Pair getModifierValues(ModifierEntry modifierEntry, int rank) { Pair outData = new Pair<>(0f, 0f); if (modifierEntry.percentage != 0f) { outData.first = modifierEntry.percentage + (modifierEntry.compoundCurveType.getValue() * rank); outData.first = outData.first * 0.01f; return outData; } // As there is a min/max we return both as a pair enabling // use in the DD modifier. // MB Dev Note: // A subset of health/mana/stam modifiers are multiplicative. // These all have "SIVL" in the curve name suggesting // that SB interpolates between min max. outData.first = modifierEntry.compoundCurveType.name().contains("SIVL") ? modifierEntry.min * (1 + (modifierEntry.compoundCurveType.getValue() * rank)) : modifierEntry.min + (modifierEntry.compoundCurveType.getValue() * rank); if (modifierEntry.max != 0) outData.second = modifierEntry.compoundCurveType.name().contains("SIVL") ? modifierEntry.max * (1 + (modifierEntry.compoundCurveType.getValue() * rank)) : modifierEntry.max + (modifierEntry.compoundCurveType.getValue() * rank); return outData; } }