diff --git a/pom.xml b/pom.xml index 82536389b4..1a333333a6 100644 --- a/pom.xml +++ b/pom.xml @@ -28,11 +28,12 @@ 2.18.0 22.2.0 4.1.79.Final - 5.9.0 1.15 1.0 5.0.1 8.0.30 + 5.9.0 + 4.7.0 @@ -120,16 +121,25 @@ org.junit.jupiter junit-jupiter-api ${junit.version} + test org.junit.jupiter junit-jupiter-engine ${junit.version} + test org.junit.jupiter junit-jupiter-params ${junit.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test diff --git a/src/main/java/client/Character.java b/src/main/java/client/Character.java index b9972d4248..5404d41530 100644 --- a/src/main/java/client/Character.java +++ b/src/main/java/client/Character.java @@ -64,10 +64,7 @@ import server.events.Events; import server.events.RescueGaga; import server.events.gm.Fitness; import server.events.gm.Ola; -import server.life.MobSkill; -import server.life.MobSkillFactory; -import server.life.Monster; -import server.life.PlayerNPC; +import server.life.*; import server.maps.*; import server.maps.MiniGame.MiniGameResult; import server.minigame.RockPaperScissor; @@ -7329,10 +7326,8 @@ public class Character extends AbstractCharacterObject { final int skilllv = rs.getInt("mobskilllv"); final long length = rs.getInt("length"); - MobSkill ms = MobSkillFactory.getMobSkill(skillid, skilllv); - if (ms != null) { - loadedDiseases.put(disease, new Pair<>(length, ms)); - } + MobSkill ms = MobSkillFactory.getMobSkill(MobSkillType.from(skillid), skilllv); + loadedDiseases.put(disease, new Pair<>(length, ms)); } } } @@ -8111,8 +8106,9 @@ public class Character extends AbstractCharacterObject { ps.setInt(2, e.getKey().ordinal()); MobSkill ms = e.getValue().getRight(); - ps.setInt(3, ms.getSkillId()); - ps.setInt(4, ms.getSkillLevel()); + MobSkillId msId = ms.getId(); + ps.setInt(3, msId.type().getId()); + ps.setInt(4, msId.level()); ps.setInt(5, e.getValue().getLeft().intValue()); ps.addBatch(); } diff --git a/src/main/java/client/Disease.java b/src/main/java/client/Disease.java index 6e8d4ec7be..57aa471e57 100644 --- a/src/main/java/client/Disease.java +++ b/src/main/java/client/Disease.java @@ -22,37 +22,34 @@ package client; import constants.game.GameConstants; +import server.life.MobSkillType; + +import java.util.Arrays; public enum Disease { NULL(0x0), - SLOW(0x1, 126), - SEDUCE(0x80, 128), + SLOW(0x1, MobSkillType.SLOW), + SEDUCE(0x80, MobSkillType.SEDUCE), FISHABLE(0x100), ZOMBIFY(0x4000), - CONFUSE(0x80000, 132), - STUN(0x2000000000000L, 123), - POISON(0x4000000000000L, 125), - SEAL(0x8000000000000L, 120), - DARKNESS(0x10000000000000L, 121), - WEAKEN(0x4000000000000000L, 122), - CURSE(0x8000000000000000L, 124); + CONFUSE(0x80000, MobSkillType.REVERSE_INPUT), + STUN(0x2000000000000L, MobSkillType.STUN), + POISON(0x4000000000000L, MobSkillType.POISON), + SEAL(0x8000000000000L, MobSkillType.SEAL), + DARKNESS(0x10000000000000L, MobSkillType.DARKNESS), + WEAKEN(0x4000000000000000L, MobSkillType.WEAKNESS), + CURSE(0x8000000000000000L, MobSkillType.CURSE); private final long i; - private final boolean first; - private final int mobskill; + private final MobSkillType mobSkillType; Disease(long i) { - this(i, false, 0); + this(i, null); } - Disease(long i, int skill) { - this(i, false, skill); - } - - Disease(long i, boolean first, int skill) { + Disease(long i, MobSkillType skill) { this.i = i; - this.first = first; - this.mobskill = skill; + this.mobSkillType = skill; } public long getValue() { @@ -60,11 +57,11 @@ public enum Disease { } public boolean isFirst() { - return first; + return false; } - public int getDisease() { - return mobskill; + public MobSkillType getMobSkillType() { + return mobSkillType; } public static Disease ordinal(int ord) { @@ -80,13 +77,14 @@ public enum Disease { return diseases[(int) (Math.random() * diseases.length)]; } - public static final Disease getBySkill(final int skill) { - for (Disease d : Disease.values()) { - if (d.getDisease() == skill && d.getDisease() != 0) { - return d; - } + public static final Disease getBySkill(MobSkillType skill) { + if (skill == null) { + return null; } - return null; + return Arrays.stream(Disease.values()) + .filter(d -> d.mobSkillType == skill) + .findAny() + .orElse(null); } } \ No newline at end of file diff --git a/src/main/java/client/command/commands/gm2/MobSkillCommand.java b/src/main/java/client/command/commands/gm2/MobSkillCommand.java index bba008aef7..63b3cb8994 100644 --- a/src/main/java/client/command/commands/gm2/MobSkillCommand.java +++ b/src/main/java/client/command/commands/gm2/MobSkillCommand.java @@ -5,6 +5,7 @@ import client.Client; import client.command.Command; import server.life.MobSkill; import server.life.MobSkillFactory; +import server.life.MobSkillType; import java.util.Collections; @@ -21,7 +22,8 @@ public class MobSkillCommand extends Command { String skillId = params[0]; String skillLevel = params[1]; - MobSkill mobSkill = MobSkillFactory.getMobSkill(Integer.parseInt(skillId), Integer.parseInt(skillLevel)); + MobSkillType type = MobSkillType.from(Integer.parseInt(skillId)); + MobSkill mobSkill = MobSkillFactory.getMobSkill(type, Integer.parseInt(skillLevel)); if (mobSkill == null) { throw new IllegalArgumentException("Mob skill not found. Id: %s, level: %s".formatted(skillId, skillLevel)); } diff --git a/src/main/java/client/command/commands/gm3/DebuffCommand.java b/src/main/java/client/command/commands/gm3/DebuffCommand.java index aed248286d..bcd18ae9c1 100644 --- a/src/main/java/client/command/commands/gm3/DebuffCommand.java +++ b/src/main/java/client/command/commands/gm3/DebuffCommand.java @@ -29,6 +29,7 @@ import client.Disease; import client.command.Command; import server.life.MobSkill; import server.life.MobSkillFactory; +import server.life.MobSkillType; import server.maps.MapObject; import server.maps.MapObjectType; @@ -51,55 +52,46 @@ public class DebuffCommand extends Command { MobSkill skill = null; switch (params[0].toUpperCase()) { - case "SLOW": + case "SLOW" -> { disease = Disease.SLOW; - skill = MobSkillFactory.getMobSkill(126, 7); - break; - - case "SEDUCE": + skill = MobSkillFactory.getMobSkill(MobSkillType.SLOW, 7); + } + case "SEDUCE" -> { disease = Disease.SEDUCE; - skill = MobSkillFactory.getMobSkill(128, 7); - break; - - case "ZOMBIFY": + skill = MobSkillFactory.getMobSkill(MobSkillType.SEDUCE, 7); + } + case "ZOMBIFY" -> { disease = Disease.ZOMBIFY; - skill = MobSkillFactory.getMobSkill(133, 1); - break; - - case "CONFUSE": + skill = MobSkillFactory.getMobSkill(MobSkillType.UNDEAD, 1); + } + case "CONFUSE" -> { disease = Disease.CONFUSE; - skill = MobSkillFactory.getMobSkill(132, 2); - break; - - case "STUN": + skill = MobSkillFactory.getMobSkill(MobSkillType.REVERSE_INPUT, 2); + } + case "STUN" -> { disease = Disease.STUN; - skill = MobSkillFactory.getMobSkill(123, 7); - break; - - case "POISON": + skill = MobSkillFactory.getMobSkill(MobSkillType.STUN, 7); + } + case "POISON" -> { disease = Disease.POISON; - skill = MobSkillFactory.getMobSkill(125, 5); - break; - - case "SEAL": + skill = MobSkillFactory.getMobSkill(MobSkillType.POISON, 5); + } + case "SEAL" -> { disease = Disease.SEAL; - skill = MobSkillFactory.getMobSkill(120, 1); - break; - - case "DARKNESS": + skill = MobSkillFactory.getMobSkill(MobSkillType.SEAL, 1); + } + case "DARKNESS" -> { disease = Disease.DARKNESS; - skill = MobSkillFactory.getMobSkill(121, 1); - break; - - case "WEAKEN": + skill = MobSkillFactory.getMobSkill(MobSkillType.DARKNESS, 1); + } + case "WEAKEN" -> { disease = Disease.WEAKEN; - skill = MobSkillFactory.getMobSkill(122, 1); - break; - - case "CURSE": + skill = MobSkillFactory.getMobSkill(MobSkillType.WEAKNESS, 1); + } + case "CURSE" -> { disease = Disease.CURSE; - skill = MobSkillFactory.getMobSkill(124, 1); - break; + skill = MobSkillFactory.getMobSkill(MobSkillType.CURSE, 1); + } } if (disease == null) { diff --git a/src/main/java/net/server/channel/handlers/AbstractDealDamageHandler.java b/src/main/java/net/server/channel/handlers/AbstractDealDamageHandler.java index 9343a4f8de..c2f8f09580 100644 --- a/src/main/java/net/server/channel/handlers/AbstractDealDamageHandler.java +++ b/src/main/java/net/server/channel/handlers/AbstractDealDamageHandler.java @@ -44,7 +44,6 @@ import server.maps.MapObject; import server.maps.MapObjectType; import server.maps.MapleMap; import tools.PacketCreator; -import tools.Pair; import tools.Randomizer; import java.awt.*; @@ -491,22 +490,18 @@ public abstract class AbstractDealDamageHandler extends AbstractPacketHandler { map.damageMonster(player, monster, totDamageToOneMonster); } if (monster.isBuffed(MonsterStatus.WEAPON_REFLECT) && !attack.magic) { - List> mobSkills = monster.getSkills(); - - for (Pair ms : mobSkills) { - if (ms.left == 145) { - MobSkill toUse = MobSkillFactory.getMobSkill(ms.left, ms.right); + for (MobSkillId msId : monster.getSkills()) { + if (msId.type() == MobSkillType.PHYSICAL_AND_MAGIC_COUNTER) { + MobSkill toUse = MobSkillFactory.getMobSkill(MobSkillType.PHYSICAL_AND_MAGIC_COUNTER, msId.level()); player.addHP(-toUse.getX()); map.broadcastMessage(player, PacketCreator.damagePlayer(0, monster.getId(), player.getId(), toUse.getX(), 0, 0, false, 0, true, monster.getObjectId(), 0, 0), true); } } } if (monster.isBuffed(MonsterStatus.MAGIC_REFLECT) && attack.magic) { - List> mobSkills = monster.getSkills(); - - for (Pair ms : mobSkills) { - if (ms.left == 145) { - MobSkill toUse = MobSkillFactory.getMobSkill(ms.left, ms.right); + for (MobSkillId msId : monster.getSkills()) { + if (msId.type() == MobSkillType.PHYSICAL_AND_MAGIC_COUNTER) { + MobSkill toUse = MobSkillFactory.getMobSkill(MobSkillType.PHYSICAL_AND_MAGIC_COUNTER, msId.level()); player.addHP(-toUse.getY()); map.broadcastMessage(player, PacketCreator.damagePlayer(0, monster.getId(), player.getId(), toUse.getY(), 0, 0, false, 0, true, monster.getObjectId(), 0, 0), true); } diff --git a/src/main/java/net/server/channel/handlers/MonsterCarnivalHandler.java b/src/main/java/net/server/channel/handlers/MonsterCarnivalHandler.java index a477e9edd9..b0b6a9f1f5 100644 --- a/src/main/java/net/server/channel/handlers/MonsterCarnivalHandler.java +++ b/src/main/java/net/server/channel/handlers/MonsterCarnivalHandler.java @@ -29,6 +29,7 @@ import net.packet.InPacket; import net.server.world.Party; import net.server.world.PartyCharacter; import server.life.LifeFactory; +import server.life.MobSkillType; import server.life.Monster; import server.partyquest.CarnivalFactory; import server.partyquest.CarnivalFactory.MCSkill; @@ -94,18 +95,15 @@ public final class MonsterCarnivalHandler extends AbstractPacketHandler { return; } final MCSkill skill = CarnivalFactory.getInstance().getSkill(skillid.get(num)); //ugh wtf - if (skill == null || c.getPlayer().getCP() < skill.cpLoss) { + if (skill == null || c.getPlayer().getCP() < skill.cpLoss()) { c.sendPacket(PacketCreator.CPQMessage((byte) 1)); c.sendPacket(PacketCreator.enableActions()); return; } final Disease dis = skill.getDisease(); Party enemies = c.getPlayer().getParty().getEnemy(); - if (skill.targetsAll) { - int hitChance = 0; - if (dis.getDisease() == 121 || dis.getDisease() == 122 || dis.getDisease() == 125 || dis.getDisease() == 126) { - hitChance = (int) (Math.random() * 100); - } + if (skill.targetsAll()) { + int hitChance = rollHitChance(dis.getMobSkillType()); if (hitChance <= 80) { for (PartyCharacter mpc : enemies.getPartyMembers()) { Character mc = mpc.getPlayer(); @@ -130,11 +128,11 @@ public final class MonsterCarnivalHandler extends AbstractPacketHandler { } } } - neededCP = skill.cpLoss; + neededCP = skill.cpLoss(); c.sendPacket(PacketCreator.enableActions()); } else if (tab == 2) { //protectors final MCSkill skill = CarnivalFactory.getInstance().getGuardian(num); - if (skill == null || c.getPlayer().getCP() < skill.cpLoss) { + if (skill == null || c.getPlayer().getCP() < skill.cpLoss()) { c.sendPacket(PacketCreator.CPQMessage((byte) 1)); c.sendPacket(PacketCreator.enableActions()); return; @@ -165,7 +163,7 @@ public final class MonsterCarnivalHandler extends AbstractPacketHandler { c.sendPacket(PacketCreator.enableActions()); return; } else { - neededCP = skill.cpLoss; + neededCP = skill.cpLoss(); } } } @@ -179,4 +177,11 @@ public final class MonsterCarnivalHandler extends AbstractPacketHandler { } } } + + private int rollHitChance(MobSkillType type) { + return switch (type) { + case DARKNESS, WEAKNESS, POISON, SLOW -> (int) (Math.random() * 100); + default -> 0; + }; + } } diff --git a/src/main/java/net/server/channel/handlers/MoveLifeHandler.java b/src/main/java/net/server/channel/handlers/MoveLifeHandler.java index 94b5871596..4b7c7d6493 100644 --- a/src/main/java/net/server/channel/handlers/MoveLifeHandler.java +++ b/src/main/java/net/server/channel/handlers/MoveLifeHandler.java @@ -27,16 +27,11 @@ import config.YamlConfig; import net.packet.InPacket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import server.life.MobSkill; -import server.life.MobSkillFactory; -import server.life.Monster; -import server.life.MonsterInformationProvider; +import server.life.*; import server.maps.MapObject; import server.maps.MapObjectType; import server.maps.MapleMap; import tools.PacketCreator; -import tools.Pair; -import tools.Randomizer; import tools.exceptions.EmptyMovementException; import java.awt.*; @@ -84,7 +79,6 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler { boolean isAttack = inRangeInclusive(rawActivity, 24, 41); boolean isSkill = inRangeInclusive(rawActivity, 42, 59); - MobSkill toUse = null; int useSkillId = 0, useSkillLevel = 0; MobSkill nextUse = null; @@ -92,18 +86,16 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler { boolean nextMovementCouldBeSkill = !(isSkill || (pNibbles != 0)); - int castPos; if (isSkill) { useSkillId = skillId; useSkillLevel = skillLv; - castPos = monster.getSkillPos(useSkillId, useSkillLevel); - if (castPos != -1) { - toUse = MobSkillFactory.getMobSkill(useSkillId, useSkillLevel); + if (monster.hasSkill(useSkillId, useSkillLevel)) { + MobSkill toUse = MobSkillFactory.getMobSkill(MobSkillType.from(useSkillId), useSkillLevel); if (monster.canUseSkill(toUse, true)) { int animationTime = MonsterInformationProvider.getInstance().getMobSkillAnimationTime(toUse); - if (animationTime > 0 && toUse.getSkillId() != 129) { + if (animationTime > 0 && toUse.getType() != MobSkillType.BANISH) { toUse.applyDelayedEffect(player, monster, true, animationTime); } else { banishPlayers = new LinkedList<>(); @@ -112,8 +104,7 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler { } } } else { - castPos = (rawActivity - 24) / 2; - + int castPos = (rawActivity - 24) / 2; int atkStatus = monster.canUseAttack(castPos, isSkill); if (atkStatus < 1) { rawActivity = -1; @@ -122,23 +113,18 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler { } int mobMp = monster.getMp(); - if (nextMovementCouldBeSkill) { - int noSkills = monster.getNoSkills(); - if (noSkills > 0) { - int rndSkill = Randomizer.nextInt(noSkills); + if (nextMovementCouldBeSkill && monster.hasAnySkill()) { + MobSkillId skillToUse = monster.getRandomSkill(); + nextSkillId = skillToUse.type().getId(); + nextSkillLevel = skillToUse.level(); + nextUse = MobSkillFactory.getMobSkill(skillToUse.type(), skillToUse.level()); - Pair skillToUse = monster.getSkills().get(rndSkill); - nextSkillId = skillToUse.getLeft(); - nextSkillLevel = skillToUse.getRight(); - nextUse = MobSkillFactory.getMobSkill(nextSkillId, nextSkillLevel); + if (!(nextUse != null && monster.canUseSkill(nextUse, false) && nextUse.getHP() >= (int) (((float) monster.getHp() / monster.getMaxHp()) * 100) && mobMp >= nextUse.getMpCon())) { + // thanks OishiiKawaiiDesu for noticing mobs trying to cast skills they are not supposed to be able - if (!(nextUse != null && monster.canUseSkill(nextUse, false) && nextUse.getHP() >= (int) (((float) monster.getHp() / monster.getMaxHp()) * 100) && mobMp >= nextUse.getMpCon())) { - // thanks OishiiKawaiiDesu for noticing mobs trying to cast skills they are not supposed to be able - - nextSkillId = 0; - nextSkillLevel = 0; - nextUse = null; - } + nextSkillId = 0; + nextSkillLevel = 0; + nextUse = null; } } @@ -168,8 +154,8 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler { p.seek(movementDataStart); if (YamlConfig.config.server.USE_DEBUG_SHOW_RCVD_MVLIFE) { - log.debug("{} castPos: {}, rawAct: {}, opt: {}, skillId: {}, skillLv: {}, allowSkill: {}, mobMp: {}", - isSkill ? "SKILL" : (isAttack ? "ATTCK" : ""), castPos, rawActivity, pOption, useSkillId, + log.debug("{} rawAct: {}, opt: {}, skillId: {}, skillLv: {}, allowSkill: {}, mobMp: {}", + isSkill ? "SKILL" : (isAttack ? "ATTCK" : ""), rawActivity, pOption, useSkillId, useSkillLevel, nextMovementCouldBeSkill, mobMp); } diff --git a/src/main/java/net/server/channel/handlers/TakeDamageHandler.java b/src/main/java/net/server/channel/handlers/TakeDamageHandler.java index 28b6373c2a..4a3f331cf5 100644 --- a/src/main/java/net/server/channel/handlers/TakeDamageHandler.java +++ b/src/main/java/net/server/channel/handlers/TakeDamageHandler.java @@ -151,7 +151,7 @@ public final class TakeDamageHandler extends AbstractPacketHandler { is_deadly = true; } mpattack += attackInfo.getMpBurn(); - MobSkill mobSkill = MobSkillFactory.getMobSkill(attackInfo.getDiseaseSkill(), attackInfo.getDiseaseLevel()); + MobSkill mobSkill = MobSkillFactory.getMobSkill(MobSkillType.from(attackInfo.getDiseaseSkill()), attackInfo.getDiseaseLevel()); if (mobSkill != null && damage > 0) { mobSkill.applyEffect(chr, attacker, false, banishPlayers); } diff --git a/src/main/java/scripting/AbstractPlayerInteraction.java b/src/main/java/scripting/AbstractPlayerInteraction.java index 12a0f34f40..91f9ee40f7 100644 --- a/src/main/java/scripting/AbstractPlayerInteraction.java +++ b/src/main/java/scripting/AbstractPlayerInteraction.java @@ -965,10 +965,6 @@ public class AbstractPlayerInteraction { return LifeFactory.getMonster(mid); } - public MobSkill getMobSkill(int skill, int level) { - return MobSkillFactory.getMobSkill(skill, level); - } - public void spawnGuide() { c.sendPacket(PacketCreator.spawnGuide(true)); } @@ -1199,12 +1195,12 @@ public class AbstractPlayerInteraction { } private void applySealSkill(Monster monster) { - MobSkill sealSkill = MobSkillFactory.getMobSkill(157, 1); + MobSkill sealSkill = MobSkillFactory.getMobSkill(MobSkillType.SEAL_SKILL, 1); sealSkill.applyEffect(monster); } private void applyReduceAvoid(Monster monster) { - MobSkill reduceAvoidSkill = MobSkillFactory.getMobSkill(155, 2); + MobSkill reduceAvoidSkill = MobSkillFactory.getMobSkill(MobSkillType.EVA, 2); reduceAvoidSkill.applyEffect(monster); } diff --git a/src/main/java/server/StatEffect.java b/src/main/java/server/StatEffect.java index 6c06f47b8d..5f308e6219 100644 --- a/src/main/java/server/StatEffect.java +++ b/src/main/java/server/StatEffect.java @@ -42,11 +42,11 @@ import provider.Data; import provider.DataTool; import server.life.MobSkill; import server.life.MobSkillFactory; +import server.life.MobSkillType; import server.life.Monster; import server.maps.*; import server.partyquest.CarnivalFactory; import server.partyquest.CarnivalFactory.MCSkill; -import tools.ArrayMap; import tools.PacketCreator; import tools.Pair; @@ -422,7 +422,7 @@ public class StatEffect { ret.itemCon = DataTool.getInt("itemCon", source, 0); ret.itemConNo = DataTool.getInt("itemConNo", source, 0); ret.moveTo = DataTool.getInt("moveTo", source, -1); - Map monsterStatus = new ArrayMap<>(); + Map monsterStatus = new EnumMap<>(MonsterStatus.class); if (skill) { switch (sourceid) { // BEGINNER @@ -1039,14 +1039,15 @@ public class StatEffect { if (skill != null) { final Disease dis = skill.getDisease(); Party opposition = applyfrom.getParty().getEnemy(); - if (skill.targetsAll) { + if (skill.targetsAll()) { for (PartyCharacter enemyChrs : opposition.getPartyMembers()) { Character chrApp = enemyChrs.getPlayer(); if (chrApp != null && chrApp.getMap().isCPQMap()) { if (dis == null) { chrApp.dispel(); } else { - chrApp.giveDebuff(dis, MCSkill.getMobSkill(dis.getDisease(), skill.level)); + MobSkill mobSkill = MobSkillFactory.getMobSkill(dis.getMobSkillType(), skill.level()); + chrApp.giveDebuff(dis, mobSkill); } } } @@ -1058,7 +1059,8 @@ public class StatEffect { if (dis == null) { chrApp.dispel(); } else { - chrApp.giveDebuff(dis, MCSkill.getMobSkill(dis.getDisease(), skill.level)); + MobSkill mobSkill = MobSkillFactory.getMobSkill(dis.getMobSkillType(), skill.level()); + chrApp.giveDebuff(dis, mobSkill); } } } @@ -1068,8 +1070,9 @@ public class StatEffect { applyfrom.dispelDebuff(debuff); } } else if (mobSkill > 0 && mobSkillLevel > 0) { - MobSkill ms = MobSkillFactory.getMobSkill(mobSkill, mobSkillLevel); - Disease dis = Disease.getBySkill(mobSkill); + var mobSkillType = MobSkillType.from(mobSkill); + MobSkill ms = MobSkillFactory.getMobSkill(mobSkillType, mobSkillLevel); + Disease dis = Disease.getBySkill(mobSkillType); if (target > 0) { for (Character chr : applyto.getMap().getAllPlayers()) { diff --git a/src/main/java/server/life/LifeFactory.java b/src/main/java/server/life/LifeFactory.java index 80eb10462c..000ec75174 100644 --- a/src/main/java/server/life/LifeFactory.java +++ b/src/main/java/server/life/LifeFactory.java @@ -188,11 +188,12 @@ public class LifeFactory { Data monsterSkillInfoData = monsterInfoData.getChildByPath("skill"); if (monsterSkillInfoData != null) { int i = 0; - List> skills = new ArrayList<>(); + Set skills = new HashSet<>(); while (monsterSkillInfoData.getChildByPath(Integer.toString(i)) != null) { int skillId = DataTool.getInt(i + "/skill", monsterSkillInfoData, 0); int skillLv = DataTool.getInt(i + "/level", monsterSkillInfoData, 0); - skills.add(new Pair<>(skillId, skillLv)); + MobSkillType type = MobSkillType.from(skillId); + skills.add(new MobSkillId(type, skillLv)); Data monsterSkillData = monsterData.getChildByPath("skill" + (i + 1)); if (monsterSkillData != null) { @@ -201,7 +202,7 @@ public class LifeFactory { animationTime += DataTool.getIntConvert("delay", effectEntry, 0); } - MobSkill skill = MobSkillFactory.getMobSkill(skillId, skillLv); + MobSkill skill = MobSkillFactory.getMobSkill(type, skillLv); mi.setMobSkillAnimationTime(skill, animationTime); } diff --git a/src/main/java/server/life/MobSkill.java b/src/main/java/server/life/MobSkill.java index 5cb85cc8c5..560a2262e2 100644 --- a/src/main/java/server/life/MobSkill.java +++ b/src/main/java/server/life/MobSkill.java @@ -26,6 +26,7 @@ import client.Disease; import client.status.MonsterStatus; import constants.id.MapId; import constants.id.MobId; +import constants.skills.Bishop; import net.server.services.task.channel.OverallService; import net.server.services.type.ChannelServices; import org.slf4j.Logger; @@ -34,7 +35,6 @@ import server.maps.MapObject; import server.maps.MapObjectType; import server.maps.MapleMap; import server.maps.Mist; -import tools.ArrayMap; import tools.Randomizer; import java.awt.*; @@ -47,68 +47,130 @@ import java.util.*; public class MobSkill { private static final Logger log = LoggerFactory.getLogger(MobSkill.class); - private final int skillId; - private final int skillLevel; - private int mpCon; - private final List toSummon = new ArrayList<>(); - private int spawnEffect, hp, x, y, count; - private long duration, cooltime; - private float prop; - private Point lt, rb; - private int limit; + private final MobSkillId id; + private final int mpCon; + private final int spawnEffect; + private final int hp; + private final int x; + private final int y; + private final int count; + private final long duration; + private final long cooltime; + private final float prop; + private final Point lt; + private final Point rb; + private final int limit; + private final List toSummon; - public MobSkill(int skillId, int level) { - this.skillId = skillId; - this.skillLevel = level; - } - - public void setMpCon(int mpCon) { + private MobSkill(MobSkillType type, int level, int mpCon, int spawnEffect, int hp, int x, int y, int count, + long duration, long cooltime, float prop, Point lt, Point rb, int limit, List toSummon) { + this.id = new MobSkillId(type, level); this.mpCon = mpCon; - } - - public void addSummons(List toSummon) { - this.toSummon.addAll(toSummon); - } - - public void setSpawnEffect(int spawnEffect) { this.spawnEffect = spawnEffect; - } - - public void setHp(int hp) { this.hp = hp; - } - - public void setX(int x) { this.x = x; - } - - public void setY(int y) { this.y = y; - } - - public void setCount(int count) { this.count = count; - } - - public void setDuration(long duration) { this.duration = duration; - } - - public void setCoolTime(long cooltime) { this.cooltime = cooltime; - } - - public void setProp(float prop) { this.prop = prop; - } - - public void setLtRb(Point lt, Point rb) { this.lt = lt; this.rb = rb; + this.limit = limit; + this.toSummon = toSummon; } - public void setLimit(int limit) { - this.limit = limit; + static class Builder { + private final MobSkillType type; + private final int level; + private int mpCon; + private int spawnEffect; + private int hp; + private int x; + private int y; + private int count; + private long duration; + private long cooltime; + private float prop; + private Point lt; + private Point rb; + private int limit; + private List toSummon; + + public Builder(MobSkillType type, int level) { + this.type = type; + this.level = level; + } + + public Builder mpCon(int mpCon) { + this.mpCon = mpCon; + return this; + } + + public Builder spawnEffect(int spawnEffect) { + this.spawnEffect = spawnEffect; + return this; + } + + public Builder hp(int hp) { + this.hp = hp; + return this; + } + + public Builder x(int x) { + this.x = x; + return this; + } + + public Builder y(int y) { + this.y = y; + return this; + } + + public Builder count(int count) { + this.count = count; + return this; + } + + public Builder duration(long duration) { + this.duration = duration; + return this; + } + + public Builder cooltime(long cooltime) { + this.cooltime = cooltime; + return this; + } + + public Builder prop(float prop) { + this.prop = prop; + return this; + } + + public Builder lt(Point lt) { + this.lt = lt; + return this; + } + + public Builder rb(Point rb) { + this.rb = rb; + return this; + } + + public Builder limit(int limit) { + this.limit = limit; + return this; + } + + public Builder toSummon(List toSummon) { + this.toSummon = Collections.unmodifiableList(toSummon); + return this; + } + + public MobSkill build() { + return new MobSkill(type, level, mpCon, spawnEffect, hp, x, y, count, duration, cooltime, prop, lt, rb, + limit, toSummon); + } } public void applyDelayedEffect(final Character player, final Monster monster, final boolean skill, int animationTime) { @@ -126,247 +188,221 @@ public class MobSkill { applyEffect(null, monster, false, Collections.emptyList()); } - public void applyEffect(Character player, Monster monster, boolean skill, List banishPlayers) { + // TODO: avoid output argument banishPlayersOutput + public void applyEffect(Character player, Monster monster, boolean skill, List banishPlayersOutput) { Disease disease = null; - Map stats = new ArrayMap<>(); - List reflection = new LinkedList<>(); - switch (skillId) { - case 100: - case 110: - case 150: - stats.put(MonsterStatus.WEAPON_ATTACK_UP, x); - break; - case 101: - case 111: - case 151: - stats.put(MonsterStatus.MAGIC_ATTACK_UP, x); - break; - case 102: - case 112: - case 152: - stats.put(MonsterStatus.WEAPON_DEFENSE_UP, x); - break; - case 103: - case 113: - case 153: - stats.put(MonsterStatus.MAGIC_DEFENSE_UP, x); - break; - case 114: - if (lt != null && rb != null && skill) { - List objects = getObjectsInRange(monster, MapObjectType.MONSTER); - final int hps = (getX() / 1000) * (int) (950 + 1050 * Math.random()); - for (MapObject mons : objects) { - ((Monster) mons).heal(hps, getY()); - } - } else { - monster.heal(getX(), getY()); - } - break; - case 120: - disease = Disease.SEAL; - break; - case 121: - disease = Disease.DARKNESS; - break; - case 122: - disease = Disease.WEAKEN; - break; - case 123: - disease = Disease.STUN; - break; - case 124: - disease = Disease.CURSE; - break; - case 125: - disease = Disease.POISON; - break; - case 126: // Slow - disease = Disease.SLOW; - break; - case 127: - if (lt != null && rb != null && skill) { - for (Character character : getPlayersInRange(monster)) { - character.dispel(); - } - } else { - player.dispel(); - } - break; - case 128: // Seduce - disease = Disease.SEDUCE; - break; - case 129: // Banish - if (lt != null && rb != null && skill) { - banishPlayers.addAll(getPlayersInRange(monster)); - } else { - banishPlayers.add(player); - } - break; - case 131: // Mist - monster.getMap().spawnMist(new Mist(calculateBoundingBox(monster.getPosition()), monster, this), x * 100, false, false, false); - break; - case 132: - disease = Disease.CONFUSE; - break; - case 133: // zombify - disease = Disease.ZOMBIFY; - break; - case 140: + Map stats = new EnumMap<>(MonsterStatus.class); + List reflection = new ArrayList<>(); + switch (id.type()) { + case ATTACK_UP, ATTACK_UP_M, PAD -> stats.put(MonsterStatus.WEAPON_ATTACK_UP, x); + case MAGIC_ATTACK_UP, MAGIC_ATTACK_UP_M, MAD -> stats.put(MonsterStatus.MAGIC_ATTACK_UP, x); + case DEFENSE_UP, DEFENSE_UP_M, PDR -> stats.put(MonsterStatus.WEAPON_DEFENSE_UP, x); + case MAGIC_DEFENSE_UP, MAGIC_DEFENSE_UP_M, MDR -> stats.put(MonsterStatus.MAGIC_DEFENSE_UP, x); + case HEAL_M -> applyHealEffect(skill, monster); + case SEAL -> disease = Disease.SEAL; + case DARKNESS -> disease = Disease.DARKNESS; + case WEAKNESS -> disease = Disease.WEAKEN; + case STUN -> disease = Disease.STUN; + case CURSE -> disease = Disease.CURSE; + case POISON -> disease = Disease.POISON; + case SLOW -> disease = Disease.SLOW; + case DISPEL -> applyDispelEffect(skill, monster, player); + case SEDUCE -> disease = Disease.SEDUCE; + case BANISH -> applyBanishEffect(skill, monster, player, banishPlayersOutput); + case AREA_POISON -> spawnMonsterMist(monster); + case REVERSE_INPUT -> disease = Disease.CONFUSE; + case UNDEAD -> disease = Disease.ZOMBIFY; + case PHYSICAL_IMMUNE -> { if (makeChanceResult() && !monster.isBuffed(MonsterStatus.MAGIC_IMMUNITY)) { stats.put(MonsterStatus.WEAPON_IMMUNITY, x); } - break; - case 141: + } + case MAGIC_IMMUNE -> { if (makeChanceResult() && !monster.isBuffed(MonsterStatus.WEAPON_IMMUNITY)) { stats.put(MonsterStatus.MAGIC_IMMUNITY, x); } - break; - case 143: // Weapon Reflect + } + case PHYSICAL_COUNTER -> { stats.put(MonsterStatus.WEAPON_REFLECT, 10); stats.put(MonsterStatus.WEAPON_IMMUNITY, 10); reflection.add(x); - break; - case 144: // Magic Reflect + } + case MAGIC_COUNTER -> { stats.put(MonsterStatus.MAGIC_REFLECT, 10); stats.put(MonsterStatus.MAGIC_IMMUNITY, 10); reflection.add(x); - break; - case 145: // Weapon / Magic reflect + } + case PHYSICAL_AND_MAGIC_COUNTER -> { stats.put(MonsterStatus.WEAPON_REFLECT, 10); stats.put(MonsterStatus.WEAPON_IMMUNITY, 10); stats.put(MonsterStatus.MAGIC_REFLECT, 10); stats.put(MonsterStatus.MAGIC_IMMUNITY, 10); reflection.add(x); - break; - case 154: - stats.put(MonsterStatus.ACC, x); - break; - case 155: - stats.put(MonsterStatus.AVOID, x); - break; - case 156: - stats.put(MonsterStatus.SPEED, x); - break; - case 157: - stats.put(MonsterStatus.SEAL_SKILL, x); - break; - case 200: // summon - int skillLimit = this.getLimit(); - MapleMap map = monster.getMap(); - - if (MapId.isDojo(map.getId())) { // spawns in dojo should be unlimited - skillLimit = Integer.MAX_VALUE; - } - - if (map.getSpawnedMonstersOnMap() < 80) { - List summons = getSummons(); - int summonLimit = monster.countAvailableMobSummons(summons.size(), skillLimit); - if (summonLimit >= 1) { - boolean bossRushMap = MapId.isBossRush(map.getId()); - - Collections.shuffle(summons); - for (Integer mobId : summons.subList(0, summonLimit)) { - Monster toSpawn = LifeFactory.getMonster(mobId); - if (toSpawn != null) { - if (bossRushMap) { - toSpawn.disableDrops(); // no littering on BRPQ pls - } - toSpawn.setPosition(monster.getPosition()); - int ypos, xpos; - xpos = (int) monster.getPosition().getX(); - ypos = (int) monster.getPosition().getY(); - switch (mobId) { - case MobId.HIGH_DARKSTAR: // Pap bomb high - toSpawn.setFh((int) Math.ceil(Math.random() * 19.0)); - ypos = -590; - break; - case MobId.LOW_DARKSTAR: // Pap bomb - xpos = (int) (monster.getPosition().getX() + Randomizer.nextInt(1000) - 500); - if (ypos != -590) { - ypos = (int) monster.getPosition().getY(); - } - break; - case MobId.BLOODY_BOOM: //Pianus bomb - if (Math.ceil(Math.random() * 5) == 1) { - ypos = 78; - xpos = Randomizer.nextInt(5) + (Randomizer.nextInt(2) == 1 ? 180 : 0); - } else { - xpos = (int) (monster.getPosition().getX() + Randomizer.nextInt(1000) - 500); - } - break; - } - switch (map.getId()) { - case MapId.ORIGIN_OF_CLOCKTOWER: //Pap map - if (xpos < -890) { - xpos = (int) (Math.ceil(Math.random() * 150) - 890); - } else if (xpos > 230) { - xpos = (int) (230 - Math.ceil(Math.random() * 150)); - } - break; - case MapId.CAVE_OF_PIANUS: // Pianus map - if (xpos < -239) { - xpos = (int) (Math.ceil(Math.random() * 150) - 239); - } else if (xpos > 371) { - xpos = (int) (371 - Math.ceil(Math.random() * 150)); - } - break; - } - toSpawn.setPosition(new Point(xpos, ypos)); - if (toSpawn.getId() == MobId.LOW_DARKSTAR) { - map.spawnFakeMonster(toSpawn); - } else { - map.spawnMonsterWithEffect(toSpawn, getSpawnEffect(), toSpawn.getPosition()); - } - monster.addSummonedMob(toSpawn); - } - } - } - } - break; - default: - log.warn("Unhandled Mob skill: {}", skillId); - break; + } + case ACC -> stats.put(MonsterStatus.ACC, x); + case EVA -> stats.put(MonsterStatus.AVOID, x); + case SPEED -> stats.put(MonsterStatus.SPEED, x); + case SEAL_SKILL -> stats.put(MonsterStatus.SEAL_SKILL, x); + case SUMMON -> summonMonsters(monster); } if (stats.size() > 0) { - if (lt != null && rb != null && skill) { - for (MapObject mons : getObjectsInRange(monster, MapObjectType.MONSTER)) { - ((Monster) mons).applyMonsterBuff(stats, getX(), getSkillId(), getDuration(), this, reflection); - } - } else { - monster.applyMonsterBuff(stats, getX(), getSkillId(), getDuration(), this, reflection); - } + applyMonsterBuffs(stats, skill, monster, reflection); } if (disease != null) { - if (lt != null && rb != null && skill) { - int i = 0; - for (Character character : getPlayersInRange(monster)) { - if (!character.hasActiveBuff(2321005)) { // holy shield - if (disease.equals(Disease.SEDUCE)) { - if (i < count) { - character.giveDebuff(Disease.SEDUCE, this); - i++; - } - } else { - character.giveDebuff(disease, this); + applyDisease(disease, skill, monster, player); + } + } + + private void applyHealEffect(boolean skill, Monster monster) { + if (lt != null && rb != null && skill) { + List objects = getObjectsInRange(monster, MapObjectType.MONSTER); + final int hps = (getX() / 1000) * (int) (950 + 1050 * Math.random()); + for (MapObject mons : objects) { + ((Monster) mons).heal(hps, getY()); + } + } else { + monster.heal(getX(), getY()); + } + } + + private void applyDispelEffect(boolean skill, Monster monster, Character player) { + if (lt != null && rb != null && skill) { + getPlayersInRange(monster).forEach(Character::dispel); + } else { + player.dispel(); + } + } + + private void applyBanishEffect(boolean skill, Monster monster, Character player, + List banishPlayersOutput) { + if (lt != null && rb != null && skill) { + banishPlayersOutput.addAll(getPlayersInRange(monster)); + } else { + banishPlayersOutput.add(player); + } + } + + private void spawnMonsterMist(Monster monster) { + Rectangle mistArea = calculateBoundingBox(monster.getPosition()); + var mist = new Mist(mistArea, monster, this); + int mistDuration = x * 100; + monster.getMap().spawnMist(mist, mistDuration, false, false, false); + } + + private void summonMonsters(Monster monster) { + int skillLimit = this.getLimit(); + MapleMap map = monster.getMap(); + + if (MapId.isDojo(map.getId())) { // spawns in dojo should be unlimited + skillLimit = Integer.MAX_VALUE; + } + + if (map.getSpawnedMonstersOnMap() < 80) { + List summons = getSummons(); + int summonLimit = monster.countAvailableMobSummons(summons.size(), skillLimit); + if (summonLimit >= 1) { + boolean bossRushMap = MapId.isBossRush(map.getId()); + + Collections.shuffle(summons); + for (Integer mobId : summons.subList(0, summonLimit)) { + Monster toSpawn = LifeFactory.getMonster(mobId); + if (toSpawn != null) { + if (bossRushMap) { + toSpawn.disableDrops(); // no littering on BRPQ pls } + toSpawn.setPosition(monster.getPosition()); + int ypos, xpos; + xpos = (int) monster.getPosition().getX(); + ypos = (int) monster.getPosition().getY(); + switch (mobId) { + case MobId.HIGH_DARKSTAR: // Pap bomb high + toSpawn.setFh((int) Math.ceil(Math.random() * 19.0)); + ypos = -590; + break; + case MobId.LOW_DARKSTAR: // Pap bomb + xpos = (int) (monster.getPosition().getX() + Randomizer.nextInt(1000) - 500); + if (ypos != -590) { + ypos = (int) monster.getPosition().getY(); + } + break; + case MobId.BLOODY_BOOM: //Pianus bomb + if (Math.ceil(Math.random() * 5) == 1) { + ypos = 78; + xpos = Randomizer.nextInt(5) + (Randomizer.nextInt(2) == 1 ? 180 : 0); + } else { + xpos = (int) (monster.getPosition().getX() + Randomizer.nextInt(1000) - 500); + } + break; + } + switch (map.getId()) { + case MapId.ORIGIN_OF_CLOCKTOWER: //Pap map + if (xpos < -890) { + xpos = (int) (Math.ceil(Math.random() * 150) - 890); + } else if (xpos > 230) { + xpos = (int) (230 - Math.ceil(Math.random() * 150)); + } + break; + case MapId.CAVE_OF_PIANUS: // Pianus map + if (xpos < -239) { + xpos = (int) (Math.ceil(Math.random() * 150) - 239); + } else if (xpos > 371) { + xpos = (int) (371 - Math.ceil(Math.random() * 150)); + } + break; + } + toSpawn.setPosition(new Point(xpos, ypos)); + if (toSpawn.getId() == MobId.LOW_DARKSTAR) { + map.spawnFakeMonster(toSpawn); + } else { + map.spawnMonsterWithEffect(toSpawn, getSpawnEffect(), toSpawn.getPosition()); + } + monster.addSummonedMob(toSpawn); } } - } else { - player.giveDebuff(disease, this); } } } + private void applyMonsterBuffs(Map stats, boolean skill, Monster monster, List reflection) { + if (lt != null && rb != null && skill) { + for (MapObject mons : getObjectsInRange(monster, MapObjectType.MONSTER)) { + ((Monster) mons).applyMonsterBuff(stats, getX(), getDuration(), this, reflection); + } + } else { + monster.applyMonsterBuff(stats, getX(), getDuration(), this, reflection); + } + } + + private void applyDisease(Disease disease, boolean skill, Monster monster, Character player) { + if (lt != null && rb != null && skill) { + int i = 0; + for (Character character : getPlayersInRange(monster)) { + if (!character.hasActiveBuff(Bishop.HOLY_SHIELD)) { + if (disease.equals(Disease.SEDUCE)) { + if (i < count) { + character.giveDebuff(Disease.SEDUCE, this); + i++; + } + } else { + character.giveDebuff(disease, this); + } + } + } + } else { + player.giveDebuff(disease, this); + } + } + private List getPlayersInRange(Monster monster) { return monster.getMap().getPlayersInRange(calculateBoundingBox(monster.getPosition())); } - public int getSkillId() { - return skillId; + public MobSkillId getId() { + return id; } - public int getSkillLevel() { - return skillLevel; + public MobSkillType getType() { + return id.type(); } public int getMpCon() { @@ -374,7 +410,7 @@ public class MobSkill { } public List getSummons() { - return new ArrayList<>(toSummon); + return toSummon; } public int getSpawnEffect() { diff --git a/src/main/java/server/life/MobSkillFactory.java b/src/main/java/server/life/MobSkillFactory.java index 055cbbc695..ac02cfa2d2 100644 --- a/src/main/java/server/life/MobSkillFactory.java +++ b/src/main/java/server/life/MobSkillFactory.java @@ -28,10 +28,8 @@ import provider.DataTool; import provider.wz.WZFiles; import java.awt.*; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -42,76 +40,93 @@ import static java.util.concurrent.TimeUnit.SECONDS; * @author Danny (Leifde) */ public class MobSkillFactory { - private static final Map mobSkills = new HashMap<>(); - private final static DataProvider dataSource = DataProviderFactory.getDataProvider(WZFiles.SKILL); + private static final DataProvider dataSource = DataProviderFactory.getDataProvider(WZFiles.SKILL); private static final Data skillRoot = dataSource.getData("MobSkill.img"); - private final static ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - private final static Lock readLock = readWriteLock.readLock(); - private final static Lock writeLock = readWriteLock.writeLock(); + private static final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + private static final Lock readLock = readWriteLock.readLock(); + private static final Lock writeLock = readWriteLock.writeLock(); - public static MobSkill getMobSkill(final int skillId, final int level) { - final String key = skillId + "" + level; + public static MobSkill getMobSkill(final MobSkillType type, final int level) { readLock.lock(); try { - MobSkill ret = mobSkills.get(key); - if (ret != null) { - return ret; + MobSkill ms = mobSkills.get(createKey(type, level)); + if (ms != null) { + return ms; } } finally { readLock.unlock(); } + + return loadMobSkill(type, level).orElseThrow( + () -> new IllegalArgumentException("No MobSkill exists for type %s, level %d".formatted(type, level)) + ); + } + + private static Optional loadMobSkill(final MobSkillType type, final int level) { writeLock.lock(); try { - MobSkill ret; - ret = mobSkills.get(key); - if (ret == null) { - Data skillData = skillRoot.getChildByPath(skillId + "/level/" + level); - if (skillData != null) { - int mpCon = DataTool.getInt(skillData.getChildByPath("mpCon"), 0); - List toSummon = new ArrayList<>(); - for (int i = 0; i > -1; i++) { - if (skillData.getChildByPath(String.valueOf(i)) == null) { - break; - } - toSummon.add(DataTool.getInt(skillData.getChildByPath(String.valueOf(i)), 0)); - } - int effect = DataTool.getInt("summonEffect", skillData, 0); - int hp = DataTool.getInt("hp", skillData, 100); - int x = DataTool.getInt("x", skillData, 1); - int y = DataTool.getInt("y", skillData, 1); - int count = DataTool.getInt("count", skillData, 1); - long duration = SECONDS.toMillis(DataTool.getInt("time", skillData, 0)); - long cooltime = SECONDS.toMillis(DataTool.getInt("interval", skillData, 0)); - int iprop = DataTool.getInt("prop", skillData, 100); - float prop = iprop / 100; - int limit = DataTool.getInt("limit", skillData, 0); - Data ltd = skillData.getChildByPath("lt"); - Point lt = null; - Point rb = null; - if (ltd != null) { - lt = (Point) ltd.getData(); - rb = (Point) skillData.getChildByPath("rb").getData(); - } - ret = new MobSkill(skillId, level); - ret.addSummons(toSummon); - ret.setCoolTime(cooltime); - ret.setDuration(duration); - ret.setHp(hp); - ret.setMpCon(mpCon); - ret.setSpawnEffect(effect); - ret.setX(x); - ret.setY(y); - ret.setCount(count); - ret.setProp(prop); - ret.setLimit(limit); - ret.setLtRb(lt, rb); - } - mobSkills.put(skillId + "" + level, ret); + MobSkill existingMs = mobSkills.get(createKey(type, level)); + if (existingMs != null) { + return Optional.of(existingMs); } - return ret; + + Data skillData = skillRoot.getChildByPath("%d/level/%d".formatted(type.getId(), level)); + if (skillData == null) { + return Optional.empty(); + } + + int mpCon = DataTool.getInt("mpCon", skillData, 0); + List toSummon = new ArrayList<>(); + for (int i = 0; i > -1; i++) { + if (skillData.getChildByPath(String.valueOf(i)) == null) { + break; + } + toSummon.add(DataTool.getInt(skillData.getChildByPath(String.valueOf(i)), 0)); + } + int effect = DataTool.getInt("summonEffect", skillData, 0); + int hp = DataTool.getInt("hp", skillData, 100); + int x = DataTool.getInt("x", skillData, 1); + int y = DataTool.getInt("y", skillData, 1); + int count = DataTool.getInt("count", skillData, 1); + long duration = SECONDS.toMillis(DataTool.getInt("time", skillData, 0)); + long cooltime = SECONDS.toMillis(DataTool.getInt("interval", skillData, 0)); + int iprop = DataTool.getInt("prop", skillData, 100); + float prop = iprop / 100; + int limit = DataTool.getInt("limit", skillData, 0); + + Data ltData = skillData.getChildByPath("lt"); + Data rbData = skillData.getChildByPath("rb"); + Point lt = null; + Point rb = null; + if (ltData != null && rbData != null) { + lt = (Point) ltData.getData(); + rb = (Point) rbData.getData(); + } + + MobSkill loadedMobSkill = new MobSkill.Builder(type, level) + .mpCon(mpCon) + .toSummon(toSummon) + .cooltime(cooltime) + .duration(duration) + .hp(hp) + .x(x) + .y(y) + .count(count) + .prop(prop) + .limit(limit) + .lt(lt) + .rb(rb) + .build(); + + mobSkills.put(createKey(type, level), loadedMobSkill); + return Optional.of(loadedMobSkill); } finally { writeLock.unlock(); } } + + private static String createKey(MobSkillType type, int skillLevel) { + return type.getId() + "" + skillLevel; + } } diff --git a/src/main/java/server/life/MobSkillId.java b/src/main/java/server/life/MobSkillId.java new file mode 100644 index 0000000000..0e80304ca0 --- /dev/null +++ b/src/main/java/server/life/MobSkillId.java @@ -0,0 +1,3 @@ +package server.life; + +public record MobSkillId(MobSkillType type, int level) {} diff --git a/src/main/java/server/life/MobSkillType.java b/src/main/java/server/life/MobSkillType.java new file mode 100644 index 0000000000..cf38c5e057 --- /dev/null +++ b/src/main/java/server/life/MobSkillType.java @@ -0,0 +1,64 @@ +package server.life; + +import java.util.Arrays; + +public enum MobSkillType { + ATTACK_UP(100), + MAGIC_ATTACK_UP(101), + DEFENSE_UP(102), + MAGIC_DEFENSE_UP(103), + ATTACK_UP_M(110), + MAGIC_ATTACK_UP_M(111), + DEFENSE_UP_M(112), + MAGIC_DEFENSE_UP_M(113), + HEAL_M(114), + HASTE_M(115), + SEAL(120), + DARKNESS(121), + WEAKNESS(122), + STUN(123), + CURSE(124), + POISON(125), + SLOW(126), + DISPEL(127), + SEDUCE(128), + BANISH(129), + AREA_POISON(131), + REVERSE_INPUT(132), + UNDEAD(133), + STOP_POTION(134), + STOP_MOTION(135), + FEAR(136), + PHYSICAL_IMMUNE(140), + MAGIC_IMMUNE(141), + HARD_SKIN(142), + PHYSICAL_COUNTER(143), + MAGIC_COUNTER(144), + PHYSICAL_AND_MAGIC_COUNTER(145), + PAD(150), + MAD(151), + PDR(152), + MDR(153), + ACC(154), + EVA(155), + SPEED(156), + SEAL_SKILL(157), + SUMMON(200); + + private final int id; + + MobSkillType(int id) { + this.id = id; + } + + public static MobSkillType from(int id) { + return Arrays.stream(values()) + .filter(type -> type.getId() == id) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + } + + public int getId() { + return id; + } +} diff --git a/src/main/java/server/life/Monster.java b/src/main/java/server/life/Monster.java index a494c01915..efd42f8b8d 100644 --- a/src/main/java/server/life/Monster.java +++ b/src/main/java/server/life/Monster.java @@ -82,8 +82,7 @@ public class Monster extends AbstractLoadedLife { private int VenomMultiplier = 0; private boolean fake = false; private boolean dropsDisabled = false; - private final List> usedSkills = new ArrayList<>(); - private final Map, Integer> skillsUsed = new HashMap<>(); + private final Set usedSkills = new HashSet<>(); private final Set usedAttacks = new HashSet<>(); private Set calledMobOids = null; private WeakReference callerMob = new WeakReference<>(null); @@ -1276,11 +1275,11 @@ public class Monster extends AbstractLoadedLife { return true; } - public final void dispelSkill(final MobSkill skillId) { + public final void dispelSkill(final MobSkill skill) { List toCancel = new ArrayList<>(); for (Entry effects : stati.entrySet()) { MonsterStatusEffect mse = effects.getValue(); - if (mse.getMobSkill() != null && mse.getMobSkill().getSkillId() == skillId.getSkillId()) { //not checking for level. + if (mse.getMobSkill() != null && mse.getMobSkill().getType() == skill.getType()) { //not checking for level. toCancel.add(effects.getKey()); } } @@ -1289,7 +1288,7 @@ public class Monster extends AbstractLoadedLife { } } - public void applyMonsterBuff(final Map stats, final int x, int skillId, long duration, MobSkill skill, final List reflection) { + public void applyMonsterBuff(final Map stats, final int x, long duration, MobSkill skill, final List reflection) { final Runnable cancelTask = () -> { if (isAlive()) { Packet packet = PacketCreator.cancelMonsterStatus(getObjectId(), stats); @@ -1426,7 +1425,7 @@ public class Monster extends AbstractLoadedLife { return map.getAggroCoordinator(); } - public List> getSkills() { + public Set getSkills() { return stats.getSkills(); } @@ -1434,26 +1433,12 @@ public class Monster extends AbstractLoadedLife { return stats.hasSkill(skillId, level); } - public int getSkillPos(int skillId, int level) { - int pos = 0; - for (Pair ms : this.getSkills()) { - if (ms.getLeft() == skillId && ms.getRight() == level) { - return pos; - } - - pos++; - } - - return -1; - } - public boolean canUseSkill(MobSkill toUse, boolean apply) { if (toUse == null || isBuffed(MonsterStatus.SEAL_SKILL)) { return false; } - int useSkillid = toUse.getSkillId(); - if (useSkillid >= 143 && useSkillid <= 145) { + if (isReflectSkill(toUse)) { if (this.isBuffed(MonsterStatus.WEAPON_REFLECT) || this.isBuffed(MonsterStatus.MAGIC_REFLECT)) { return false; } @@ -1461,10 +1446,8 @@ public class Monster extends AbstractLoadedLife { monsterLock.lock(); try { - for (Pair skill : usedSkills) { // thanks OishiiKawaiiDesu for noticing an issue with mobskill cooldown - if (skill.getLeft() == useSkillid && skill.getRight() == toUse.getSkillLevel()) { - return false; - } + if (usedSkills.contains(toUse.getId())) { + return false; } int mpCon = toUse.getMpCon(); @@ -1488,48 +1471,36 @@ public class Monster extends AbstractLoadedLife { return true; } - private void usedSkill(MobSkill skill) { - final int skillId = skill.getSkillId(), level = skill.getSkillLevel(); - long cooltime = skill.getCoolTime(); + private boolean isReflectSkill(MobSkill mobSkill) { + return switch (mobSkill.getType()) { + case PHYSICAL_COUNTER, MAGIC_COUNTER, PHYSICAL_AND_MAGIC_COUNTER -> true; + default -> false; + }; + } + private void usedSkill(MobSkill skill) { + final MobSkillId msId = skill.getId(); monsterLock.lock(); try { mp -= skill.getMpCon(); - Pair skillKey = new Pair<>(skillId, level); - this.usedSkills.add(skillKey); - - Integer useCount = this.skillsUsed.remove(skillKey); - if (useCount != null) { - this.skillsUsed.put(skillKey, useCount + 1); - } else { - this.skillsUsed.put(skillKey, 1); - } + this.usedSkills.add(msId); } finally { monsterLock.unlock(); } final Monster mons = this; MapleMap mmap = mons.getMap(); - Runnable r = () -> mons.clearSkill(skillId, level); + Runnable r = () -> mons.clearSkill(skill.getId()); MobClearSkillService service = (MobClearSkillService) map.getChannelServer().getServiceAccess(ChannelServices.MOB_CLEAR_SKILL); - service.registerMobClearSkillAction(mmap.getId(), r, cooltime); + service.registerMobClearSkillAction(mmap.getId(), r, skill.getCoolTime()); } - private void clearSkill(int skillId, int level) { + private void clearSkill(MobSkillId msId) { monsterLock.lock(); try { - int index = -1; - for (Pair skill : usedSkills) { - if (skill.getLeft() == skillId && skill.getRight() == level) { - index = usedSkills.indexOf(skill); - break; - } - } - if (index != -1) { - usedSkills.remove(index); - } + usedSkills.remove(msId); } finally { monsterLock.unlock(); } @@ -1593,8 +1564,20 @@ public class Monster extends AbstractLoadedLife { } } - public int getNoSkills() { - return this.stats.getNoSkills(); + public boolean hasAnySkill() { + return this.stats.getNoSkills() > 0; + } + + public MobSkillId getRandomSkill() { + Set skills = stats.getSkills(); + if (skills.size() == 0) { + return null; + } + // There is no simple way of getting a random element from a Set. Have to make do with this. + return skills.stream() + .skip(Randomizer.nextInt(skills.size())) + .findAny() + .orElse(null); } public boolean isFirstAttack() { diff --git a/src/main/java/server/life/MonsterStats.java b/src/main/java/server/life/MonsterStats.java index 8a7cecddbd..093adc9334 100644 --- a/src/main/java/server/life/MonsterStats.java +++ b/src/main/java/server/life/MonsterStats.java @@ -41,7 +41,7 @@ public class MonsterStats { public Map resistance = new HashMap<>(); public List revives = Collections.emptyList(); public byte tagColor, tagBgColor; - public List> skills = new ArrayList<>(); + public Set skills = new HashSet<>(); public Pair cool = null; public BanishInfo banish = null; public List loseItem = null; @@ -190,18 +190,12 @@ public class MonsterStats { this.tagBgColor = (byte) tagBgColor; } - public void setSkills(List> skills) { - for (int i = this.skills.size(); i < skills.size(); i++) { - this.skills.add(null); - } - - for (int i = 0; i < skills.size(); i++) { - this.skills.set(i, skills.get(i)); - } + public void setSkills(Set skills) { + this.skills = skills; } - public List> getSkills() { - return Collections.unmodifiableList(this.skills); + public Set getSkills() { + return Collections.unmodifiableSet(this.skills); } public int getNoSkills() { @@ -209,8 +203,8 @@ public class MonsterStats { } public boolean hasSkill(int skillId, int level) { - for (Pair skill : skills) { - if (skill.getLeft() == skillId && skill.getRight() == level) { + for (MobSkillId skill : skills) { + if (skill.type().getId() == skillId && skill.level() == level) { return true; } } diff --git a/src/main/java/server/maps/Mist.java b/src/main/java/server/maps/Mist.java index 0b9960824e..6aa41391d8 100644 --- a/src/main/java/server/maps/Mist.java +++ b/src/main/java/server/maps/Mist.java @@ -138,14 +138,14 @@ public class Mist extends AbstractMapObject { if (owner != null) { return PacketCreator.spawnMist(getObjectId(), owner.getId(), getSourceSkill().getId(), owner.getSkillLevel(SkillFactory.getSkill(source.getSourceId())), this); } - return PacketCreator.spawnMist(getObjectId(), mob.getId(), skill.getSkillId(), skill.getSkillLevel(), this); + return PacketCreator.spawnMobMist(getObjectId(), mob.getId(), skill.getId(), this); } public final Packet makeFakeSpawnData(int level) { if (owner != null) { return PacketCreator.spawnMist(getObjectId(), owner.getId(), getSourceSkill().getId(), level, this); } - return PacketCreator.spawnMist(getObjectId(), mob.getId(), skill.getSkillId(), skill.getSkillLevel(), this); + return PacketCreator.spawnMobMist(getObjectId(), mob.getId(), skill.getId(), this); } @Override diff --git a/src/main/java/server/partyquest/CarnivalFactory.java b/src/main/java/server/partyquest/CarnivalFactory.java index 5eb40a6224..450a08c080 100644 --- a/src/main/java/server/partyquest/CarnivalFactory.java +++ b/src/main/java/server/partyquest/CarnivalFactory.java @@ -8,6 +8,7 @@ import provider.DataTool; import provider.wz.WZFiles; import server.life.MobSkill; import server.life.MobSkillFactory; +import server.life.MobSkillType; import java.util.ArrayList; import java.util.HashMap; @@ -42,7 +43,15 @@ public class CarnivalFactory { } for (Data z : dataRoot.getData("MCSkill.img")) { Integer id = Integer.parseInt(z.getName()); - MCSkill ms = new MCSkill(DataTool.getInt("spendCP", z, 0), DataTool.getInt("mobSkillID", z, 0), DataTool.getInt("level", z, 0), DataTool.getInt("target", z, 1) > 1); + int spendCp = DataTool.getInt("spendCP", z, 0); + int mobSkillId = DataTool.getInt("mobSkillID", z, 0); + MobSkillType mobSkillType = null; + if (mobSkillId != 0) { + mobSkillType = MobSkillType.from(mobSkillId); + } + int level = DataTool.getInt("level", z, 0); + boolean isMultiTarget = DataTool.getInt("target", z, 1) > 1; + MCSkill ms = new MCSkill(spendCp, mobSkillType, level, isMultiTarget); skills.put(id, ms); if (ms.targetsAll) { @@ -52,7 +61,11 @@ public class CarnivalFactory { } } for (Data z : dataRoot.getData("MCGuardian.img")) { - guardians.put(Integer.parseInt(z.getName()), new MCSkill(DataTool.getInt("spendCP", z, 0), DataTool.getInt("mobSkillID", z, 0), DataTool.getInt("level", z, 0), true)); + int spendCp = DataTool.getInt("spendCP", z, 0); + int mobSkillId = DataTool.getInt("mobSkillID", z, 0); + MobSkillType mobSkillType = MobSkillType.from(mobSkillId); + int level = DataTool.getInt("level", z, 0); + guardians.put(Integer.parseInt(z.getName()), new MCSkill(spendCp, mobSkillType, level, true)); } } @@ -66,7 +79,7 @@ public class CarnivalFactory { public MCSkill getSkill(final int id) { MCSkill skill = skills.get(id); - if (skill != null && skill.skillid <= 0) { + if (skill != null && skill.mobSkillType == null) { return randomizeSkill(skill.targetsAll); } else { return skill; @@ -77,28 +90,13 @@ public class CarnivalFactory { return guardians.get(id); } - public static class MCSkill { - - public int cpLoss, skillid, level; - public boolean targetsAll; - - public MCSkill(int _cpLoss, int _skillid, int _level, boolean _targetsAll) { - cpLoss = _cpLoss; - skillid = _skillid; - level = _level; - targetsAll = _targetsAll; - } - + public record MCSkill(int cpLoss, MobSkillType mobSkillType, int level, boolean targetsAll) { public MobSkill getSkill() { - return getMobSkill(skillid, level); - } - - public static MobSkill getMobSkill(int skillid, int level) { - return MobSkillFactory.getMobSkill(skillid, level); + return MobSkillFactory.getMobSkill(mobSkillType, level); } public Disease getDisease() { - return Disease.getBySkill(skillid); + return Disease.getBySkill(mobSkillType); } } } diff --git a/src/main/java/tools/ArrayMap.java b/src/main/java/tools/ArrayMap.java deleted file mode 100644 index a224b3059d..0000000000 --- a/src/main/java/tools/ArrayMap.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - This file is part of the OdinMS Maple Story Server - Copyright (C) 2008 Patrick Huy - Matthias Butz - Jan Christian Meyer - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation version 3 as published by - the Free Software Foundation. You may not use, modify or distribute - this program under any other version of the GNU Affero General Public - License. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ -package tools; - -import java.util.*; - -public class ArrayMap extends AbstractMap { - - static class Entry implements Map.Entry { - protected K key; - protected V value; - - public Entry(K key, V value) { - this.key = key; - this.value = value; - } - - @Override - public K getKey() { - return key; - } - - @Override - public V getValue() { - return value; - } - - @Override - public V setValue(V newValue) { - V oldValue = value; - value = newValue; - return oldValue; - } - - @Override - public boolean equals(Object o) { - if (o instanceof Map.Entry e) { - return (key == null ? e.getKey() == null : key.equals(e.getKey())) && (value == null ? e.getValue() == null : value.equals(e.getValue())); - } - return false; - } - - @Override - public int hashCode() { - int keyHash = (key == null ? 0 : key.hashCode()); - int valueHash = (value == null ? 0 : value.hashCode()); - return keyHash ^ valueHash; - } - - @Override - public String toString() { - return key + "=" + value; - } - } - - private Set> entries = null; - private final ArrayList> list; - - public ArrayMap() { - list = new ArrayList<>(); - } - - public ArrayMap(Map map) { - list = new ArrayList<>(); - putAll(map); - } - - public ArrayMap(int initialCapacity) { - list = new ArrayList<>(initialCapacity); - } - - @Override - @SuppressWarnings("unchecked") - public Set> entrySet() { - if (entries == null) { - entries = new AbstractSet>() { - @Override - public void clear() { - throw new UnsupportedOperationException(); - } - - @Override - public Iterator> iterator() { - return list.iterator(); - } - - @Override - public int size() { - return list.size(); - } - }; - } - return (Set>) entries; - } - - @Override - public V put(K key, V value) { - int size = list.size(); - Entry entry = null; - int i; - if (key == null) { - for (i = 0; i < size; i++) { - entry = (list.get(i)); - if (entry.getKey() == null) { - break; - } - } - } else { - for (i = 0; i < size; i++) { - entry = (list.get(i)); - if (key.equals(entry.getKey())) { - break; - } - } - } - V oldValue = null; - if (i < size) { - oldValue = entry.getValue(); - entry.setValue(value); - } else { - list.add(new Entry<>(key, value)); - } - return oldValue; - } -} diff --git a/src/main/java/tools/PacketCreator.java b/src/main/java/tools/PacketCreator.java index 6df026de3d..54629626b3 100644 --- a/src/main/java/tools/PacketCreator.java +++ b/src/main/java/tools/PacketCreator.java @@ -64,10 +64,7 @@ import server.CashShop.CashItemFactory; import server.CashShop.SpecialCashItem; import server.*; import server.events.gm.Snowball; -import server.life.MobSkill; -import server.life.Monster; -import server.life.NPC; -import server.life.PlayerNPC; +import server.life.*; import server.maps.*; import server.maps.MiniGame.MiniGameResult; import server.movement.LifeMovementFragment; @@ -106,6 +103,11 @@ public class PacketCreator { return utcTimestamp * 10000 + FT_UT_OFFSET; } + private static void writeMobSkillId(OutPacket packet, MobSkillId msId) { + packet.writeShort(msId.type().getId()); + packet.writeShort(msId.level()); + } + public static Packet showHpHealed(int cid, int amount) { OutPacket p = OutPacket.create(SendOpcode.SHOW_FOREIGN_EFFECT); p.writeInt(cid); @@ -1410,8 +1412,7 @@ public class PacketCreator { MobSkill mobSkill = mse.getMobSkill(); if (mobSkill != null) { - p.writeShort(mobSkill.getSkillId()); - p.writeShort(mobSkill.getSkillLevel()); + writeMobSkillId(p, mobSkill.getId()); switch (s.getKey()) { case WEAPON_REFLECT -> pCounter = mobSkill.getX(); @@ -2917,8 +2918,7 @@ public class PacketCreator { writeLongMaskD(p, statups); for (Pair statup : statups) { p.writeShort(statup.getRight().shortValue()); - p.writeShort(skill.getSkillId()); - p.writeShort(skill.getSkillLevel()); + writeMobSkillId(p, skill.getId()); p.writeInt((int) skill.getDuration()); } p.writeShort(0); // ??? wk charges have 600 here o.o @@ -2936,8 +2936,7 @@ public class PacketCreator { if (statup.getLeft() == Disease.POISON) { p.writeShort(statup.getRight().shortValue()); } - p.writeShort(skill.getSkillId()); - p.writeShort(skill.getSkillLevel()); + writeMobSkillId(p, skill.getId()); } p.writeShort(0); // same as give_buff p.writeShort(900);//Delay @@ -3051,8 +3050,7 @@ public class PacketCreator { if (statup.getLeft() == Disease.POISON) { p.writeShort(statup.getRight().shortValue()); } - p.writeShort(skill.getSkillId()); - p.writeShort(skill.getSkillLevel()); + writeMobSkillId(p, skill.getId()); } p.writeShort(0); // same as give_buff p.writeShort(900);//Delay @@ -3932,8 +3930,7 @@ public class PacketCreator { for (Map.Entry stat : stati.entrySet()) { p.writeShort(stat.getValue()); if (mse.isMonsterSkill()) { - p.writeShort(mse.getMobSkill().getSkillId()); - p.writeShort(mse.getMobSkill().getSkillLevel()); + writeMobSkillId(p, mse.getMobSkill().getId()); } else { p.writeInt(mse.getSkill().getId()); } @@ -3984,11 +3981,15 @@ public class PacketCreator { return p; } - public static Packet spawnMist(int objId, int ownerChrId, int skill, int level, Mist mist) { + public static Packet spawnMobMist(int objId, int ownerMobId, MobSkillId msId, Mist mist) { + return spawnMist(objId, ownerMobId, msId.type().getId(), msId.level(), mist); + } + + public static Packet spawnMist(int objId, int ownerId, int skill, int level, Mist mist) { OutPacket p = OutPacket.create(SendOpcode.SPAWN_MIST); p.writeInt(objId); p.writeInt(mist.isMobMist() ? 0 : mist.isPoisonMist() ? 1 : mist.isRecoveryMist() ? 4 : 2); // mob mist = 0, player poison = 1, smokescreen = 2, unknown = 3, recovery = 4 - p.writeInt(ownerChrId); + p.writeInt(ownerId); p.writeInt(skill); p.writeByte(level); p.writeShort(mist.getSkillDelay()); // Skill delay diff --git a/src/main/java/tools/mapletools/MonsterStatFetcher.java b/src/main/java/tools/mapletools/MonsterStatFetcher.java index 508e6d0815..8b0dc826a6 100644 --- a/src/main/java/tools/mapletools/MonsterStatFetcher.java +++ b/src/main/java/tools/mapletools/MonsterStatFetcher.java @@ -3,12 +3,10 @@ package tools.mapletools; import provider.*; import provider.wz.DataType; import provider.wz.WZFiles; -import server.life.Element; -import server.life.ElementalEffectiveness; +import server.life.*; import server.life.LifeFactory.BanishInfo; import server.life.LifeFactory.loseItem; import server.life.LifeFactory.selfDestruction; -import server.life.MonsterStats; import tools.Pair; import java.time.Duration; @@ -110,9 +108,12 @@ public class MonsterStatFetcher { Data monsterSkillData = monsterInfoData.getChildByPath("skill"); if (monsterSkillData != null) { int i = 0; - List> skills = new ArrayList<>(); + Set skills = new HashSet<>(); while (monsterSkillData.getChildByPath(Integer.toString(i)) != null) { - skills.add(new Pair<>(DataTool.getInt(i + "/skill", monsterSkillData, 0), DataTool.getInt(i + "/level", monsterSkillData, 0))); + int skillId = DataTool.getInt(i + "/skill", monsterSkillData, 0); + MobSkillType type = MobSkillType.from(skillId); + int skillLevel = DataTool.getInt(i + "/level", monsterSkillData, 0); + skills.add(new MobSkillId(type, skillLevel)); i++; } stats.setSkills(skills); diff --git a/src/test/java/server/life/MobSkillFactoryTest.java b/src/test/java/server/life/MobSkillFactoryTest.java new file mode 100644 index 0000000000..09b4f7fbbe --- /dev/null +++ b/src/test/java/server/life/MobSkillFactoryTest.java @@ -0,0 +1,68 @@ +package server.life; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class MobSkillFactoryTest { + + @TempDir + private Path wzPath; + + @BeforeEach + void setWzPath() { + MockitoAnnotations.openMocks(this); + writeTestFileToTempDir(); + System.setProperty("wz-path", "%s/wz".formatted(wzPath.toString())); + } + + private void writeTestFileToTempDir() { + try { + String testFileContents = readTestFileContents(); + writeTempDirFile(testFileContents); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String readTestFileContents() throws IOException { + return new String(getClass() + .getClassLoader() + .getResourceAsStream("MobSkill-test.img.xml") + .readAllBytes() + ); + } + + private void writeTempDirFile(String fileContents) throws IOException { + Path tempDirDirectory = wzPath.resolve("wz/Skill.wz"); + Files.createDirectories(tempDirDirectory); + Path tempDirFile = Files.createFile(tempDirDirectory.resolve("MobSkill.img.xml")); + Files.writeString(tempDirFile, fileContents); + } + + @Test + void shouldLoadExistingMobSkill() { + MobSkill mobSkill = MobSkillFactory.getMobSkill(MobSkillType.ATTACK_UP, 1); + + assertNotNull(mobSkill); + assertAll("MobSkill", + () -> assertEquals(115, mobSkill.getX()), + () -> assertEquals(5, mobSkill.getMpCon()), + () -> assertEquals(40_000, mobSkill.getCoolTime()), + () -> assertEquals(30_000, mobSkill.getDuration()) + ); + } + + @Test + void shouldThrowExceptionOnNonExisting() { + assertThrows(IllegalArgumentException.class, () -> MobSkillFactory.getMobSkill(MobSkillType.DEFENSE_UP, 1)); + } + +} \ No newline at end of file diff --git a/src/test/resources/MobSkill-test.img.xml b/src/test/resources/MobSkill-test.img.xml new file mode 100644 index 0000000000..3a2658d0be --- /dev/null +++ b/src/test/resources/MobSkill-test.img.xml @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +