/* This file is part of the OdinMS Maple Story Server Copyright (C) 2008 Patrick Huy Matthias Butz Jan Christian Meyer This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation version 3 as published by the Free Software Foundation. You may not use, modify or distribute this program under any other version of the GNU Affero General Public License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package server.life; import client.MapleBuffStat; import client.MapleCharacter; import client.MapleClient; import client.MapleJob; import client.Skill; import client.SkillFactory; import client.status.MonsterStatus; import client.status.MonsterStatusEffect; import constants.ServerConstants; import constants.skills.FPMage; import constants.skills.Hermit; import constants.skills.ILMage; import constants.skills.NightLord; import constants.skills.NightWalker; import constants.skills.Shadower; import constants.skills.SuperGM; import java.awt.Point; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import net.server.world.MapleParty; import net.server.world.MaplePartyCharacter; import server.TimerManager; import server.life.MapleLifeFactory.BanishInfo; import server.maps.MapleMap; import server.maps.MapleMapObject; import server.maps.MapleMapObjectType; import tools.MaplePacketCreator; import tools.Pair; import tools.Randomizer; public class MapleMonster extends AbstractLoadedMapleLife { private ChangeableStats ostats = null; //unused, v83 WZs offers no support for changeable stats. private MapleMonsterStats stats; private int hp, mp; private WeakReference controller = new WeakReference<>(null); private boolean controllerHasAggro, controllerKnowsAboutAggro; private Collection listeners = new LinkedList<>(); private EnumMap stati = new EnumMap<>(MonsterStatus.class); private ArrayList alreadyBuffed = new ArrayList(); private MapleMap map; private int VenomMultiplier = 0; private boolean fake = false; private boolean dropsDisabled = false; private List> usedSkills = new ArrayList<>(); private Map, Integer> skillsUsed = new HashMap<>(); private List stolenItems = new ArrayList<>(); private int team; private final HashMap takenDamage = new HashMap<>(); private ReentrantLock monsterLock = new ReentrantLock(); public MapleMonster(int id, MapleMonsterStats stats) { super(id); initWithStats(stats); } public MapleMonster(MapleMonster monster) { super(monster); initWithStats(monster.stats); } public void lockMonster() { monsterLock.lock(); } public void unlockMonster() { monsterLock.unlock(); } private void initWithStats(MapleMonsterStats stats) { setStance(5); this.stats = stats; hp = stats.getHp(); mp = stats.getMp(); } public void disableDrops() { this.dropsDisabled = true; } public boolean dropsDisabled() { return dropsDisabled; } public void setMap(MapleMap map) { this.map = map; } public int getHp() { return hp; } public void setHp(int hp) { this.hp = hp; } public int getMaxHp() { return stats.getHp(); } public int getMp() { return mp; } public void setMp(int mp) { if (mp < 0) { mp = 0; } this.mp = mp; } public int getMaxMp() { return stats.getMp(); } public int getExp() { return stats.getExp(); } int getLevel() { return stats.getLevel(); } public int getCP() { return stats.getCP(); } public int getTeam() { return team; } public void setTeam(int team) { this.team = team; } public int getVenomMulti() { return this.VenomMultiplier; } public void setVenomMulti(int multiplier) { this.VenomMultiplier = multiplier; } public MapleMonsterStats getStats() { return stats; } public boolean isBoss() { return stats.isBoss() || isHT(); } public int getAnimationTime(String name) { return stats.getAnimationTime(name); } private List getRevives() { return stats.getRevives(); } private byte getTagColor() { return stats.getTagColor(); } private byte getTagBgColor() { return stats.getTagBgColor(); } /** * * @param from the player that dealt the damage * @param damage */ public synchronized void damage(MapleCharacter from, int damage) { // may be pointless synchronization if (!isAlive()) { return; } int trueDamage = Math.min(hp, damage); // since magic happens otherwise B^) if(ServerConstants.USE_DEBUG == true && from != null) from.dropMessage(5, "Hitted MOB " + this.getId()); dispatchMonsterDamaged(from, trueDamage); hp -= damage; if (takenDamage.containsKey(from.getId())) { takenDamage.get(from.getId()).addAndGet(trueDamage); } else { takenDamage.put(from.getId(), new AtomicInteger(trueDamage)); } if (hasBossHPBar()) { from.getMap().broadcastMessage(makeBossHPBarPacket(), getPosition()); } else if (!isBoss()) { int remainingHP = (int) Math.max(1, hp * 100f / getMaxHp()); byte[] packet = MaplePacketCreator.showMonsterHP(getObjectId(), remainingHP); if (from.getParty() != null) { for (MaplePartyCharacter mpc : from.getParty().getMembers()) { MapleCharacter member = from.getMap().getCharacterById(mpc.getId()); // god bless if (member != null) { member.announce(packet.clone()); // clone it just in case of crypto } } } else { from.announce(packet); } } } public void heal(int hp, int mp) { int hp2Heal = getHp() + hp; int mp2Heal = getMp() + mp; if (hp2Heal >= getMaxHp()) { hp2Heal = getMaxHp(); } if (mp2Heal >= getMaxMp()) { mp2Heal = getMaxMp(); } setHp(hp2Heal); setMp(mp2Heal); getMap().broadcastMessage(MaplePacketCreator.healMonster(getObjectId(), hp)); } public boolean isAttackedBy(MapleCharacter chr) { return takenDamage.containsKey(chr.getId()); } private void distributeExperienceToParty(int pid, int exp, int killer, Map expDist) { LinkedList members = new LinkedList<>(); Collection chrs = map.getCharacters(); for (MapleCharacter mc : chrs) { if (mc.getPartyId() == pid) { members.add(mc); } } final int minLevel = getLevel() - 5; int partyLevel = 0; int leechMinLevel = 0; for (MapleCharacter mc : members) { if (mc.getLevel() >= minLevel) { leechMinLevel = Math.min(mc.getLevel() - 5, minLevel); } } int leechCount = 0; for (MapleCharacter mc : members) { if (mc.getLevel() >= leechMinLevel) { partyLevel += mc.getLevel(); leechCount++; } } final int mostDamageCid = getHighestDamagerId(); for (MapleCharacter mc : members) { int id = mc.getId(); int level = mc.getLevel(); if (expDist.containsKey(id) || level >= leechMinLevel) { boolean isKiller = killer == id; boolean mostDamage = mostDamageCid == id; int xp = (int) (exp * 0.80f * level / partyLevel); if (mostDamage) { xp += (exp * 0.20f); } giveExpToCharacter(mc, xp, isKiller, leechCount); } } } public void distributeExperience(int killerId) { if (isAlive()) { return; } int exp = getExp(); int totalHealth = getMaxHp(); Map expDist = new HashMap<>(); Map partyExp = new HashMap<>(); // 80% of pool is split amongst all the damagers for (Entry damage : takenDamage.entrySet()) { expDist.put(damage.getKey(), (int) (0.80f * exp * damage.getValue().get() / totalHealth)); } Collection chrs = map.getCharacters(); for (MapleCharacter mc : chrs) { if (expDist.containsKey(mc.getId())) { boolean isKiller = mc.getId() == killerId; int xp = expDist.get(mc.getId()); if (isKiller) { xp += exp / 5; } MapleParty p = mc.getParty(); if (p != null) { int pID = p.getId(); int pXP = xp + (partyExp.containsKey(pID) ? partyExp.get(pID) : 0); partyExp.put(pID, pXP); } else { giveExpToCharacter(mc, xp, isKiller, 1); } } } for (Entry party : partyExp.entrySet()) { distributeExperienceToParty(party.getKey(), party.getValue(), killerId, expDist); } } public void giveExpToCharacter(MapleCharacter attacker, int exp, boolean isKiller, int numExpSharers) { if (isKiller) { if (getMap().getEventInstance() != null) { getMap().getEventInstance().monsterKilled(attacker, this); } } final int partyModifier = numExpSharers > 1 ? (110 + (5 * (numExpSharers - 2))) : 0; int partyExp = 0; if (attacker.getHp() > 0) { int personalExp = exp * attacker.getExpRate(); if (exp > 0) { if (partyModifier > 0) { partyExp = (int) (personalExp * ServerConstants.PARTY_EXPERIENCE_MOD * partyModifier / 1000f); } Integer holySymbol = attacker.getBuffedValue(MapleBuffStat.HOLY_SYMBOL); boolean GMHolySymbol = attacker.getBuffSource(MapleBuffStat.HOLY_SYMBOL) == SuperGM.HOLY_SYMBOL; if (holySymbol != null) { if (numExpSharers == 1 && !GMHolySymbol) { personalExp *= 1.0 + (holySymbol.doubleValue() / 500.0); } else { personalExp *= 1.0 + (holySymbol.doubleValue() / 100.0); } } if (stati.containsKey(MonsterStatus.SHOWDOWN)) { personalExp *= (stati.get(MonsterStatus.SHOWDOWN).getStati().get(MonsterStatus.SHOWDOWN).doubleValue() / 100.0 + 1.0); } } attacker.gainExp(personalExp, partyExp, true, false, isKiller); attacker.mobKilled(getId()); attacker.increaseEquipExp(personalExp); } } public MapleCharacter killBy(final MapleCharacter killer) { distributeExperience(killer != null ? killer.getId() : 0); if (getController() != null) { // this can/should only happen when a hidden gm attacks the monster getController().getClient().announce(MaplePacketCreator.stopControllingMonster(this.getObjectId())); getController().stopControllingMonster(this); } final List toSpawn = this.getRevives(); // this doesn't work (?) if (toSpawn != null) { final MapleMap reviveMap = killer.getMap(); if (toSpawn.contains(9300216) && reviveMap.getId() > 925000000 && reviveMap.getId() < 926000000) { reviveMap.broadcastMessage(MaplePacketCreator.playSound("Dojang/clear")); reviveMap.broadcastMessage(MaplePacketCreator.showEffect("dojang/end/clear")); } Pair timeMob = reviveMap.getTimeMob(); if (timeMob != null) { if (toSpawn.contains(timeMob.getLeft())) { reviveMap.broadcastMessage(MaplePacketCreator.serverNotice(6, timeMob.getRight())); } if (timeMob.getLeft() == 9300338 && (reviveMap.getId() >= 922240100 && reviveMap.getId() <= 922240119)) { if (!reviveMap.containsNPC(9001108)) { MapleNPC npc = MapleLifeFactory.getNPC(9001108); npc.setPosition(new Point(172, 9)); npc.setCy(9); npc.setRx0(172 + 50); npc.setRx1(172 - 50); npc.setFh(27); reviveMap.addMapObject(npc); reviveMap.broadcastMessage(MaplePacketCreator.spawnNPC(npc)); } else { reviveMap.toggleHiddenNPC(9001108); } } } TimerManager.getInstance().schedule(new Runnable() { @Override public void run() { for (Integer mid : toSpawn) { final MapleMonster mob = MapleLifeFactory.getMonster(mid); mob.setPosition(getPosition()); if (dropsDisabled()) { mob.disableDrops(); } reviveMap.spawnMonster(mob); if(mob.getId() >= 8810010 && mob.getId() <= 8810017 && reviveMap.isHorntailDefeated()) { for(int i = 8810018; i >= 8810010; i--) reviveMap.killMonster(reviveMap.getMonsterById(i), killer, true); } } } }, getAnimationTime("die1")); } else { // is this even necessary? System.out.println("[CRITICAL LOSS] toSpawn is null for " + this.getName()); } MapleCharacter looter = map.getCharacterById(getHighestDamagerId()); return looter != null ? looter : killer; } public void dispatchMonsterKilled() { if (getMap().getEventInstance() != null) { if (!this.getStats().isFriendly()) { getMap().getEventInstance().monsterKilled(this); } } for (MonsterListener listener : listeners.toArray(new MonsterListener[listeners.size()])) { listener.monsterKilled(getAnimationTime("die1")); } } public void dispatchMonsterDamaged(MapleCharacter from, int trueDmg) { for (MonsterListener listener : listeners.toArray(new MonsterListener[listeners.size()])) { listener.monsterDamaged(from, trueDmg); } } // should only really be used to determine drop owner private int getHighestDamagerId() { int curId = 0; int curDmg = 0; for (Entry damage : takenDamage.entrySet()) { curId = damage.getValue().get() >= curDmg ? damage.getKey() : curId; curDmg = damage.getKey() == curId ? damage.getValue().get() : curDmg; } return curId; } public boolean isAlive() { return this.hp > 0; } public MapleCharacter getController() { return controller.get(); } public void setController(MapleCharacter controller) { this.controller = new WeakReference<>(controller); } public void switchController(MapleCharacter newController, boolean immediateAggro) { MapleCharacter controllers = getController(); if (controllers == newController) { return; } if (controllers != null) { controllers.stopControllingMonster(this); controllers.getClient().announce(MaplePacketCreator.stopControllingMonster(getObjectId())); } newController.controlMonster(this, immediateAggro); setController(newController); if (immediateAggro) { setControllerHasAggro(true); } setControllerKnowsAboutAggro(false); } public void addListener(MonsterListener listener) { listeners.add(listener); } public boolean isControllerHasAggro() { return fake ? false : controllerHasAggro; } public void setControllerHasAggro(boolean controllerHasAggro) { if (fake) { return; } this.controllerHasAggro = controllerHasAggro; } public boolean isControllerKnowsAboutAggro() { return fake ? false : controllerKnowsAboutAggro; } public void setControllerKnowsAboutAggro(boolean controllerKnowsAboutAggro) { if (fake) { return; } this.controllerKnowsAboutAggro = controllerKnowsAboutAggro; } public byte[] makeBossHPBarPacket() { return MaplePacketCreator.showBossHP(getId(), getHp(), getMaxHp(), getTagColor(), getTagBgColor()); } public boolean hasBossHPBar() { return (isBoss() && getTagColor() > 0) || isHT(); } private boolean isHT() { return getId() == 8810018; } @Override public void sendSpawnData(MapleClient c) { if (!isAlive()) { return; } if (isFake()) { c.announce(MaplePacketCreator.spawnFakeMonster(this, 0)); } else { c.announce(MaplePacketCreator.spawnMonster(this, false)); } if (stati.size() > 0) { for (final MonsterStatusEffect mse : this.stati.values()) { c.announce(MaplePacketCreator.applyMonsterStatus(getObjectId(), mse, null)); } } if (hasBossHPBar()) { if (this.getMap().countMonster(8810026) > 0 && this.getMap().getId() == 240060200) { this.getMap().killAllMonsters(); return; } c.announce(makeBossHPBarPacket()); } } @Override public void sendDestroyData(MapleClient client) { client.announce(MaplePacketCreator.killMonster(getObjectId(), false)); } @Override public MapleMapObjectType getType() { return MapleMapObjectType.MONSTER; } public boolean isMobile() { return stats.isMobile(); } public ElementalEffectiveness getEffectiveness(Element e) { if (stati.size() > 0 && stati.get(MonsterStatus.DOOM) != null) { return ElementalEffectiveness.NORMAL; // like blue snails } return stats.getEffectiveness(e); } public boolean applyStatus(MapleCharacter from, final MonsterStatusEffect status, boolean poison, long duration) { return applyStatus(from, status, poison, duration, false); } public boolean applyStatus(MapleCharacter from, final MonsterStatusEffect status, boolean poison, long duration, boolean venom) { switch (stats.getEffectiveness(status.getSkill().getElement())) { case IMMUNE: case STRONG: case NEUTRAL: return false; case NORMAL: case WEAK: break; default: { System.out.println("Unknown elemental effectiveness: " + stats.getEffectiveness(status.getSkill().getElement())); return false; } } if (status.getSkill().getId() == FPMage.ELEMENT_COMPOSITION) { // fp compo ElementalEffectiveness effectiveness = stats.getEffectiveness(Element.POISON); if (effectiveness == ElementalEffectiveness.IMMUNE || effectiveness == ElementalEffectiveness.STRONG) { return false; } } else if (status.getSkill().getId() == ILMage.ELEMENT_COMPOSITION) { // il compo ElementalEffectiveness effectiveness = stats.getEffectiveness(Element.ICE); if (effectiveness == ElementalEffectiveness.IMMUNE || effectiveness == ElementalEffectiveness.STRONG) { return false; } } else if (status.getSkill().getId() == NightLord.VENOMOUS_STAR || status.getSkill().getId() == Shadower.VENOMOUS_STAB || status.getSkill().getId() == NightWalker.VENOM) {// venom if (stats.getEffectiveness(Element.POISON) == ElementalEffectiveness.WEAK) { return false; } } if (poison && getHp() <= 1) { return false; } final Map statis = status.getStati(); if (stats.isBoss()) { if (!(statis.containsKey(MonsterStatus.SPEED) && statis.containsKey(MonsterStatus.NINJA_AMBUSH) && statis.containsKey(MonsterStatus.WATK))) { return false; } } for (MonsterStatus stat : statis.keySet()) { final MonsterStatusEffect oldEffect = stati.get(stat); if (oldEffect != null) { oldEffect.removeActiveStatus(stat); if (oldEffect.getStati().isEmpty()) { oldEffect.cancelTask(); oldEffect.cancelDamageSchedule(); } } } TimerManager timerManager = TimerManager.getInstance(); final Runnable cancelTask = new Runnable() { @Override public void run() { if (isAlive()) { byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), status.getStati()); map.broadcastMessage(packet, getPosition()); if (getController() != null && !getController().isMapObjectVisible(MapleMonster.this)) { getController().getClient().announce(packet); } } for (MonsterStatus stat : status.getStati().keySet()) { stati.remove(stat); } setVenomMulti(0); status.cancelDamageSchedule(); } }; if (poison) { int poisonLevel = from.getSkillLevel(status.getSkill()); int poisonDamage = Math.min(Short.MAX_VALUE, (int) (getMaxHp() / (70.0 - poisonLevel) + 0.999)); status.setValue(MonsterStatus.POISON, Integer.valueOf(poisonDamage)); status.setDamageSchedule(timerManager.register(new DamageTask(poisonDamage, from, status, cancelTask, 0), 1000, 1000)); } else if (venom) { if (from.getJob() == MapleJob.NIGHTLORD || from.getJob() == MapleJob.SHADOWER || from.getJob().isA(MapleJob.NIGHTWALKER3)) { int poisonLevel, matk, id = from.getJob().getId(); int skill = (id == 412 ? NightLord.VENOMOUS_STAR : (id == 422 ? Shadower.VENOMOUS_STAB : NightWalker.VENOM)); poisonLevel = from.getSkillLevel(SkillFactory.getSkill(skill)); if (poisonLevel <= 0) { return false; } matk = SkillFactory.getSkill(skill).getEffect(poisonLevel).getMatk(); int luk = from.getLuk(); int maxDmg = (int) Math.ceil(Math.min(Short.MAX_VALUE, 0.2 * luk * matk)); int minDmg = (int) Math.ceil(Math.min(Short.MAX_VALUE, 0.1 * luk * matk)); int gap = maxDmg - minDmg; if (gap == 0) { gap = 1; } int poisonDamage = 0; for (int i = 0; i < getVenomMulti(); i++) { poisonDamage += (Randomizer.nextInt(gap) + minDmg); } poisonDamage = Math.min(Short.MAX_VALUE, poisonDamage); status.setValue(MonsterStatus.VENOMOUS_WEAPON, Integer.valueOf(poisonDamage)); status.setDamageSchedule(timerManager.register(new DamageTask(poisonDamage, from, status, cancelTask, 0), 1000, 1000)); } else { return false; } } else if (status.getSkill().getId() == Hermit.SHADOW_WEB || status.getSkill().getId() == NightWalker.SHADOW_WEB) { //Shadow Web status.setDamageSchedule(timerManager.schedule(new DamageTask((int) (getMaxHp() / 50.0 + 0.999), from, status, cancelTask, 1), 3500)); } else if (status.getSkill().getId() == 4121004 || status.getSkill().getId() == 4221004) { // Ninja Ambush final Skill skill = SkillFactory.getSkill(status.getSkill().getId()); final byte level = from.getSkillLevel(skill); final int damage = (int) ((from.getStr() + from.getLuk()) * (1.5 + (level * 0.05)) * skill.getEffect(level).getDamage()); /*if (getHp() - damage <= 1) { make hp 1 betch damage = getHp() - (getHp() - 1); }*/ status.setValue(MonsterStatus.NINJA_AMBUSH, Integer.valueOf(damage)); status.setDamageSchedule(timerManager.register(new DamageTask(damage, from, status, cancelTask, 2), 1000, 1000)); } for (MonsterStatus stat : status.getStati().keySet()) { stati.put(stat, status); alreadyBuffed.add(stat); } int animationTime = status.getSkill().getAnimationTime(); byte[] packet = MaplePacketCreator.applyMonsterStatus(getObjectId(), status, null); map.broadcastMessage(packet, getPosition()); if (getController() != null && !getController().isMapObjectVisible(this)) { getController().getClient().announce(packet); } status.setCancelTask(timerManager.schedule(cancelTask, duration + animationTime)); return true; } public void applyMonsterBuff(final Map stats, final int x, int skillId, long duration, MobSkill skill, final List reflection) { TimerManager timerManager = TimerManager.getInstance(); final Runnable cancelTask = new Runnable() { @Override public void run() { if (isAlive()) { byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), stats); map.broadcastMessage(packet, getPosition()); if (getController() != null && !getController().isMapObjectVisible(MapleMonster.this)) { getController().getClient().announce(packet); } for (final MonsterStatus stat : stats.keySet()) { stati.remove(stat); } } } }; final MonsterStatusEffect effect = new MonsterStatusEffect(stats, null, skill, true); byte[] packet = MaplePacketCreator.applyMonsterStatus(getObjectId(), effect, reflection); map.broadcastMessage(packet, getPosition()); for (MonsterStatus stat : stats.keySet()) { stati.put(stat, effect); alreadyBuffed.add(stat); } if (getController() != null && !getController().isMapObjectVisible(this)) { getController().getClient().announce(packet); } effect.setCancelTask(timerManager.schedule(cancelTask, duration)); } public void debuffMob(int skillid) { //skillid is not going to be used for now until I get warrior debuff working MonsterStatus[] stats = {MonsterStatus.WEAPON_ATTACK_UP, MonsterStatus.WEAPON_DEFENSE_UP, MonsterStatus.MAGIC_ATTACK_UP, MonsterStatus.MAGIC_DEFENSE_UP}; for (int i = 0; i < stats.length; i++) { if (isBuffed(stats[i])) { final MonsterStatusEffect oldEffect = stati.get(stats[i]); byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), oldEffect.getStati()); map.broadcastMessage(packet, getPosition()); if (getController() != null && !getController().isMapObjectVisible(MapleMonster.this)) { getController().getClient().announce(packet); } stati.remove(stats); } } } public boolean isBuffed(MonsterStatus status) { return stati.containsKey(status); } public void setFake(boolean fake) { this.fake = fake; } public boolean isFake() { return fake; } public MapleMap getMap() { return map; } public List> getSkills() { return stats.getSkills(); } public boolean hasSkill(int skillId, int level) { return stats.hasSkill(skillId, level); } public boolean canUseSkill(MobSkill toUse) { if (toUse == null) { return false; } for (Pair skill : usedSkills) { if (skill.getLeft() == toUse.getSkillId() && skill.getRight() == toUse.getSkillLevel()) { return false; } } if (toUse.getLimit() > 0) { if (this.skillsUsed.containsKey(new Pair<>(toUse.getSkillId(), toUse.getSkillLevel()))) { int times = this.skillsUsed.get(new Pair<>(toUse.getSkillId(), toUse.getSkillLevel())); if (times >= toUse.getLimit()) { return false; } } } if (toUse.getSkillId() == 200) { Collection mmo = getMap().getMapObjects(); int i = 0; for (MapleMapObject mo : mmo) { if (mo.getType() == MapleMapObjectType.MONSTER) { i++; } } if (i > 100) { return false; } } return true; } public void usedSkill(final int skillId, final int level, long cooltime) { this.usedSkills.add(new Pair<>(skillId, level)); if (this.skillsUsed.containsKey(new Pair<>(skillId, level))) { int times = this.skillsUsed.get(new Pair<>(skillId, level)) + 1; this.skillsUsed.remove(new Pair<>(skillId, level)); this.skillsUsed.put(new Pair<>(skillId, level), times); } else { this.skillsUsed.put(new Pair<>(skillId, level), 1); } final MapleMonster mons = this; TimerManager tMan = TimerManager.getInstance(); tMan.schedule( new Runnable() { @Override public void run() { mons.clearSkill(skillId, level); } }, cooltime); } public void clearSkill(int skillId, int level) { int index = -1; for (Pair skill : usedSkills) { if (skill.getLeft() == skillId && skill.getRight() == level) { index = usedSkills.indexOf(skill); break; } } if (index != -1) { usedSkills.remove(index); } } public int getNoSkills() { return this.stats.getNoSkills(); } public boolean isFirstAttack() { return this.stats.isFirstAttack(); } public int getBuffToGive() { return this.stats.getBuffToGive(); } private final class DamageTask implements Runnable { private final int dealDamage; private final MapleCharacter chr; private final MonsterStatusEffect status; private final Runnable cancelTask; private final int type; private final MapleMap map; private DamageTask(int dealDamage, MapleCharacter chr, MonsterStatusEffect status, Runnable cancelTask, int type) { this.dealDamage = dealDamage; this.chr = chr; this.status = status; this.cancelTask = cancelTask; this.type = type; this.map = chr.getMap(); } @Override public void run() { int damage = dealDamage; if (damage >= hp) { damage = hp - 1; if (type == 1 || type == 2) { map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), damage), getPosition()); cancelTask.run(); status.getCancelTask().cancel(false); } } if (hp > 1 && damage > 0) { damage(chr, damage); if (type == 1) { map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), damage), getPosition()); } } } } public String getName() { return stats.getName(); } public void addStolen(int itemId) { stolenItems.add(itemId); } public List getStolen() { return stolenItems; } public void setTempEffectiveness(Element e, ElementalEffectiveness ee, long milli) { final Element fE = e; final ElementalEffectiveness fEE = stats.getEffectiveness(e); if (!stats.getEffectiveness(e).equals(ElementalEffectiveness.WEAK)) { stats.setEffectiveness(e, ee); TimerManager.getInstance().schedule(new Runnable() { @Override public void run() { stats.removeEffectiveness(fE); stats.setEffectiveness(fE, fEE); } }, milli); } } public Collection alreadyBuffedStats() { return Collections.unmodifiableCollection(alreadyBuffed); } public BanishInfo getBanish() { return stats.getBanishInfo(); } public void setBoss(boolean boss) { this.stats.setBoss(boss); } public int getDropPeriodTime() { return stats.getDropPeriod(); } public int getPADamage() { return stats.getPADamage(); } public Map getStati() { return stati; } // ---- one can always have fun trying these pieces of codes below in-game rofl ---- public final ChangeableStats getChangedStats() { return ostats; } public final int getMobMaxHp() { if (ostats != null) { return ostats.hp; } return stats.getHp(); } public final void setOverrideStats(final OverrideMonsterStats ostats) { this.ostats = new ChangeableStats(stats, ostats); this.hp = ostats.getHp(); this.mp = ostats.getMp(); } public final void changeLevel(final int newLevel) { changeLevel(newLevel, true); } public final void changeLevel(final int newLevel, boolean pqMob) { if (!stats.isChangeable()) { return; } this.ostats = new ChangeableStats(stats, newLevel, pqMob); this.hp = ostats.getHp(); this.mp = ostats.getMp(); } private float getDifficultyRate(final int difficulty) { switch(difficulty) { case 6: return(7.7f); case 5: return(5.6f); case 4: return(3.2f); case 3: return(2.1f); case 2: return(1.4f); } return(1.0f); } private void changeLevelByDifficulty(final int difficulty, boolean pqMob) { changeLevel((int)(this.getLevel() * getDifficultyRate(difficulty)), pqMob); } public final void changeDifficulty(final int difficulty, boolean pqMob) { changeLevelByDifficulty(difficulty, pqMob); } }