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 --> <log4j.version>2.18.0</log4j.version> <!-- Slf4j implementation -->
<graalvm.version>22.2.0</graalvm.version> <!-- ScriptEngine implementation --> <graalvm.version>22.2.0</graalvm.version> <!-- ScriptEngine implementation -->
<netty.version>4.1.79.Final</netty.version> <!-- Networking --> <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 --> <yamlbeans.version>1.15</yamlbeans.version> <!-- Config file -->
<jcip-annotations.version>1.0</jcip-annotations.version> <!-- Annotations for concurrency documentation --> <jcip-annotations.version>1.0</jcip-annotations.version> <!-- Annotations for concurrency documentation -->
<HikariCP.version>5.0.1</HikariCP.version> <!-- Database connection pool --> <HikariCP.version>5.0.1</HikariCP.version> <!-- Database connection pool -->
<mysql-connector-java.version>8.0.30</mysql-connector-java.version> <!-- MySQL JDBC driver --> <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> </properties>
<dependencies> <dependencies>
@@ -120,16 +121,25 @@
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId> <artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version> <version>${junit.version}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId> <artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version> <version>${junit.version}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId> <artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version> <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> </dependency>
</dependencies> </dependencies>
+5 -9
View File
@@ -64,10 +64,7 @@ import server.events.Events;
import server.events.RescueGaga; import server.events.RescueGaga;
import server.events.gm.Fitness; import server.events.gm.Fitness;
import server.events.gm.Ola; import server.events.gm.Ola;
import server.life.MobSkill; import server.life.*;
import server.life.MobSkillFactory;
import server.life.Monster;
import server.life.PlayerNPC;
import server.maps.*; import server.maps.*;
import server.maps.MiniGame.MiniGameResult; import server.maps.MiniGame.MiniGameResult;
import server.minigame.RockPaperScissor; import server.minigame.RockPaperScissor;
@@ -7329,13 +7326,11 @@ public class Character extends AbstractCharacterObject {
final int skilllv = rs.getInt("mobskilllv"); final int skilllv = rs.getInt("mobskilllv");
final long length = rs.getInt("length"); final long length = rs.getInt("length");
MobSkill ms = MobSkillFactory.getMobSkill(skillid, skilllv); MobSkill ms = MobSkillFactory.getMobSkill(MobSkillType.from(skillid), skilllv);
if (ms != null) {
loadedDiseases.put(disease, new Pair<>(length, ms)); loadedDiseases.put(disease, new Pair<>(length, ms));
} }
} }
} }
}
// Debuffs (delete) // Debuffs (delete)
try (PreparedStatement ps = con.prepareStatement("DELETE FROM playerdiseases WHERE charid = ?")) { try (PreparedStatement ps = con.prepareStatement("DELETE FROM playerdiseases WHERE charid = ?")) {
@@ -8111,8 +8106,9 @@ public class Character extends AbstractCharacterObject {
ps.setInt(2, e.getKey().ordinal()); ps.setInt(2, e.getKey().ordinal());
MobSkill ms = e.getValue().getRight(); MobSkill ms = e.getValue().getRight();
ps.setInt(3, ms.getSkillId()); MobSkillId msId = ms.getId();
ps.setInt(4, ms.getSkillLevel()); ps.setInt(3, msId.type().getId());
ps.setInt(4, msId.level());
ps.setInt(5, e.getValue().getLeft().intValue()); ps.setInt(5, e.getValue().getLeft().intValue());
ps.addBatch(); ps.addBatch();
} }
+26 -28
View File
@@ -22,37 +22,34 @@
package client; package client;
import constants.game.GameConstants; import constants.game.GameConstants;
import server.life.MobSkillType;
import java.util.Arrays;
public enum Disease { public enum Disease {
NULL(0x0), NULL(0x0),
SLOW(0x1, 126), SLOW(0x1, MobSkillType.SLOW),
SEDUCE(0x80, 128), SEDUCE(0x80, MobSkillType.SEDUCE),
FISHABLE(0x100), FISHABLE(0x100),
ZOMBIFY(0x4000), ZOMBIFY(0x4000),
CONFUSE(0x80000, 132), CONFUSE(0x80000, MobSkillType.REVERSE_INPUT),
STUN(0x2000000000000L, 123), STUN(0x2000000000000L, MobSkillType.STUN),
POISON(0x4000000000000L, 125), POISON(0x4000000000000L, MobSkillType.POISON),
SEAL(0x8000000000000L, 120), SEAL(0x8000000000000L, MobSkillType.SEAL),
DARKNESS(0x10000000000000L, 121), DARKNESS(0x10000000000000L, MobSkillType.DARKNESS),
WEAKEN(0x4000000000000000L, 122), WEAKEN(0x4000000000000000L, MobSkillType.WEAKNESS),
CURSE(0x8000000000000000L, 124); CURSE(0x8000000000000000L, MobSkillType.CURSE);
private final long i; private final long i;
private final boolean first; private final MobSkillType mobSkillType;
private final int mobskill;
Disease(long i) { Disease(long i) {
this(i, false, 0); this(i, null);
} }
Disease(long i, int skill) { Disease(long i, MobSkillType skill) {
this(i, false, skill);
}
Disease(long i, boolean first, int skill) {
this.i = i; this.i = i;
this.first = first; this.mobSkillType = skill;
this.mobskill = skill;
} }
public long getValue() { public long getValue() {
@@ -60,11 +57,11 @@ public enum Disease {
} }
public boolean isFirst() { public boolean isFirst() {
return first; return false;
} }
public int getDisease() { public MobSkillType getMobSkillType() {
return mobskill; return mobSkillType;
} }
public static Disease ordinal(int ord) { public static Disease ordinal(int ord) {
@@ -80,13 +77,14 @@ public enum Disease {
return diseases[(int) (Math.random() * diseases.length)]; return diseases[(int) (Math.random() * diseases.length)];
} }
public static final Disease getBySkill(final int skill) { public static final Disease getBySkill(MobSkillType skill) {
for (Disease d : Disease.values()) { if (skill == null) {
if (d.getDisease() == skill && d.getDisease() != 0) {
return d;
}
}
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 client.command.Command;
import server.life.MobSkill; import server.life.MobSkill;
import server.life.MobSkillFactory; import server.life.MobSkillFactory;
import server.life.MobSkillType;
import java.util.Collections; import java.util.Collections;
@@ -21,7 +22,8 @@ public class MobSkillCommand extends Command {
String skillId = params[0]; String skillId = params[0];
String skillLevel = params[1]; 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) { if (mobSkill == null) {
throw new IllegalArgumentException("Mob skill not found. Id: %s, level: %s".formatted(skillId, skillLevel)); 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 client.command.Command;
import server.life.MobSkill; import server.life.MobSkill;
import server.life.MobSkillFactory; import server.life.MobSkillFactory;
import server.life.MobSkillType;
import server.maps.MapObject; import server.maps.MapObject;
import server.maps.MapObjectType; import server.maps.MapObjectType;
@@ -51,55 +52,46 @@ public class DebuffCommand extends Command {
MobSkill skill = null; MobSkill skill = null;
switch (params[0].toUpperCase()) { switch (params[0].toUpperCase()) {
case "SLOW": case "SLOW" -> {
disease = Disease.SLOW; disease = Disease.SLOW;
skill = MobSkillFactory.getMobSkill(126, 7); skill = MobSkillFactory.getMobSkill(MobSkillType.SLOW, 7);
break; }
case "SEDUCE" -> {
case "SEDUCE":
disease = Disease.SEDUCE; disease = Disease.SEDUCE;
skill = MobSkillFactory.getMobSkill(128, 7); skill = MobSkillFactory.getMobSkill(MobSkillType.SEDUCE, 7);
break; }
case "ZOMBIFY" -> {
case "ZOMBIFY":
disease = Disease.ZOMBIFY; disease = Disease.ZOMBIFY;
skill = MobSkillFactory.getMobSkill(133, 1); skill = MobSkillFactory.getMobSkill(MobSkillType.UNDEAD, 1);
break; }
case "CONFUSE" -> {
case "CONFUSE":
disease = Disease.CONFUSE; disease = Disease.CONFUSE;
skill = MobSkillFactory.getMobSkill(132, 2); skill = MobSkillFactory.getMobSkill(MobSkillType.REVERSE_INPUT, 2);
break; }
case "STUN" -> {
case "STUN":
disease = Disease.STUN; disease = Disease.STUN;
skill = MobSkillFactory.getMobSkill(123, 7); skill = MobSkillFactory.getMobSkill(MobSkillType.STUN, 7);
break; }
case "POISON" -> {
case "POISON":
disease = Disease.POISON; disease = Disease.POISON;
skill = MobSkillFactory.getMobSkill(125, 5); skill = MobSkillFactory.getMobSkill(MobSkillType.POISON, 5);
break; }
case "SEAL" -> {
case "SEAL":
disease = Disease.SEAL; disease = Disease.SEAL;
skill = MobSkillFactory.getMobSkill(120, 1); skill = MobSkillFactory.getMobSkill(MobSkillType.SEAL, 1);
break; }
case "DARKNESS" -> {
case "DARKNESS":
disease = Disease.DARKNESS; disease = Disease.DARKNESS;
skill = MobSkillFactory.getMobSkill(121, 1); skill = MobSkillFactory.getMobSkill(MobSkillType.DARKNESS, 1);
break; }
case "WEAKEN" -> {
case "WEAKEN":
disease = Disease.WEAKEN; disease = Disease.WEAKEN;
skill = MobSkillFactory.getMobSkill(122, 1); skill = MobSkillFactory.getMobSkill(MobSkillType.WEAKNESS, 1);
break; }
case "CURSE" -> {
case "CURSE":
disease = Disease.CURSE; disease = Disease.CURSE;
skill = MobSkillFactory.getMobSkill(124, 1); skill = MobSkillFactory.getMobSkill(MobSkillType.CURSE, 1);
break; }
} }
if (disease == null) { if (disease == null) {
@@ -44,7 +44,6 @@ import server.maps.MapObject;
import server.maps.MapObjectType; import server.maps.MapObjectType;
import server.maps.MapleMap; import server.maps.MapleMap;
import tools.PacketCreator; import tools.PacketCreator;
import tools.Pair;
import tools.Randomizer; import tools.Randomizer;
import java.awt.*; import java.awt.*;
@@ -491,22 +490,18 @@ public abstract class AbstractDealDamageHandler extends AbstractPacketHandler {
map.damageMonster(player, monster, totDamageToOneMonster); map.damageMonster(player, monster, totDamageToOneMonster);
} }
if (monster.isBuffed(MonsterStatus.WEAPON_REFLECT) && !attack.magic) { if (monster.isBuffed(MonsterStatus.WEAPON_REFLECT) && !attack.magic) {
List<Pair<Integer, Integer>> mobSkills = monster.getSkills(); for (MobSkillId msId : monster.getSkills()) {
if (msId.type() == MobSkillType.PHYSICAL_AND_MAGIC_COUNTER) {
for (Pair<Integer, Integer> ms : mobSkills) { MobSkill toUse = MobSkillFactory.getMobSkill(MobSkillType.PHYSICAL_AND_MAGIC_COUNTER, msId.level());
if (ms.left == 145) {
MobSkill toUse = MobSkillFactory.getMobSkill(ms.left, ms.right);
player.addHP(-toUse.getX()); 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); 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) { if (monster.isBuffed(MonsterStatus.MAGIC_REFLECT) && attack.magic) {
List<Pair<Integer, Integer>> mobSkills = monster.getSkills(); for (MobSkillId msId : monster.getSkills()) {
if (msId.type() == MobSkillType.PHYSICAL_AND_MAGIC_COUNTER) {
for (Pair<Integer, Integer> ms : mobSkills) { MobSkill toUse = MobSkillFactory.getMobSkill(MobSkillType.PHYSICAL_AND_MAGIC_COUNTER, msId.level());
if (ms.left == 145) {
MobSkill toUse = MobSkillFactory.getMobSkill(ms.left, ms.right);
player.addHP(-toUse.getY()); 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); 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.Party;
import net.server.world.PartyCharacter; import net.server.world.PartyCharacter;
import server.life.LifeFactory; import server.life.LifeFactory;
import server.life.MobSkillType;
import server.life.Monster; import server.life.Monster;
import server.partyquest.CarnivalFactory; import server.partyquest.CarnivalFactory;
import server.partyquest.CarnivalFactory.MCSkill; import server.partyquest.CarnivalFactory.MCSkill;
@@ -94,18 +95,15 @@ public final class MonsterCarnivalHandler extends AbstractPacketHandler {
return; return;
} }
final MCSkill skill = CarnivalFactory.getInstance().getSkill(skillid.get(num)); //ugh wtf 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.CPQMessage((byte) 1));
c.sendPacket(PacketCreator.enableActions()); c.sendPacket(PacketCreator.enableActions());
return; return;
} }
final Disease dis = skill.getDisease(); final Disease dis = skill.getDisease();
Party enemies = c.getPlayer().getParty().getEnemy(); Party enemies = c.getPlayer().getParty().getEnemy();
if (skill.targetsAll) { if (skill.targetsAll()) {
int hitChance = 0; int hitChance = rollHitChance(dis.getMobSkillType());
if (dis.getDisease() == 121 || dis.getDisease() == 122 || dis.getDisease() == 125 || dis.getDisease() == 126) {
hitChance = (int) (Math.random() * 100);
}
if (hitChance <= 80) { if (hitChance <= 80) {
for (PartyCharacter mpc : enemies.getPartyMembers()) { for (PartyCharacter mpc : enemies.getPartyMembers()) {
Character mc = mpc.getPlayer(); Character mc = mpc.getPlayer();
@@ -130,11 +128,11 @@ public final class MonsterCarnivalHandler extends AbstractPacketHandler {
} }
} }
} }
neededCP = skill.cpLoss; neededCP = skill.cpLoss();
c.sendPacket(PacketCreator.enableActions()); c.sendPacket(PacketCreator.enableActions());
} else if (tab == 2) { //protectors } else if (tab == 2) { //protectors
final MCSkill skill = CarnivalFactory.getInstance().getGuardian(num); 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.CPQMessage((byte) 1));
c.sendPacket(PacketCreator.enableActions()); c.sendPacket(PacketCreator.enableActions());
return; return;
@@ -165,7 +163,7 @@ public final class MonsterCarnivalHandler extends AbstractPacketHandler {
c.sendPacket(PacketCreator.enableActions()); c.sendPacket(PacketCreator.enableActions());
return; return;
} else { } 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 net.packet.InPacket;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import server.life.MobSkill; import server.life.*;
import server.life.MobSkillFactory;
import server.life.Monster;
import server.life.MonsterInformationProvider;
import server.maps.MapObject; import server.maps.MapObject;
import server.maps.MapObjectType; import server.maps.MapObjectType;
import server.maps.MapleMap; import server.maps.MapleMap;
import tools.PacketCreator; import tools.PacketCreator;
import tools.Pair;
import tools.Randomizer;
import tools.exceptions.EmptyMovementException; import tools.exceptions.EmptyMovementException;
import java.awt.*; import java.awt.*;
@@ -84,7 +79,6 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler {
boolean isAttack = inRangeInclusive(rawActivity, 24, 41); boolean isAttack = inRangeInclusive(rawActivity, 24, 41);
boolean isSkill = inRangeInclusive(rawActivity, 42, 59); boolean isSkill = inRangeInclusive(rawActivity, 42, 59);
MobSkill toUse = null;
int useSkillId = 0, useSkillLevel = 0; int useSkillId = 0, useSkillLevel = 0;
MobSkill nextUse = null; MobSkill nextUse = null;
@@ -92,18 +86,16 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler {
boolean nextMovementCouldBeSkill = !(isSkill || (pNibbles != 0)); boolean nextMovementCouldBeSkill = !(isSkill || (pNibbles != 0));
int castPos;
if (isSkill) { if (isSkill) {
useSkillId = skillId; useSkillId = skillId;
useSkillLevel = skillLv; useSkillLevel = skillLv;
castPos = monster.getSkillPos(useSkillId, useSkillLevel); if (monster.hasSkill(useSkillId, useSkillLevel)) {
if (castPos != -1) { MobSkill toUse = MobSkillFactory.getMobSkill(MobSkillType.from(useSkillId), useSkillLevel);
toUse = MobSkillFactory.getMobSkill(useSkillId, useSkillLevel);
if (monster.canUseSkill(toUse, true)) { if (monster.canUseSkill(toUse, true)) {
int animationTime = MonsterInformationProvider.getInstance().getMobSkillAnimationTime(toUse); 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); toUse.applyDelayedEffect(player, monster, true, animationTime);
} else { } else {
banishPlayers = new LinkedList<>(); banishPlayers = new LinkedList<>();
@@ -112,8 +104,7 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler {
} }
} }
} else { } else {
castPos = (rawActivity - 24) / 2; int castPos = (rawActivity - 24) / 2;
int atkStatus = monster.canUseAttack(castPos, isSkill); int atkStatus = monster.canUseAttack(castPos, isSkill);
if (atkStatus < 1) { if (atkStatus < 1) {
rawActivity = -1; rawActivity = -1;
@@ -122,15 +113,11 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler {
} }
int mobMp = monster.getMp(); int mobMp = monster.getMp();
if (nextMovementCouldBeSkill) { if (nextMovementCouldBeSkill && monster.hasAnySkill()) {
int noSkills = monster.getNoSkills(); MobSkillId skillToUse = monster.getRandomSkill();
if (noSkills > 0) { nextSkillId = skillToUse.type().getId();
int rndSkill = Randomizer.nextInt(noSkills); 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())) { 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 // thanks OishiiKawaiiDesu for noticing mobs trying to cast skills they are not supposed to be able
@@ -140,7 +127,6 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler {
nextUse = null; nextUse = null;
} }
} }
}
p.readByte(); p.readByte();
p.readInt(); // whatever p.readInt(); // whatever
@@ -168,8 +154,8 @@ public final class MoveLifeHandler extends AbstractMovementPacketHandler {
p.seek(movementDataStart); p.seek(movementDataStart);
if (YamlConfig.config.server.USE_DEBUG_SHOW_RCVD_MVLIFE) { if (YamlConfig.config.server.USE_DEBUG_SHOW_RCVD_MVLIFE) {
log.debug("{} castPos: {}, rawAct: {}, opt: {}, skillId: {}, skillLv: {}, allowSkill: {}, mobMp: {}", log.debug("{} rawAct: {}, opt: {}, skillId: {}, skillLv: {}, allowSkill: {}, mobMp: {}",
isSkill ? "SKILL" : (isAttack ? "ATTCK" : ""), castPos, rawActivity, pOption, useSkillId, isSkill ? "SKILL" : (isAttack ? "ATTCK" : ""), rawActivity, pOption, useSkillId,
useSkillLevel, nextMovementCouldBeSkill, mobMp); useSkillLevel, nextMovementCouldBeSkill, mobMp);
} }
@@ -151,7 +151,7 @@ public final class TakeDamageHandler extends AbstractPacketHandler {
is_deadly = true; is_deadly = true;
} }
mpattack += attackInfo.getMpBurn(); 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) { if (mobSkill != null && damage > 0) {
mobSkill.applyEffect(chr, attacker, false, banishPlayers); mobSkill.applyEffect(chr, attacker, false, banishPlayers);
} }
@@ -965,10 +965,6 @@ public class AbstractPlayerInteraction {
return LifeFactory.getMonster(mid); return LifeFactory.getMonster(mid);
} }
public MobSkill getMobSkill(int skill, int level) {
return MobSkillFactory.getMobSkill(skill, level);
}
public void spawnGuide() { public void spawnGuide() {
c.sendPacket(PacketCreator.spawnGuide(true)); c.sendPacket(PacketCreator.spawnGuide(true));
} }
@@ -1199,12 +1195,12 @@ public class AbstractPlayerInteraction {
} }
private void applySealSkill(Monster monster) { private void applySealSkill(Monster monster) {
MobSkill sealSkill = MobSkillFactory.getMobSkill(157, 1); MobSkill sealSkill = MobSkillFactory.getMobSkill(MobSkillType.SEAL_SKILL, 1);
sealSkill.applyEffect(monster); sealSkill.applyEffect(monster);
} }
private void applyReduceAvoid(Monster monster) { private void applyReduceAvoid(Monster monster) {
MobSkill reduceAvoidSkill = MobSkillFactory.getMobSkill(155, 2); MobSkill reduceAvoidSkill = MobSkillFactory.getMobSkill(MobSkillType.EVA, 2);
reduceAvoidSkill.applyEffect(monster); reduceAvoidSkill.applyEffect(monster);
} }
+10 -7
View File
@@ -42,11 +42,11 @@ import provider.Data;
import provider.DataTool; import provider.DataTool;
import server.life.MobSkill; import server.life.MobSkill;
import server.life.MobSkillFactory; import server.life.MobSkillFactory;
import server.life.MobSkillType;
import server.life.Monster; import server.life.Monster;
import server.maps.*; import server.maps.*;
import server.partyquest.CarnivalFactory; import server.partyquest.CarnivalFactory;
import server.partyquest.CarnivalFactory.MCSkill; import server.partyquest.CarnivalFactory.MCSkill;
import tools.ArrayMap;
import tools.PacketCreator; import tools.PacketCreator;
import tools.Pair; import tools.Pair;
@@ -422,7 +422,7 @@ public class StatEffect {
ret.itemCon = DataTool.getInt("itemCon", source, 0); ret.itemCon = DataTool.getInt("itemCon", source, 0);
ret.itemConNo = DataTool.getInt("itemConNo", source, 0); ret.itemConNo = DataTool.getInt("itemConNo", source, 0);
ret.moveTo = DataTool.getInt("moveTo", source, -1); ret.moveTo = DataTool.getInt("moveTo", source, -1);
Map<MonsterStatus, Integer> monsterStatus = new ArrayMap<>(); Map<MonsterStatus, Integer> monsterStatus = new EnumMap<>(MonsterStatus.class);
if (skill) { if (skill) {
switch (sourceid) { switch (sourceid) {
// BEGINNER // BEGINNER
@@ -1039,14 +1039,15 @@ public class StatEffect {
if (skill != null) { if (skill != null) {
final Disease dis = skill.getDisease(); final Disease dis = skill.getDisease();
Party opposition = applyfrom.getParty().getEnemy(); Party opposition = applyfrom.getParty().getEnemy();
if (skill.targetsAll) { if (skill.targetsAll()) {
for (PartyCharacter enemyChrs : opposition.getPartyMembers()) { for (PartyCharacter enemyChrs : opposition.getPartyMembers()) {
Character chrApp = enemyChrs.getPlayer(); Character chrApp = enemyChrs.getPlayer();
if (chrApp != null && chrApp.getMap().isCPQMap()) { if (chrApp != null && chrApp.getMap().isCPQMap()) {
if (dis == null) { if (dis == null) {
chrApp.dispel(); chrApp.dispel();
} else { } 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) { if (dis == null) {
chrApp.dispel(); chrApp.dispel();
} else { } 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); applyfrom.dispelDebuff(debuff);
} }
} else if (mobSkill > 0 && mobSkillLevel > 0) { } else if (mobSkill > 0 && mobSkillLevel > 0) {
MobSkill ms = MobSkillFactory.getMobSkill(mobSkill, mobSkillLevel); var mobSkillType = MobSkillType.from(mobSkill);
Disease dis = Disease.getBySkill(mobSkill); MobSkill ms = MobSkillFactory.getMobSkill(mobSkillType, mobSkillLevel);
Disease dis = Disease.getBySkill(mobSkillType);
if (target > 0) { if (target > 0) {
for (Character chr : applyto.getMap().getAllPlayers()) { for (Character chr : applyto.getMap().getAllPlayers()) {
+4 -3
View File
@@ -188,11 +188,12 @@ public class LifeFactory {
Data monsterSkillInfoData = monsterInfoData.getChildByPath("skill"); Data monsterSkillInfoData = monsterInfoData.getChildByPath("skill");
if (monsterSkillInfoData != null) { if (monsterSkillInfoData != null) {
int i = 0; int i = 0;
List<Pair<Integer, Integer>> skills = new ArrayList<>(); Set<MobSkillId> skills = new HashSet<>();
while (monsterSkillInfoData.getChildByPath(Integer.toString(i)) != null) { while (monsterSkillInfoData.getChildByPath(Integer.toString(i)) != null) {
int skillId = DataTool.getInt(i + "/skill", monsterSkillInfoData, 0); int skillId = DataTool.getInt(i + "/skill", monsterSkillInfoData, 0);
int skillLv = DataTool.getInt(i + "/level", 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)); Data monsterSkillData = monsterData.getChildByPath("skill" + (i + 1));
if (monsterSkillData != null) { if (monsterSkillData != null) {
@@ -201,7 +202,7 @@ public class LifeFactory {
animationTime += DataTool.getIntConvert("delay", effectEntry, 0); animationTime += DataTool.getIntConvert("delay", effectEntry, 0);
} }
MobSkill skill = MobSkillFactory.getMobSkill(skillId, skillLv); MobSkill skill = MobSkillFactory.getMobSkill(type, skillLv);
mi.setMobSkillAnimationTime(skill, animationTime); mi.setMobSkillAnimationTime(skill, animationTime);
} }
+205 -169
View File
@@ -26,6 +26,7 @@ import client.Disease;
import client.status.MonsterStatus; import client.status.MonsterStatus;
import constants.id.MapId; import constants.id.MapId;
import constants.id.MobId; import constants.id.MobId;
import constants.skills.Bishop;
import net.server.services.task.channel.OverallService; import net.server.services.task.channel.OverallService;
import net.server.services.type.ChannelServices; import net.server.services.type.ChannelServices;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -34,7 +35,6 @@ import server.maps.MapObject;
import server.maps.MapObjectType; import server.maps.MapObjectType;
import server.maps.MapleMap; import server.maps.MapleMap;
import server.maps.Mist; import server.maps.Mist;
import tools.ArrayMap;
import tools.Randomizer; import tools.Randomizer;
import java.awt.*; import java.awt.*;
@@ -47,68 +47,130 @@ import java.util.*;
public class MobSkill { public class MobSkill {
private static final Logger log = LoggerFactory.getLogger(MobSkill.class); private static final Logger log = LoggerFactory.getLogger(MobSkill.class);
private final int skillId; private final MobSkillId id;
private final int skillLevel; private final int mpCon;
private int mpCon; private final int spawnEffect;
private final List<Integer> toSummon = new ArrayList<>(); private final int hp;
private int spawnEffect, hp, x, y, count; private final int x;
private long duration, cooltime; private final int y;
private float prop; private final int count;
private Point lt, rb; private final long duration;
private int limit; 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) { private MobSkill(MobSkillType type, int level, int mpCon, int spawnEffect, int hp, int x, int y, int count,
this.skillId = skillId; long duration, long cooltime, float prop, Point lt, Point rb, int limit, List<Integer> toSummon) {
this.skillLevel = level; this.id = new MobSkillId(type, level);
}
public void setMpCon(int mpCon) {
this.mpCon = mpCon; this.mpCon = mpCon;
}
public void addSummons(List<Integer> toSummon) {
this.toSummon.addAll(toSummon);
}
public void setSpawnEffect(int spawnEffect) {
this.spawnEffect = spawnEffect; this.spawnEffect = spawnEffect;
}
public void setHp(int hp) {
this.hp = hp; this.hp = hp;
}
public void setX(int x) {
this.x = x; this.x = x;
}
public void setY(int y) {
this.y = y; this.y = y;
}
public void setCount(int count) {
this.count = count; this.count = count;
}
public void setDuration(long duration) {
this.duration = duration; this.duration = duration;
}
public void setCoolTime(long cooltime) {
this.cooltime = cooltime; this.cooltime = cooltime;
}
public void setProp(float prop) {
this.prop = prop; this.prop = prop;
}
public void setLtRb(Point lt, Point rb) {
this.lt = lt; this.lt = lt;
this.rb = rb; this.rb = rb;
this.limit = limit;
this.toSummon = toSummon;
} }
public void setLimit(int 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; 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) { public void applyDelayedEffect(final Character player, final Monster monster, final boolean skill, int animationTime) {
@@ -126,32 +188,72 @@ public class MobSkill {
applyEffect(null, monster, false, Collections.emptyList()); 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; Disease disease = null;
Map<MonsterStatus, Integer> stats = new ArrayMap<>(); Map<MonsterStatus, Integer> stats = new EnumMap<>(MonsterStatus.class);
List<Integer> reflection = new LinkedList<>(); List<Integer> reflection = new ArrayList<>();
switch (skillId) { switch (id.type()) {
case 100: case ATTACK_UP, ATTACK_UP_M, PAD -> stats.put(MonsterStatus.WEAPON_ATTACK_UP, x);
case 110: case MAGIC_ATTACK_UP, MAGIC_ATTACK_UP_M, MAD -> stats.put(MonsterStatus.MAGIC_ATTACK_UP, x);
case 150: case DEFENSE_UP, DEFENSE_UP_M, PDR -> stats.put(MonsterStatus.WEAPON_DEFENSE_UP, x);
stats.put(MonsterStatus.WEAPON_ATTACK_UP, x); case MAGIC_DEFENSE_UP, MAGIC_DEFENSE_UP_M, MDR -> stats.put(MonsterStatus.MAGIC_DEFENSE_UP, x);
break; case HEAL_M -> applyHealEffect(skill, monster);
case 101: case SEAL -> disease = Disease.SEAL;
case 111: case DARKNESS -> disease = Disease.DARKNESS;
case 151: case WEAKNESS -> disease = Disease.WEAKEN;
stats.put(MonsterStatus.MAGIC_ATTACK_UP, x); case STUN -> disease = Disease.STUN;
break; case CURSE -> disease = Disease.CURSE;
case 102: case POISON -> disease = Disease.POISON;
case 112: case SLOW -> disease = Disease.SLOW;
case 152: case DISPEL -> applyDispelEffect(skill, monster, player);
stats.put(MonsterStatus.WEAPON_DEFENSE_UP, x); case SEDUCE -> disease = Disease.SEDUCE;
break; case BANISH -> applyBanishEffect(skill, monster, player, banishPlayersOutput);
case 103: case AREA_POISON -> spawnMonsterMist(monster);
case 113: case REVERSE_INPUT -> disease = Disease.CONFUSE;
case 153: case UNDEAD -> disease = Disease.ZOMBIFY;
stats.put(MonsterStatus.MAGIC_DEFENSE_UP, x); case PHYSICAL_IMMUNE -> {
break; if (makeChanceResult() && !monster.isBuffed(MonsterStatus.MAGIC_IMMUNITY)) {
case 114: stats.put(MonsterStatus.WEAPON_IMMUNITY, x);
}
}
case MAGIC_IMMUNE -> {
if (makeChanceResult() && !monster.isBuffed(MonsterStatus.WEAPON_IMMUNITY)) {
stats.put(MonsterStatus.MAGIC_IMMUNITY, x);
}
}
case PHYSICAL_COUNTER -> {
stats.put(MonsterStatus.WEAPON_REFLECT, 10);
stats.put(MonsterStatus.WEAPON_IMMUNITY, 10);
reflection.add(x);
}
case MAGIC_COUNTER -> {
stats.put(MonsterStatus.MAGIC_REFLECT, 10);
stats.put(MonsterStatus.MAGIC_IMMUNITY, 10);
reflection.add(x);
}
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);
}
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) {
applyMonsterBuffs(stats, skill, monster, reflection);
}
if (disease != null) {
applyDisease(disease, skill, monster, player);
}
}
private void applyHealEffect(boolean skill, Monster monster) {
if (lt != null && rb != null && skill) { if (lt != null && rb != null && skill) {
List<MapObject> objects = getObjectsInRange(monster, MapObjectType.MONSTER); List<MapObject> objects = getObjectsInRange(monster, MapObjectType.MONSTER);
final int hps = (getX() / 1000) * (int) (950 + 1050 * Math.random()); final int hps = (getX() / 1000) * (int) (950 + 1050 * Math.random());
@@ -161,96 +263,33 @@ public class MobSkill {
} else { } else {
monster.heal(getX(), getY()); 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();
} }
private void applyDispelEffect(boolean skill, Monster monster, Character player) {
if (lt != null && rb != null && skill) {
getPlayersInRange(monster).forEach(Character::dispel);
} else { } else {
player.dispel(); player.dispel();
} }
break; }
case 128: // Seduce
disease = Disease.SEDUCE; private void applyBanishEffect(boolean skill, Monster monster, Character player,
break; List<Character> banishPlayersOutput) {
case 129: // Banish
if (lt != null && rb != null && skill) { if (lt != null && rb != null && skill) {
banishPlayers.addAll(getPlayersInRange(monster)); banishPlayersOutput.addAll(getPlayersInRange(monster));
} else { } else {
banishPlayers.add(player); banishPlayersOutput.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:
if (makeChanceResult() && !monster.isBuffed(MonsterStatus.MAGIC_IMMUNITY)) {
stats.put(MonsterStatus.WEAPON_IMMUNITY, x);
} }
break;
case 141: private void spawnMonsterMist(Monster monster) {
if (makeChanceResult() && !monster.isBuffed(MonsterStatus.WEAPON_IMMUNITY)) { Rectangle mistArea = calculateBoundingBox(monster.getPosition());
stats.put(MonsterStatus.MAGIC_IMMUNITY, x); var mist = new Mist(mistArea, monster, this);
int mistDuration = x * 100;
monster.getMap().spawnMist(mist, mistDuration, false, false, false);
} }
break;
case 143: // Weapon Reflect private void summonMonsters(Monster monster) {
stats.put(MonsterStatus.WEAPON_REFLECT, 10);
stats.put(MonsterStatus.WEAPON_IMMUNITY, 10);
reflection.add(x);
break;
case 144: // Magic Reflect
stats.put(MonsterStatus.MAGIC_REFLECT, 10);
stats.put(MonsterStatus.MAGIC_IMMUNITY, 10);
reflection.add(x);
break;
case 145: // Weapon / Magic reflect
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(); int skillLimit = this.getLimit();
MapleMap map = monster.getMap(); MapleMap map = monster.getMap();
@@ -322,25 +361,23 @@ public class MobSkill {
} }
} }
} }
break;
default:
log.warn("Unhandled Mob skill: {}", skillId);
break;
} }
if (stats.size() > 0) {
private void applyMonsterBuffs(Map<MonsterStatus, Integer> stats, boolean skill, Monster monster, List<Integer> reflection) {
if (lt != null && rb != null && skill) { if (lt != null && rb != null && skill) {
for (MapObject mons : getObjectsInRange(monster, MapObjectType.MONSTER)) { for (MapObject mons : getObjectsInRange(monster, MapObjectType.MONSTER)) {
((Monster) mons).applyMonsterBuff(stats, getX(), getSkillId(), getDuration(), this, reflection); ((Monster) mons).applyMonsterBuff(stats, getX(), getDuration(), this, reflection);
} }
} else { } else {
monster.applyMonsterBuff(stats, getX(), getSkillId(), getDuration(), this, reflection); monster.applyMonsterBuff(stats, getX(), getDuration(), this, reflection);
} }
} }
if (disease != null) {
private void applyDisease(Disease disease, boolean skill, Monster monster, Character player) {
if (lt != null && rb != null && skill) { if (lt != null && rb != null && skill) {
int i = 0; int i = 0;
for (Character character : getPlayersInRange(monster)) { for (Character character : getPlayersInRange(monster)) {
if (!character.hasActiveBuff(2321005)) { // holy shield if (!character.hasActiveBuff(Bishop.HOLY_SHIELD)) {
if (disease.equals(Disease.SEDUCE)) { if (disease.equals(Disease.SEDUCE)) {
if (i < count) { if (i < count) {
character.giveDebuff(Disease.SEDUCE, this); character.giveDebuff(Disease.SEDUCE, this);
@@ -355,18 +392,17 @@ public class MobSkill {
player.giveDebuff(disease, this); player.giveDebuff(disease, this);
} }
} }
}
private List<Character> getPlayersInRange(Monster monster) { private List<Character> getPlayersInRange(Monster monster) {
return monster.getMap().getPlayersInRange(calculateBoundingBox(monster.getPosition())); return monster.getMap().getPlayersInRange(calculateBoundingBox(monster.getPosition()));
} }
public int getSkillId() { public MobSkillId getId() {
return skillId; return id;
} }
public int getSkillLevel() { public MobSkillType getType() {
return skillLevel; return id.type();
} }
public int getMpCon() { public int getMpCon() {
@@ -374,7 +410,7 @@ public class MobSkill {
} }
public List<Integer> getSummons() { public List<Integer> getSummons() {
return new ArrayList<>(toSummon); return toSummon;
} }
public int getSpawnEffect() { public int getSpawnEffect() {
+55 -40
View File
@@ -28,10 +28,8 @@ import provider.DataTool;
import provider.wz.WZFiles; import provider.wz.WZFiles;
import java.awt.*; import java.awt.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.*;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -42,33 +40,43 @@ import static java.util.concurrent.TimeUnit.SECONDS;
* @author Danny (Leifde) * @author Danny (Leifde)
*/ */
public class MobSkillFactory { public class MobSkillFactory {
private static final Map<String, MobSkill> mobSkills = new HashMap<>(); 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 static final Data skillRoot = dataSource.getData("MobSkill.img");
private final static ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private static final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final static Lock readLock = readWriteLock.readLock(); private static final Lock readLock = readWriteLock.readLock();
private final static Lock writeLock = readWriteLock.writeLock(); private static final Lock writeLock = readWriteLock.writeLock();
public static MobSkill getMobSkill(final int skillId, final int level) { public static MobSkill getMobSkill(final MobSkillType type, final int level) {
final String key = skillId + "" + level;
readLock.lock(); readLock.lock();
try { try {
MobSkill ret = mobSkills.get(key); MobSkill ms = mobSkills.get(createKey(type, level));
if (ret != null) { if (ms != null) {
return ret; return ms;
} }
} finally { } finally {
readLock.unlock(); 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(); writeLock.lock();
try { try {
MobSkill ret; MobSkill existingMs = mobSkills.get(createKey(type, level));
ret = mobSkills.get(key); if (existingMs != null) {
if (ret == null) { return Optional.of(existingMs);
Data skillData = skillRoot.getChildByPath(skillId + "/level/" + level); }
if (skillData != null) {
int mpCon = DataTool.getInt(skillData.getChildByPath("mpCon"), 0); 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<>(); List<Integer> toSummon = new ArrayList<>();
for (int i = 0; i > -1; i++) { for (int i = 0; i > -1; i++) {
if (skillData.getChildByPath(String.valueOf(i)) == null) { if (skillData.getChildByPath(String.valueOf(i)) == null) {
@@ -86,32 +94,39 @@ public class MobSkillFactory {
int iprop = DataTool.getInt("prop", skillData, 100); int iprop = DataTool.getInt("prop", skillData, 100);
float prop = iprop / 100; float prop = iprop / 100;
int limit = DataTool.getInt("limit", skillData, 0); int limit = DataTool.getInt("limit", skillData, 0);
Data ltd = skillData.getChildByPath("lt");
Data ltData = skillData.getChildByPath("lt");
Data rbData = skillData.getChildByPath("rb");
Point lt = null; Point lt = null;
Point rb = null; Point rb = null;
if (ltd != null) { if (ltData != null && rbData != null) {
lt = (Point) ltd.getData(); lt = (Point) ltData.getData();
rb = (Point) skillData.getChildByPath("rb").getData(); rb = (Point) rbData.getData();
} }
ret = new MobSkill(skillId, level);
ret.addSummons(toSummon); MobSkill loadedMobSkill = new MobSkill.Builder(type, level)
ret.setCoolTime(cooltime); .mpCon(mpCon)
ret.setDuration(duration); .toSummon(toSummon)
ret.setHp(hp); .cooltime(cooltime)
ret.setMpCon(mpCon); .duration(duration)
ret.setSpawnEffect(effect); .hp(hp)
ret.setX(x); .x(x)
ret.setY(y); .y(y)
ret.setCount(count); .count(count)
ret.setProp(prop); .prop(prop)
ret.setLimit(limit); .limit(limit)
ret.setLtRb(lt, rb); .lt(lt)
} .rb(rb)
mobSkills.put(skillId + "" + level, ret); .build();
}
return ret; mobSkills.put(createKey(type, level), loadedMobSkill);
return Optional.of(loadedMobSkill);
} finally { } finally {
writeLock.unlock(); 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;
}
}
+34 -51
View File
@@ -82,8 +82,7 @@ public class Monster extends AbstractLoadedLife {
private int VenomMultiplier = 0; private int VenomMultiplier = 0;
private boolean fake = false; private boolean fake = false;
private boolean dropsDisabled = false; private boolean dropsDisabled = false;
private final List<Pair<Integer, Integer>> usedSkills = new ArrayList<>(); private final Set<MobSkillId> usedSkills = new HashSet<>();
private final Map<Pair<Integer, Integer>, Integer> skillsUsed = new HashMap<>();
private final Set<Integer> usedAttacks = new HashSet<>(); private final Set<Integer> usedAttacks = new HashSet<>();
private Set<Integer> calledMobOids = null; private Set<Integer> calledMobOids = null;
private WeakReference<Monster> callerMob = new WeakReference<>(null); private WeakReference<Monster> callerMob = new WeakReference<>(null);
@@ -1276,11 +1275,11 @@ public class Monster extends AbstractLoadedLife {
return true; return true;
} }
public final void dispelSkill(final MobSkill skillId) { public final void dispelSkill(final MobSkill skill) {
List<MonsterStatus> toCancel = new ArrayList<>(); List<MonsterStatus> toCancel = new ArrayList<>();
for (Entry<MonsterStatus, MonsterStatusEffect> effects : stati.entrySet()) { for (Entry<MonsterStatus, MonsterStatusEffect> effects : stati.entrySet()) {
MonsterStatusEffect mse = effects.getValue(); 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()); 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 = () -> { final Runnable cancelTask = () -> {
if (isAlive()) { if (isAlive()) {
Packet packet = PacketCreator.cancelMonsterStatus(getObjectId(), stats); Packet packet = PacketCreator.cancelMonsterStatus(getObjectId(), stats);
@@ -1426,7 +1425,7 @@ public class Monster extends AbstractLoadedLife {
return map.getAggroCoordinator(); return map.getAggroCoordinator();
} }
public List<Pair<Integer, Integer>> getSkills() { public Set<MobSkillId> getSkills() {
return stats.getSkills(); return stats.getSkills();
} }
@@ -1434,26 +1433,12 @@ public class Monster extends AbstractLoadedLife {
return stats.hasSkill(skillId, level); 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) { public boolean canUseSkill(MobSkill toUse, boolean apply) {
if (toUse == null || isBuffed(MonsterStatus.SEAL_SKILL)) { if (toUse == null || isBuffed(MonsterStatus.SEAL_SKILL)) {
return false; return false;
} }
int useSkillid = toUse.getSkillId(); if (isReflectSkill(toUse)) {
if (useSkillid >= 143 && useSkillid <= 145) {
if (this.isBuffed(MonsterStatus.WEAPON_REFLECT) || this.isBuffed(MonsterStatus.MAGIC_REFLECT)) { if (this.isBuffed(MonsterStatus.WEAPON_REFLECT) || this.isBuffed(MonsterStatus.MAGIC_REFLECT)) {
return false; return false;
} }
@@ -1461,11 +1446,9 @@ public class Monster extends AbstractLoadedLife {
monsterLock.lock(); monsterLock.lock();
try { try {
for (Pair<Integer, Integer> skill : usedSkills) { // thanks OishiiKawaiiDesu for noticing an issue with mobskill cooldown if (usedSkills.contains(toUse.getId())) {
if (skill.getLeft() == useSkillid && skill.getRight() == toUse.getSkillLevel()) {
return false; return false;
} }
}
int mpCon = toUse.getMpCon(); int mpCon = toUse.getMpCon();
if (mp < mpCon) { if (mp < mpCon) {
@@ -1488,48 +1471,36 @@ public class Monster extends AbstractLoadedLife {
return true; return true;
} }
private void usedSkill(MobSkill skill) { private boolean isReflectSkill(MobSkill mobSkill) {
final int skillId = skill.getSkillId(), level = skill.getSkillLevel(); return switch (mobSkill.getType()) {
long cooltime = skill.getCoolTime(); case PHYSICAL_COUNTER, MAGIC_COUNTER, PHYSICAL_AND_MAGIC_COUNTER -> true;
default -> false;
};
}
private void usedSkill(MobSkill skill) {
final MobSkillId msId = skill.getId();
monsterLock.lock(); monsterLock.lock();
try { try {
mp -= skill.getMpCon(); mp -= skill.getMpCon();
Pair<Integer, Integer> skillKey = new Pair<>(skillId, level); this.usedSkills.add(msId);
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);
}
} finally { } finally {
monsterLock.unlock(); monsterLock.unlock();
} }
final Monster mons = this; final Monster mons = this;
MapleMap mmap = mons.getMap(); 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); 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(); monsterLock.lock();
try { try {
int index = -1; usedSkills.remove(msId);
for (Pair<Integer, Integer> skill : usedSkills) {
if (skill.getLeft() == skillId && skill.getRight() == level) {
index = usedSkills.indexOf(skill);
break;
}
}
if (index != -1) {
usedSkills.remove(index);
}
} finally { } finally {
monsterLock.unlock(); monsterLock.unlock();
} }
@@ -1593,8 +1564,20 @@ public class Monster extends AbstractLoadedLife {
} }
} }
public int getNoSkills() { public boolean hasAnySkill() {
return this.stats.getNoSkills(); 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() { public boolean isFirstAttack() {
+7 -13
View File
@@ -41,7 +41,7 @@ public class MonsterStats {
public Map<Element, ElementalEffectiveness> resistance = new HashMap<>(); public Map<Element, ElementalEffectiveness> resistance = new HashMap<>();
public List<Integer> revives = Collections.emptyList(); public List<Integer> revives = Collections.emptyList();
public byte tagColor, tagBgColor; 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 Pair<Integer, Integer> cool = null;
public BanishInfo banish = null; public BanishInfo banish = null;
public List<loseItem> loseItem = null; public List<loseItem> loseItem = null;
@@ -190,18 +190,12 @@ public class MonsterStats {
this.tagBgColor = (byte) tagBgColor; this.tagBgColor = (byte) tagBgColor;
} }
public void setSkills(List<Pair<Integer, Integer>> skills) { public void setSkills(Set<MobSkillId> skills) {
for (int i = this.skills.size(); i < skills.size(); i++) { this.skills = skills;
this.skills.add(null);
} }
for (int i = 0; i < skills.size(); i++) { public Set<MobSkillId> getSkills() {
this.skills.set(i, skills.get(i)); return Collections.unmodifiableSet(this.skills);
}
}
public List<Pair<Integer, Integer>> getSkills() {
return Collections.unmodifiableList(this.skills);
} }
public int getNoSkills() { public int getNoSkills() {
@@ -209,8 +203,8 @@ public class MonsterStats {
} }
public boolean hasSkill(int skillId, int level) { public boolean hasSkill(int skillId, int level) {
for (Pair<Integer, Integer> skill : skills) { for (MobSkillId skill : skills) {
if (skill.getLeft() == skillId && skill.getRight() == level) { if (skill.type().getId() == skillId && skill.level() == level) {
return true; return true;
} }
} }
+2 -2
View File
@@ -138,14 +138,14 @@ public class Mist extends AbstractMapObject {
if (owner != null) { if (owner != null) {
return PacketCreator.spawnMist(getObjectId(), owner.getId(), getSourceSkill().getId(), owner.getSkillLevel(SkillFactory.getSkill(source.getSourceId())), this); 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) { public final Packet makeFakeSpawnData(int level) {
if (owner != null) { if (owner != null) {
return PacketCreator.spawnMist(getObjectId(), owner.getId(), getSourceSkill().getId(), level, this); 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 @Override
@@ -8,6 +8,7 @@ import provider.DataTool;
import provider.wz.WZFiles; import provider.wz.WZFiles;
import server.life.MobSkill; import server.life.MobSkill;
import server.life.MobSkillFactory; import server.life.MobSkillFactory;
import server.life.MobSkillType;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@@ -42,7 +43,15 @@ public class CarnivalFactory {
} }
for (Data z : dataRoot.getData("MCSkill.img")) { for (Data z : dataRoot.getData("MCSkill.img")) {
Integer id = Integer.parseInt(z.getName()); 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); skills.put(id, ms);
if (ms.targetsAll) { if (ms.targetsAll) {
@@ -52,7 +61,11 @@ public class CarnivalFactory {
} }
} }
for (Data z : dataRoot.getData("MCGuardian.img")) { 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) { public MCSkill getSkill(final int id) {
MCSkill skill = skills.get(id); MCSkill skill = skills.get(id);
if (skill != null && skill.skillid <= 0) { if (skill != null && skill.mobSkillType == null) {
return randomizeSkill(skill.targetsAll); return randomizeSkill(skill.targetsAll);
} else { } else {
return skill; return skill;
@@ -77,28 +90,13 @@ public class CarnivalFactory {
return guardians.get(id); return guardians.get(id);
} }
public static class MCSkill { public record MCSkill(int cpLoss, MobSkillType mobSkillType, int level, boolean targetsAll) {
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 MobSkill getSkill() { public MobSkill getSkill() {
return getMobSkill(skillid, level); return MobSkillFactory.getMobSkill(mobSkillType, level);
}
public static MobSkill getMobSkill(int skillid, int level) {
return MobSkillFactory.getMobSkill(skillid, level);
} }
public Disease getDisease() { 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.CashShop.SpecialCashItem;
import server.*; import server.*;
import server.events.gm.Snowball; import server.events.gm.Snowball;
import server.life.MobSkill; import server.life.*;
import server.life.Monster;
import server.life.NPC;
import server.life.PlayerNPC;
import server.maps.*; import server.maps.*;
import server.maps.MiniGame.MiniGameResult; import server.maps.MiniGame.MiniGameResult;
import server.movement.LifeMovementFragment; import server.movement.LifeMovementFragment;
@@ -106,6 +103,11 @@ public class PacketCreator {
return utcTimestamp * 10000 + FT_UT_OFFSET; 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) { public static Packet showHpHealed(int cid, int amount) {
OutPacket p = OutPacket.create(SendOpcode.SHOW_FOREIGN_EFFECT); OutPacket p = OutPacket.create(SendOpcode.SHOW_FOREIGN_EFFECT);
p.writeInt(cid); p.writeInt(cid);
@@ -1410,8 +1412,7 @@ public class PacketCreator {
MobSkill mobSkill = mse.getMobSkill(); MobSkill mobSkill = mse.getMobSkill();
if (mobSkill != null) { if (mobSkill != null) {
p.writeShort(mobSkill.getSkillId()); writeMobSkillId(p, mobSkill.getId());
p.writeShort(mobSkill.getSkillLevel());
switch (s.getKey()) { switch (s.getKey()) {
case WEAPON_REFLECT -> pCounter = mobSkill.getX(); case WEAPON_REFLECT -> pCounter = mobSkill.getX();
@@ -2917,8 +2918,7 @@ public class PacketCreator {
writeLongMaskD(p, statups); writeLongMaskD(p, statups);
for (Pair<Disease, Integer> statup : statups) { for (Pair<Disease, Integer> statup : statups) {
p.writeShort(statup.getRight().shortValue()); p.writeShort(statup.getRight().shortValue());
p.writeShort(skill.getSkillId()); writeMobSkillId(p, skill.getId());
p.writeShort(skill.getSkillLevel());
p.writeInt((int) skill.getDuration()); p.writeInt((int) skill.getDuration());
} }
p.writeShort(0); // ??? wk charges have 600 here o.o p.writeShort(0); // ??? wk charges have 600 here o.o
@@ -2936,8 +2936,7 @@ public class PacketCreator {
if (statup.getLeft() == Disease.POISON) { if (statup.getLeft() == Disease.POISON) {
p.writeShort(statup.getRight().shortValue()); p.writeShort(statup.getRight().shortValue());
} }
p.writeShort(skill.getSkillId()); writeMobSkillId(p, skill.getId());
p.writeShort(skill.getSkillLevel());
} }
p.writeShort(0); // same as give_buff p.writeShort(0); // same as give_buff
p.writeShort(900);//Delay p.writeShort(900);//Delay
@@ -3051,8 +3050,7 @@ public class PacketCreator {
if (statup.getLeft() == Disease.POISON) { if (statup.getLeft() == Disease.POISON) {
p.writeShort(statup.getRight().shortValue()); p.writeShort(statup.getRight().shortValue());
} }
p.writeShort(skill.getSkillId()); writeMobSkillId(p, skill.getId());
p.writeShort(skill.getSkillLevel());
} }
p.writeShort(0); // same as give_buff p.writeShort(0); // same as give_buff
p.writeShort(900);//Delay p.writeShort(900);//Delay
@@ -3932,8 +3930,7 @@ public class PacketCreator {
for (Map.Entry<MonsterStatus, Integer> stat : stati.entrySet()) { for (Map.Entry<MonsterStatus, Integer> stat : stati.entrySet()) {
p.writeShort(stat.getValue()); p.writeShort(stat.getValue());
if (mse.isMonsterSkill()) { if (mse.isMonsterSkill()) {
p.writeShort(mse.getMobSkill().getSkillId()); writeMobSkillId(p, mse.getMobSkill().getId());
p.writeShort(mse.getMobSkill().getSkillLevel());
} else { } else {
p.writeInt(mse.getSkill().getId()); p.writeInt(mse.getSkill().getId());
} }
@@ -3984,11 +3981,15 @@ public class PacketCreator {
return p; 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); OutPacket p = OutPacket.create(SendOpcode.SPAWN_MIST);
p.writeInt(objId); 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(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.writeInt(skill);
p.writeByte(level); p.writeByte(level);
p.writeShort(mist.getSkillDelay()); // Skill delay p.writeShort(mist.getSkillDelay()); // Skill delay
@@ -3,12 +3,10 @@ package tools.mapletools;
import provider.*; import provider.*;
import provider.wz.DataType; import provider.wz.DataType;
import provider.wz.WZFiles; import provider.wz.WZFiles;
import server.life.Element; import server.life.*;
import server.life.ElementalEffectiveness;
import server.life.LifeFactory.BanishInfo; import server.life.LifeFactory.BanishInfo;
import server.life.LifeFactory.loseItem; import server.life.LifeFactory.loseItem;
import server.life.LifeFactory.selfDestruction; import server.life.LifeFactory.selfDestruction;
import server.life.MonsterStats;
import tools.Pair; import tools.Pair;
import java.time.Duration; import java.time.Duration;
@@ -110,9 +108,12 @@ public class MonsterStatFetcher {
Data monsterSkillData = monsterInfoData.getChildByPath("skill"); Data monsterSkillData = monsterInfoData.getChildByPath("skill");
if (monsterSkillData != null) { if (monsterSkillData != null) {
int i = 0; int i = 0;
List<Pair<Integer, Integer>> skills = new ArrayList<>(); Set<MobSkillId> skills = new HashSet<>();
while (monsterSkillData.getChildByPath(Integer.toString(i)) != null) { 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++; i++;
} }
stats.setSkills(skills); 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>