You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
450 lines
18 KiB
450 lines
18 KiB
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . |
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· |
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ |
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ |
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ |
|
// 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.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<String, Effect> effect_data = new HashMap<>(); |
|
public static HashMap<Integer, PowerAction> power_actions = new HashMap<>(); |
|
public static HashMap<Integer, Power> powers = 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 = powers.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 = powers.get(msg.getPowerUsedID()); |
|
if (powerUsed == null) |
|
return; |
|
if (powerUsed.maxMobTargets > 1 || powerUsed.maxPlayerTargets > 1) { |
|
AoeHandler(caster, target, powerUsed, msg.getNumTrains()); |
|
} else { |
|
applyAllPowerEffects(caster, powerUsed, msg.getNumTrains(), target); |
|
} |
|
} |
|
|
|
private static void AoeHandler(PlayerCharacter caster, AbstractWorldObject target, Power powerUsed, int rank) { |
|
|
|
HashSet<AbstractWorldObject> mobTargets = new HashSet<>(); |
|
HashSet<AbstractWorldObject> 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) { |
|
applyAllPowerEffects(caster, powerUsed, rank, mob); |
|
count++; |
|
} else { |
|
break; |
|
} |
|
} |
|
|
|
count = 1; |
|
for (AbstractWorldObject pc : pcTargets) { |
|
if (count < powerUsed.maxPlayerTargets + 1) { |
|
applyAllPowerEffects(caster, powerUsed, rank, pc); |
|
count++; |
|
} else { |
|
break; |
|
} |
|
} |
|
} |
|
|
|
public static void applyAllPowerEffects(AbstractCharacter caster, Power power, int rank, AbstractWorldObject target) { |
|
|
|
for (ActionEntry actionEntry : power.actionEntries) { |
|
|
|
Effect effect = effect_data.get(actionEntry.effect_id); |
|
|
|
if (effect == null) { |
|
Logger.error("Null effect for " + actionEntry.effect_id); |
|
continue; |
|
} |
|
|
|
// Create pojo to hold effect/modifiers stored in AWO |
|
|
|
AppliedEffect appliedEffect = new AppliedEffect(); |
|
appliedEffect.effect = effect; |
|
appliedEffect.rank = rank; |
|
|
|
// Add calculated modifiers to pojo |
|
|
|
for (ModifierEntry modifierEntry : effect.mods) { |
|
Object modifier = modifierEntry.type.behaviorType.apply(caster, target, power, |
|
actionEntry, effect, modifierEntry, rank); |
|
appliedEffect.modifiers.put(modifierEntry.type, modifier); |
|
} |
|
|
|
// Add this power effect to the target |
|
// or overwrite the old value |
|
|
|
target._effects.put(effect, appliedEffect); |
|
// target.updateBonuses()? |
|
} |
|
|
|
} |
|
|
|
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 float applyCurveToModifier(ActionEntry powerAction, ModifierEntry modifierEntry, int rank) { |
|
|
|
boolean additiveMode = true; |
|
float modifierCurveValue = modifierEntry.compoundCurveType.getValue(); |
|
float modValue; |
|
|
|
if (modifierCurveValue > 0 && modifierCurveValue < .1 && |
|
modifierEntry.min != 0 && modifierEntry.max != 0) |
|
additiveMode = false; |
|
|
|
if (additiveMode) |
|
modValue = powerAction.curve.getValue() + (modifierCurveValue * rank); |
|
else |
|
modValue = powerAction.curve.getValue() * (1 + (modifierCurveValue * rank)); |
|
|
|
modValue = modValue * 0.01f; |
|
|
|
return modValue; |
|
} |
|
}
|
|
|