Merge pull request #116 from P0nk/mob-skill-refactoring

Clean up and refactor mob skills
This commit is contained in:
Ponk
2022-09-07 19:28:53 +02:00
committed by GitHub
25 changed files with 1240 additions and 715 deletions
+11 -1
View File
@@ -28,11 +28,12 @@
<log4j.version>2.18.0</log4j.version> <!-- Slf4j implementation -->
<graalvm.version>22.2.0</graalvm.version> <!-- ScriptEngine implementation -->
<netty.version>4.1.79.Final</netty.version> <!-- Networking -->
<junit.version>5.9.0</junit.version> <!-- Unit test -->
<yamlbeans.version>1.15</yamlbeans.version> <!-- Config file -->
<jcip-annotations.version>1.0</jcip-annotations.version> <!-- Annotations for concurrency documentation -->
<HikariCP.version>5.0.1</HikariCP.version> <!-- Database connection pool -->
<mysql-connector-java.version>8.0.30</mysql-connector-java.version> <!-- MySQL JDBC driver -->
<junit.version>5.9.0</junit.version> <!-- Unit test -->
<mockito.version>4.7.0</mockito.version> <!-- Unit test -->
</properties>
<dependencies>
@@ -120,16 +121,25 @@
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
+6 -10
View File
@@ -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();
}
+26 -28
View File
@@ -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);
}
}
@@ -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));
}
@@ -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) {
@@ -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<Pair<Integer, Integer>> mobSkills = monster.getSkills();
for (Pair<Integer, Integer> 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<Pair<Integer, Integer>> mobSkills = monster.getSkills();
for (Pair<Integer, Integer> 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);
}
@@ -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;
};
}
}
@@ -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<Integer, Integer> 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);
}
@@ -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);
}
@@ -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);
}
+10 -7
View File
@@ -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, Integer> monsterStatus = new ArrayMap<>();
Map<MonsterStatus, Integer> 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()) {
+4 -3
View File
@@ -188,11 +188,12 @@ public class LifeFactory {
Data monsterSkillInfoData = monsterInfoData.getChildByPath("skill");
if (monsterSkillInfoData != null) {
int i = 0;
List<Pair<Integer, Integer>> skills = new ArrayList<>();
Set<MobSkillId> 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);
}
+292 -256
View File
@@ -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<Integer> 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<Integer> 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<Integer> toSummon) {
this.id = new MobSkillId(type, level);
this.mpCon = mpCon;
}
public void addSummons(List<Integer> 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<Integer> 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<Integer> 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<Character> banishPlayers) {
// TODO: avoid output argument banishPlayersOutput
public void applyEffect(Character player, Monster monster, boolean skill, List<Character> banishPlayersOutput) {
Disease disease = null;
Map<MonsterStatus, Integer> stats = new ArrayMap<>();
List<Integer> 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<MapObject> 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<MonsterStatus, Integer> stats = new EnumMap<>(MonsterStatus.class);
List<Integer> 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<Integer> 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<MapObject> 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<Character> 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<Integer> 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<MonsterStatus, Integer> stats, boolean skill, Monster monster, List<Integer> 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<Character> 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<Integer> getSummons() {
return new ArrayList<>(toSummon);
return toSummon;
}
public int getSpawnEffect() {
+74 -59
View File
@@ -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<String, MobSkill> 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<MobSkill> 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<Integer> 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<Integer> 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;
}
}
@@ -0,0 +1,3 @@
package server.life;
public record MobSkillId(MobSkillType type, int level) {}
@@ -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;
}
}
+35 -52
View File
@@ -82,8 +82,7 @@ public class Monster extends AbstractLoadedLife {
private int VenomMultiplier = 0;
private boolean fake = false;
private boolean dropsDisabled = false;
private final List<Pair<Integer, Integer>> usedSkills = new ArrayList<>();
private final Map<Pair<Integer, Integer>, Integer> skillsUsed = new HashMap<>();
private final Set<MobSkillId> usedSkills = new HashSet<>();
private final Set<Integer> usedAttacks = new HashSet<>();
private Set<Integer> calledMobOids = null;
private WeakReference<Monster> 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<MonsterStatus> toCancel = new ArrayList<>();
for (Entry<MonsterStatus, MonsterStatusEffect> 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<MonsterStatus, Integer> stats, final int x, int skillId, long duration, MobSkill skill, final List<Integer> reflection) {
public void applyMonsterBuff(final Map<MonsterStatus, Integer> stats, final int x, long duration, MobSkill skill, final List<Integer> 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<Pair<Integer, Integer>> getSkills() {
public Set<MobSkillId> 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<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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<MobSkillId> 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() {
+7 -13
View File
@@ -41,7 +41,7 @@ public class MonsterStats {
public Map<Element, ElementalEffectiveness> resistance = new HashMap<>();
public List<Integer> revives = Collections.emptyList();
public byte tagColor, tagBgColor;
public List<Pair<Integer, Integer>> skills = new ArrayList<>();
public Set<MobSkillId> skills = new HashSet<>();
public Pair<Integer, Integer> cool = null;
public BanishInfo banish = null;
public List<loseItem> loseItem = null;
@@ -190,18 +190,12 @@ public class MonsterStats {
this.tagBgColor = (byte) tagBgColor;
}
public void setSkills(List<Pair<Integer, Integer>> 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<MobSkillId> skills) {
this.skills = skills;
}
public List<Pair<Integer, Integer>> getSkills() {
return Collections.unmodifiableList(this.skills);
public Set<MobSkillId> 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<Integer, Integer> skill : skills) {
if (skill.getLeft() == skillId && skill.getRight() == level) {
for (MobSkillId skill : skills) {
if (skill.type().getId() == skillId && skill.level() == level) {
return true;
}
}
+2 -2
View File
@@ -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
@@ -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);
}
}
}
-144
View File
@@ -1,144 +0,0 @@
/*
This file is part of the OdinMS Maple Story Server
Copyright (C) 2008 Patrick Huy <patrick.huy@frz.cc>
Matthias Butz <matze@odinms.de>
Jan Christian Meyer <vimes@odinms.de>
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 <http://www.gnu.org/licenses/>.
*/
package tools;
import java.util.*;
public class ArrayMap<K, V> extends AbstractMap<K, V> {
static class Entry<K, V> implements Map.Entry<K, V> {
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<K, V>> entries = null;
private final ArrayList<Entry<K, V>> list;
public ArrayMap() {
list = new ArrayList<>();
}
public ArrayMap(Map<K, V> map) {
list = new ArrayList<>();
putAll(map);
}
public ArrayMap(int initialCapacity) {
list = new ArrayList<>(initialCapacity);
}
@Override
@SuppressWarnings("unchecked")
public Set<java.util.Map.Entry<K, V>> entrySet() {
if (entries == null) {
entries = new AbstractSet<Entry<K, V>>() {
@Override
public void clear() {
throw new UnsupportedOperationException();
}
@Override
public Iterator<Entry<K, V>> iterator() {
return list.iterator();
}
@Override
public int size() {
return list.size();
}
};
}
return (Set<java.util.Map.Entry<K, V>>) entries;
}
@Override
public V put(K key, V value) {
int size = list.size();
Entry<K, V> 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;
}
}
+17 -16
View File
@@ -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<Disease, Integer> 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<MonsterStatus, Integer> 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
@@ -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<Pair<Integer, Integer>> skills = new ArrayList<>();
Set<MobSkillId> 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);
@@ -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));
}
}
+522
View File
@@ -0,0 +1,522 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<imgdir name="MobSkill.img">
<imgdir name="100">
<imgdir name="level">
<imgdir name="1">
<int name="x" value="115"/>
<int name="mpCon" value="5"/>
<int name="interval" value="40"/>
<int name="time" value="30"/>
<imgdir name="effect">
<canvas name="0" width="128" height="128">
<vector name="origin" x="62" y="65"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="1" width="128" height="127">
<vector name="origin" x="61" y="64"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="2" width="131" height="130">
<vector name="origin" x="65" y="65"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="3" width="141" height="140">
<vector name="origin" x="68" y="70"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="4" width="123" height="123">
<vector name="origin" x="58" y="62"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="5" width="126" height="122">
<vector name="origin" x="63" y="63"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="6" width="111" height="112">
<vector name="origin" x="54" y="57"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="7" width="24" height="23">
<vector name="origin" x="9" y="15"/>
<int name="delay" value="100"/>
<int name="a0" value="255"/>
<int name="a1" value="0"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="19" height="23">
<vector name="origin" x="9" y="44"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="2">
<int name="x" value="130"/>
<int name="mpCon" value="5"/>
<int name="interval" value="40"/>
<int name="time" value="30"/>
<imgdir name="effect">
<canvas name="0" width="128" height="128">
<vector name="origin" x="62" y="65"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="1" width="128" height="127">
<vector name="origin" x="61" y="64"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="2" width="131" height="130">
<vector name="origin" x="65" y="65"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="3" width="141" height="140">
<vector name="origin" x="68" y="70"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="4" width="123" height="123">
<vector name="origin" x="58" y="62"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="5" width="126" height="122">
<vector name="origin" x="63" y="63"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="6" width="111" height="112">
<vector name="origin" x="54" y="57"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="7" width="24" height="23">
<vector name="origin" x="9" y="15"/>
<int name="delay" value="100"/>
<int name="a0" value="255"/>
<int name="a1" value="0"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="19" height="23">
<vector name="origin" x="9" y="44"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="3">
<int name="x" value="130"/>
<int name="hp" value="50"/>
<int name="mpCon" value="5"/>
<int name="interval" value="40"/>
<int name="time" value="30"/>
<imgdir name="effect">
<canvas name="0" width="128" height="128">
<vector name="origin" x="62" y="65"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="1" width="128" height="127">
<vector name="origin" x="61" y="64"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="2" width="131" height="130">
<vector name="origin" x="65" y="65"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="3" width="141" height="140">
<vector name="origin" x="68" y="70"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="4" width="123" height="123">
<vector name="origin" x="58" y="62"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="5" width="126" height="122">
<vector name="origin" x="63" y="63"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="6" width="111" height="112">
<vector name="origin" x="54" y="57"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="7" width="24" height="23">
<vector name="origin" x="9" y="15"/>
<int name="delay" value="100"/>
<int name="a0" value="255"/>
<int name="a1" value="0"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="19" height="23">
<vector name="origin" x="9" y="44"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="4">
<int name="x" value="100"/>
<int name="mpCon" value="5"/>
<int name="interval" value="30"/>
<int name="time" value="60"/>
<imgdir name="mob">
<canvas name="0" width="84" height="74">
<vector name="origin" x="42" y="37"/>
<int name="delay" value="120"/>
</canvas>
<canvas name="1" width="84" height="75">
<vector name="origin" x="43" y="36"/>
<int name="delay" value="120"/>
</canvas>
<canvas name="2" width="84" height="75">
<vector name="origin" x="43" y="36"/>
<int name="delay" value="120"/>
</canvas>
<canvas name="3" width="85" height="75">
<vector name="origin" x="44" y="35"/>
<int name="delay" value="120"/>
</canvas>
<canvas name="4" width="84" height="74">
<vector name="origin" x="42" y="37"/>
<int name="delay" value="120"/>
</canvas>
<canvas name="5" width="85" height="73">
<vector name="origin" x="43" y="38"/>
<int name="delay" value="120"/>
</canvas>
<int name="pos" value="2"/>
<int name="repeat" value="1"/>
</imgdir>
</imgdir>
<imgdir name="5">
<int name="x" value="100"/>
<int name="mpCon" value="5"/>
<int name="interval" value="30"/>
<int name="time" value="60"/>
<imgdir name="mob">
<canvas name="0" width="84" height="74">
<vector name="origin" x="42" y="37"/>
<int name="delay" value="120"/>
</canvas>
<canvas name="1" width="84" height="75">
<vector name="origin" x="43" y="36"/>
<int name="delay" value="120"/>
</canvas>
<canvas name="2" width="84" height="75">
<vector name="origin" x="43" y="36"/>
<int name="delay" value="120"/>
</canvas>
<canvas name="3" width="85" height="75">
<vector name="origin" x="44" y="35"/>
<int name="delay" value="120"/>
</canvas>
<canvas name="4" width="84" height="74">
<vector name="origin" x="42" y="37"/>
<int name="delay" value="120"/>
</canvas>
<canvas name="5" width="85" height="73">
<vector name="origin" x="43" y="38"/>
<int name="delay" value="120"/>
</canvas>
<int name="pos" value="2"/>
<int name="repeat" value="1"/>
</imgdir>
</imgdir>
<imgdir name="6">
<int name="x" value="100"/>
<int name="mpCon" value="1"/>
<int name="interval" value="5"/>
<int name="time" value="1"/>
</imgdir>
<imgdir name="7">
<int name="x" value="1"/>
<int name="mpCon" value="1"/>
<int name="interval" value="8"/>
<int name="time" value="1"/>
</imgdir>
<imgdir name="8">
<int name="x" value="100"/>
<int name="mpCon" value="1"/>
<int name="interval" value="90"/>
<int name="time" value="10"/>
</imgdir>
<imgdir name="9">
<int name="x" value="110"/>
<int name="time" value="6000"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="10">
<int name="x" value="120"/>
<int name="time" value="6000"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="11">
<int name="x" value="130"/>
<int name="time" value="6000"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="12">
<int name="x" value="140"/>
<int name="time" value="6000"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="13">
<int name="x" value="150"/>
<int name="time" value="6000"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="14">
<int name="x" value="160"/>
<int name="time" value="6000"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="15">
<int name="x" value="170"/>
<int name="time" value="6000"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="16">
<int name="x" value="170"/>
<int name="time" value="15"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="19" height="23">
<vector name="origin" x="9" y="44"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="17">
<int name="x" value="100"/>
<int name="mpCon" value="1"/>
<int name="interval" value="15"/>
<int name="time" value="1"/>
</imgdir>
<imgdir name="18">
<int name="x" value="100"/>
<int name="mpCon" value="1"/>
<int name="interval" value="5"/>
<int name="time" value="1"/>
</imgdir>
<imgdir name="19">
<int name="x" value="170"/>
<int name="time" value="15"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="19" height="23">
<vector name="origin" x="9" y="44"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="20">
<int name="x" value="115"/>
<int name="mpCon" value="5"/>
<int name="interval" value="40"/>
<int name="time" value="30"/>
<imgdir name="effect">
<canvas name="0" width="128" height="128">
<vector name="origin" x="62" y="65"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="1" width="128" height="127">
<vector name="origin" x="61" y="64"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="2" width="131" height="130">
<vector name="origin" x="65" y="65"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="3" width="141" height="140">
<vector name="origin" x="68" y="70"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="4" width="123" height="123">
<vector name="origin" x="58" y="62"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="5" width="126" height="122">
<vector name="origin" x="63" y="63"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="6" width="111" height="112">
<vector name="origin" x="54" y="57"/>
<int name="delay" value="100"/>
</canvas>
<canvas name="7" width="24" height="23">
<vector name="origin" x="9" y="15"/>
<int name="delay" value="100"/>
<int name="a0" value="255"/>
<int name="a1" value="0"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="49" height="54">
<vector name="origin" x="22" y="72"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="21">
<string name="info" value="넥스1/넥스2"/>
<int name="x" value="120"/>
<int name="time" value="15"/>
<int name="mpCon" value="20"/>
<int name="interval" value="30"/>
<int name="hp" value="70"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="19" height="23">
<vector name="origin" x="9" y="44"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="22">
<string name="info" value="넥스3/넥스4"/>
<int name="x" value="140"/>
<int name="time" value="15"/>
<int name="mpCon" value="20"/>
<int name="interval" value="30"/>
<int name="hp" value="70"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="19" height="23">
<vector name="origin" x="9" y="44"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
<imgdir name="23">
<string name="info" value="넥스5/넥스6"/>
<int name="x" value="160"/>
<int name="time" value="15"/>
<int name="mpCon" value="20"/>
<int name="interval" value="30"/>
<int name="hp" value="70"/>
<imgdir name="effect">
<canvas name="0" width="10" height="10">
<vector name="origin" x="-2" y="32"/>
</canvas>
<int name="pos" value="3"/>
</imgdir>
<imgdir name="mob">
<canvas name="0" width="19" height="23">
<vector name="origin" x="9" y="44"/>
</canvas>
<int name="pos" value="2"/>
<int name="icon" value="1"/>
</imgdir>
</imgdir>
</imgdir>
</imgdir>
</imgdir>