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 extends java.util.Map.Entry> 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+