Welcome!

Join our community of MMO enthusiasts and game developers! By registering, you'll gain access to discussions on the latest developments in MMO server files and collaborate with like-minded individuals. Join us today and unlock the potential of MMO server development!

Join Today!

HeavenMS Exp System.

Newbie Spellweaver
Joined
Mar 16, 2011
Messages
14
Reaction score
0
Hey guys. I'm playing with HeavenMS and trying to change the way party EXP works in MapleMonster.java. Basically with HeavenMS, the two functions; EXP_SPLIT_LEECH_INTERVAL: 5 , and EXP_SPLIT_LEVEL_INTERVAL: 5 , seem to work in opposing ways of each other. I tried setting split leech (difference between level in party members) to a high value, and leaving split level (level between you and the mob) to the normal value of 5 below, and it doesn't function as it should. Basically, instead of you being 110/113 leeching at skeles, you'd be able to leech at any level as long as you were in range of the leecher's level based on the value of split leech interval.

My GOAL is so that it's "gms like" and you can only leech from a mob if you are at LEAST 5 levels below it or greater. Not dependent on party member's level, regardless. Can anyone help me with this?
-- also a side note, I have a VPS and am trying to make a fun server, would anyone wanna join in?
Thanks for any help in advance!
MapleMonster.java :
PHP:
/*
 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 server.life;

import client.MapleBuffStat;
import client.MapleCharacter;
import client.MapleClient;
import client.MapleFamilyEntry;
import client.MapleJob;
import client.Skill;
import client.SkillFactory;
import client.status.MonsterStatus;
import client.status.MonsterStatusEffect;
import config.YamlConfig;
import constants.skills.Crusader;
import constants.skills.FPMage;
import constants.skills.Hermit;
import constants.skills.ILMage;
import constants.skills.NightLord;
import constants.skills.NightWalker;
import constants.skills.Priest;
import constants.skills.Shadower;
import constants.skills.WhiteKnight;
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.HashSet;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import net.server.audit.locks.MonitoredReentrantLock;
import net.server.channel.Channel;
import net.server.world.MapleParty;
import net.server.world.MaplePartyCharacter;
import scripting.event.EventInstanceManager;
import server.TimerManager;
import server.life.MapleLifeFactory.BanishInfo;
import server.maps.MapleMap;
import server.maps.MapleMapObjectType;
import tools.IntervalBuilder;
import tools.MaplePacketCreator;
import tools.Pair;
import tools.Randomizer;
import net.server.audit.LockCollector;
import net.server.audit.locks.MonitoredLockType;
import net.server.audit.locks.factory.MonitoredReentrantLockFactory;
import net.server.services.type.ChannelServices;
import net.server.services.task.channel.MobAnimationService;
import net.server.services.task.channel.MobClearSkillService;
import net.server.services.task.channel.MobStatusService;
import net.server.services.task.channel.OverallService;
import net.server.coordinator.world.MapleMonsterAggroCoordinator;
import server.MapleStatEffect;
import server.loot.MapleLootManager;
import server.maps.MapleSummon;

public class MapleMonster extends AbstractLoadedMapleLife {
    
    private ChangeableStats ostats = null;  //unused, v83 WZs offers no support for changeable stats.
    private MapleMonsterStats stats;
    private AtomicInteger hp = new AtomicInteger(1);
    private AtomicLong maxHpPlusHeal = new AtomicLong(1);
    private int mp;
    private WeakReference<MapleCharacter> controller = new WeakReference<>(null);
    private boolean controllerHasAggro, controllerKnowsAboutAggro, controllerHasPuppet;
    private Collection<MonsterListener> listeners = new LinkedList<>();
    private EnumMap<MonsterStatus, MonsterStatusEffect> stati = new EnumMap<>(MonsterStatus.class);
    private ArrayList<MonsterStatus> alreadyBuffed = new ArrayList<>();
    private MapleMap map;
    private int VenomMultiplier = 0;
    private boolean fake = false;
    private boolean dropsDisabled = false;
    private List<Pair<Integer, Integer>> usedSkills = new ArrayList<>();
    private Map<Pair<Integer, Integer>, Integer> skillsUsed = new HashMap<>();
    private Set<Integer> usedAttacks = new HashSet<>();
    private Set<Integer> calledMobOids = null;
    private WeakReference<MapleMonster> callerMob = new WeakReference<>(null);
    private List<Integer> stolenItems = new ArrayList<>(5);
    private int team;
    private int parentMobOid = 0;
    private int spawnEffect = 0;
    private final HashMap<Integer, AtomicLong> takenDamage = new HashMap<>();
    private ScheduledFuture<?> monsterItemDrop = null;
    private Runnable removeAfterAction = null;
    private boolean availablePuppetUpdate = true;

    private MonitoredReentrantLock externalLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.MOB_EXT);
    private MonitoredReentrantLock monsterLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.MOB, true);
    private MonitoredReentrantLock statiLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.MOB_STATI);
    private MonitoredReentrantLock animationLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.MOB_ANI);
    private MonitoredReentrantLock aggroUpdateLock = MonitoredReentrantLockFactory.createLock(MonitoredLockType.MOB_AGGRO);

    public MapleMonster(int id, MapleMonsterStats stats) {
        super(id);
        initWithStats(stats);
    }

    public MapleMonster(MapleMonster monster) {
        super(monster);
        initWithStats(monster.stats);
    }
    
    public void lockMonster() {
        externalLock.lock();
    }
    
    public void unlockMonster() {
        externalLock.unlock();
    }

    private void initWithStats(MapleMonsterStats baseStats) {
        setStance(5);
        this.stats = baseStats.copy();
        hp.set(stats.getHp());
        mp = stats.getMp();
        
        maxHpPlusHeal.set(hp.get());
    }
    
    public void setSpawnEffect(int effect) {
        spawnEffect = effect;
    }
    
    public int getSpawnEffect() {
        return spawnEffect;
    }

    public void disableDrops() {
        this.dropsDisabled = true;
    }

    public void enableDrops() {
        this.dropsDisabled = false;
    }
    
    public boolean dropsDisabled() {
        return dropsDisabled;
    }

    public void setMap(MapleMap map) {
        this.map = map;
    }
    
    public int getParentMobOid() {
        return parentMobOid;
    }

    public void setParentMobOid(int parentMobId) {
        this.parentMobOid = parentMobId;
    }
    
    public int countAvailableMobSummons(int summonsSize, int skillLimit) {    // limit prop for summons has another conotation, found thanks to MedicOP
        int summonsCount;
        
        Set<Integer> calledOids = this.calledMobOids;
        if(calledOids != null) {
            summonsCount = calledOids.size();
        } else {
            summonsCount = 0;
        }
        
        return Math.min(summonsSize, skillLimit - summonsCount);
    }
    
    public void addSummonedMob(MapleMonster mob) {
        Set<Integer> calledOids = this.calledMobOids;
        if (calledOids == null) {
            calledOids = Collections.synchronizedSet(new HashSet<Integer>());
            this.calledMobOids = calledOids;
        }
        
        calledOids.add(mob.getObjectId());
        mob.setSummonerMob(this);
    }
    
    private void removeSummonedMob(int mobOid) {
        Set<Integer> calledOids = this.calledMobOids;
        if (calledOids != null) {
            calledOids.remove(mobOid);
        }
    }
    
    private void setSummonerMob(MapleMonster mob) {
        this.callerMob = new WeakReference<>(mob);
    }
    
    private void dispatchClearSummons() {
        MapleMonster caller = this.callerMob.get();
        if (caller != null) {
            caller.removeSummonedMob(this.getObjectId());
        }
        
        this.calledMobOids = null;
    }
    
    public void pushRemoveAfterAction(Runnable run) {
        this.removeAfterAction = run;
    }
    
    public Runnable popRemoveAfterAction() {
        Runnable r = this.removeAfterAction;
        this.removeAfterAction = null;
        
        return r;
    }
    
    public int getHp() {
        return hp.get();
    }
    
    public synchronized void addHp(int hp) {
        if (this.hp.get() <= 0) {
            return;
        }
        this.hp.addAndGet(hp);
    }
    
    public synchronized void setStartingHp(int hp) {
        stats.setHp(hp);    // refactored mob stats after non-static HP pool suggestion thanks to twigs
        this.hp.set(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();
    }

    public 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();
    }

    public int getAnimationTime(String name) {
        return stats.getAnimationTime(name);
    }

    private List<Integer> getRevives() {
        return stats.getRevives();
    }

    private byte getTagColor() {
        return stats.getTagColor();
    }

    private byte getTagBgColor() {
        return stats.getTagBgColor();
    }

    public void setHpZero() {     // force HP = 0
        applyAndGetHpDamage(Integer.MAX_VALUE, false);
    }
    
    private boolean applyAnimationIfRoaming(int attackPos, MobSkill skill) {   // roam: not casting attack or skill animations
        if (!animationLock.tryLock()) {
            return false;
        }
    
        try {
            long animationTime;
        
            if(skill == null) {
                animationTime = MapleMonsterInformationProvider.getInstance().getMobAttackAnimationTime(this.getId(), attackPos);
            } else {
                animationTime = MapleMonsterInformationProvider.getInstance().getMobSkillAnimationTime(skill);
            }

            if(animationTime > 0) {
                MobAnimationService service = (MobAnimationService) map.getChannelServer().getServiceAccess(ChannelServices.MOB_ANIMATION);
                return service.registerMobOnAnimationEffect(map.getId(), this.hashCode(), animationTime);
            } else {
                return true;
            }
        } finally {
            animationLock.unlock();
        }
    }
    
    public synchronized Integer applyAndGetHpDamage(int delta, boolean stayAlive) {
        int curHp = hp.get();
        if (curHp <= 0) {       // this monster is already dead
            return null;
        }
        
        if(delta >= 0) {
            if (stayAlive) {
                curHp--;
            }
            int trueDamage = Math.min(curHp, delta);
            
            hp.addAndGet(-trueDamage);
            return trueDamage;
        } else {
            int trueHeal = -delta;
            int hp2Heal = curHp + trueHeal;
            int maxHp = getMaxHp();
            
            if (hp2Heal > maxHp) {
                trueHeal -= (hp2Heal - maxHp);
            }
            
            hp.addAndGet(trueHeal);
            return trueHeal;
        }
    }
    
    public synchronized void disposeMapObject() {     // mob is no longer associated with the map it was in
        hp.set(-1);
    }
    
    public void broadcastMobHpBar(MapleCharacter from) {
        if (hasBossHPBar()) {
            from.setPlayerAggro(this.hashCode());
            from.getMap().broadcastBossHpMessage(this, this.hashCode(), makeBossHPBarPacket(), getPosition());
        } else if (!isBoss()) {
            int remainingHP = (int) Math.max(1, hp.get() * 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 boolean damage(MapleCharacter attacker, int damage, boolean stayAlive) {
        boolean lastHit = false;
        
        this.lockMonster();
        try {
            if (!this.isAlive()) {
                return false;
            }

            /* pyramid not implemented
            Pair<Integer, Integer> cool = this.getStats().getCool();
            if (cool != null) {
                Pyramid pq = (Pyramid) chr.getPartyQuest();
                if (pq != null) {
                    if (damage > 0) {
                        if (damage >= cool.getLeft()) {
                            if ((Math.random() * 100) < cool.getRight()) {
                                pq.cool();
                            } else {
                                pq.kill();
                            }
                        } else {
                            pq.kill();
                        }
                    } else {
                        pq.miss();
                    }
                    killed = true;
                }
            }
            */

            if (damage > 0) {
                this.applyDamage(attacker, damage, stayAlive, false);
                if (!this.isAlive()) {  // monster just died
                    lastHit = true;
                }
            }
        } finally {
            this.unlockMonster();
        }
        
        return lastHit;
    }
    
    /**
     *
     * @param from the player that dealt the damage
     * @param damage
     * @param stayAlive
     */
    private void applyDamage(MapleCharacter from, int damage, boolean stayAlive, boolean fake) {
        Integer trueDamage = applyAndGetHpDamage(damage, stayAlive);
        if (trueDamage == null) {
            return;
        }
        
        if (YamlConfig.config.server.USE_DEBUG) {
            from.dropMessage(5, "Hitted MOB " + this.getId() + ", OID " + this.getObjectId());
        }
        
        if (!fake) {
            dispatchMonsterDamaged(from, trueDamage);
        }
        
        if (!takenDamage.containsKey(from.getId())) {
            takenDamage.put(from.getId(), new AtomicLong(trueDamage));
        } else {
            takenDamage.get(from.getId()).addAndGet(trueDamage);
        }

        broadcastMobHpBar(from);
    }
    
    public void applyFakeDamage(MapleCharacter from, int damage, boolean stayAlive) {
        applyDamage(from, damage, stayAlive, true);
    }
    
    public void heal(int hp, int mp) {
        Integer hpHealed = applyAndGetHpDamage(-hp, false);
        if (hpHealed == null) {
            return;
        }
        
        int mp2Heal = getMp() + mp;
        int maxMp = getMaxMp();
        if (mp2Heal >= maxMp) {
            mp2Heal = maxMp;
        }
        setMp(mp2Heal);
        
        if (hp > 0) {
            getMap().broadcastMessage(MaplePacketCreator.healMonster(getObjectId(), hp, getHp(), getMaxHp()));
        }
        
        maxHpPlusHeal.addAndGet(hpHealed);
        dispatchMonsterHealed(hpHealed);
    }

    public boolean isAttackedBy(MapleCharacter chr) {
        return takenDamage.containsKey(chr.getId());
    }
    
    private static boolean isWhiteExpGain(MapleCharacter chr, Map<Integer, Float> personalRatio, double sdevRatio) {
        Float pr = personalRatio.get(chr.getId());
        if (pr == null) {
            return false;
        }
        
        return pr >= sdevRatio;
    }
    
    private static double calcExperienceStandDevThreshold(List<Float> entryExpRatio, int totalEntries) {
        float avgExpReward = 0.0f;
        for (Float exp : entryExpRatio) {
            avgExpReward += exp;
        }
        
        // thanks Simon (HarborMS) for finding an issue with solo party player gaining yellow EXP when soloing mobs
        avgExpReward /= totalEntries;
        
        float varExpReward = 0.0f;
        for (Float exp : entryExpRatio) {
            varExpReward += Math.pow(exp - avgExpReward, 2);
        }
        varExpReward /= entryExpRatio.size();
        
        return avgExpReward + Math.sqrt(varExpReward);
    }
    
    private void distributePlayerExperience(MapleCharacter chr, float exp, float partyBonusMod, int totalPartyLevel, boolean highestPartyDamager, boolean whiteExpGain, boolean hasPartySharers) {
        float playerExp = (YamlConfig.config.server.EXP_SPLIT_COMMON_MOD * chr.getLevel()) / totalPartyLevel;
        if (highestPartyDamager) playerExp += YamlConfig.config.server.EXP_SPLIT_MVP_MOD;
        
        playerExp *= exp;
        float bonusExp = partyBonusMod * playerExp;
        
        this.giveExpToCharacter(chr, playerExp, bonusExp, whiteExpGain, hasPartySharers);
        giveFamilyRep(chr.getFamilyEntry());
    }
    
    private void distributePartyExperience(Map<MapleCharacter, Long> partyParticipation, float expPerDmg, Set<MapleCharacter> underleveled, Map<Integer, Float> personalRatio, double sdevRatio) {
        IntervalBuilder leechInterval = new IntervalBuilder();
        leechInterval.addInterval(this.getLevel() - YamlConfig.config.server.EXP_SPLIT_LEVEL_INTERVAL, this.getLevel() + YamlConfig.config.server.EXP_SPLIT_LEVEL_INTERVAL);
        
        long maxDamage = 0, partyDamage = 0;
        MapleCharacter participationMvp = null;
        for (Entry<MapleCharacter, Long> e : partyParticipation.entrySet()) {
            long entryDamage = e.getValue();
            partyDamage += entryDamage;
            
            if (maxDamage < entryDamage) {
                maxDamage = entryDamage;
                participationMvp = e.getKey();
            }
            
            // thanks Thora for pointing out leech level limitation
            int chrLevel = e.getKey().getLevel();
            leechInterval.addInterval(chrLevel - YamlConfig.config.server.EXP_SPLIT_LEECH_INTERVAL, chrLevel + YamlConfig.config.server.EXP_SPLIT_LEECH_INTERVAL);
        }
        
        List<MapleCharacter> expMembers = new LinkedList<>();
        int totalPartyLevel = 0;
        
        // thanks G h o s t, Alfred, Vcoc, BHB for poiting out a bug in detecting party members after membership transactions in a party took place
        if (YamlConfig.config.server.USE_ENFORCE_MOB_LEVEL_RANGE) {
            for (MapleCharacter member : partyParticipation.keySet().iterator().next().getPartyMembersOnSameMap()) {
                if (!leechInterval.inInterval(member.getLevel())) {
                    underleveled.add(member);
                    continue;
                }

                totalPartyLevel += member.getLevel();
                expMembers.add(member);
            }
        } else {    // thanks Ari for noticing unused server flag after EXP system overhaul
            for (MapleCharacter member : partyParticipation.keySet().iterator().next().getPartyMembersOnSameMap()) {
                totalPartyLevel += member.getLevel();
                expMembers.add(member);
            }
        }
        
        int membersSize = expMembers.size();
        float participationExp = partyDamage * expPerDmg;
        
        // thanks Crypter for reporting an insufficiency on party exp bonuses
        boolean hasPartySharers = membersSize > 1;
        float partyBonusMod = hasPartySharers ? 0.05f * membersSize : 0.0f;
        
        for (MapleCharacter mc : expMembers) {
            distributePlayerExperience(mc, participationExp, partyBonusMod, totalPartyLevel, mc == participationMvp, isWhiteExpGain(mc, personalRatio, sdevRatio), hasPartySharers);
            giveFamilyRep(mc.getFamilyEntry());
        }
    }
    
    private void distributeExperience(int killerId) {
        if (isAlive()) {
            return;
        }
        
        Map<MapleParty, Map<MapleCharacter, Long>> partyExpDist = new HashMap<>();
        Map<MapleCharacter, Long> soloExpDist = new HashMap<>();
        
        Map<Integer, MapleCharacter> mapPlayers = map.getMapAllPlayers();
        
        int totalEntries = 0;   // counts "participant parties", players who no longer are available in the map is an "independent party"
        for (Entry<Integer, AtomicLong> e : takenDamage.entrySet()) {
            MapleCharacter chr = mapPlayers.get(e.getKey());
            if (chr != null) {
                long damage = e.getValue().longValue();
                
                MapleParty p = chr.getParty();
                if (p != null) {
                    Map<MapleCharacter, Long> partyParticipation = partyExpDist.get(p);
                    if (partyParticipation == null) {
                        partyParticipation = new HashMap<>(6);
                        partyExpDist.put(p, partyParticipation);
                        
                        totalEntries += 1;
                    }
                    
                    partyParticipation.put(chr, damage);
                } else {
                    soloExpDist.put(chr, damage);
                    totalEntries += 1;
                }
            } else {
                totalEntries += 1;
            }
        }
        
        long totalDamage = maxHpPlusHeal.get();
        int mobExp = getExp();
        float expPerDmg = ((float) mobExp) / totalDamage;
        
        Map<Integer, Float> personalRatio = new HashMap<>();
        List<Float> entryExpRatio = new LinkedList<>();
        for (Entry<MapleCharacter, Long> e : soloExpDist.entrySet()) {
            float ratio = ((float) e.getValue()) / totalDamage;
            
            personalRatio.put(e.getKey().getId(), ratio);
            entryExpRatio.add(ratio);
        }
        
        for (Map<MapleCharacter, Long> m : partyExpDist.values()) {
            float ratio = 0.0f;
            for (Entry<MapleCharacter, Long> e : m.entrySet()) {
                float chrRatio = ((float) e.getValue()) / totalDamage;
                
                personalRatio.put(e.getKey().getId(), chrRatio);
                ratio += chrRatio;
            }
            
            entryExpRatio.add(ratio);
        }
        
        double sdevRatio = calcExperienceStandDevThreshold(entryExpRatio, totalEntries);
        
        // GMS-like player and party split calculations found thanks to Russt, KaidaTan, Dusk, AyumiLove - src: https://ayumilovemaple.wordpress.com/maplestory_calculator_formula/
        Set<MapleCharacter> underleveled = new HashSet<>();
        for (Entry<MapleCharacter, Long> chrParticipation : soloExpDist.entrySet()) {
            float exp = chrParticipation.getValue() * expPerDmg;
            MapleCharacter chr = chrParticipation.getKey();
            
            distributePlayerExperience(chr, exp, 0.0f, chr.getLevel(), true, isWhiteExpGain(chr, personalRatio, sdevRatio), false);
        }
        
        for (Map<MapleCharacter, Long> partyParticipation : partyExpDist.values()) {
            distributePartyExperience(partyParticipation, expPerDmg, underleveled, personalRatio, sdevRatio);
        }
        
        EventInstanceManager eim = getMap().getEventInstance();
        if (eim != null) {
            MapleCharacter chr = mapPlayers.get(killerId);
            if (chr != null) {
                eim.monsterKilled(chr, this);
            }
        }
        
        for(MapleCharacter mc : underleveled) {
            mc.showUnderleveledInfo(this);
        }
        
    }
    
    private float getStatusExpMultiplier(MapleCharacter attacker, boolean hasPartySharers) {
        float multiplier = 1.0f;
        
        // thanks Prophecy & Aika for finding out Holy Symbol not being applied on party bonuses
        Integer holySymbol = attacker.getBuffedValue(MapleBuffStat.HOLY_SYMBOL);
        if (holySymbol != null) {
            if (YamlConfig.config.server.USE_FULL_HOLY_SYMBOL) { // thanks Mordred, xinyifly, AyumiLove, andy33 for noticing HS hands out 20% of its potential on less than 3 players
                multiplier *= (1.0 + (holySymbol.doubleValue() / 100.0));
            } else {
                multiplier *= (1.0 + (holySymbol.doubleValue() / (hasPartySharers ? 100.0 : 500.0)));
            }
        }

        statiLock.lock();
        try {
            MonsterStatusEffect mse = stati.get(MonsterStatus.SHOWDOWN);
            if (mse != null) {
                multiplier *= (1.0 + (mse.getStati().get(MonsterStatus.SHOWDOWN).doubleValue() / 100.0));
            }
        } finally {
            statiLock.unlock();
        }
        
        return multiplier;
    }
    
    private static int expValueToInteger(double exp) {
        if (exp > Integer.MAX_VALUE) {
            exp = Integer.MAX_VALUE;
        } else if (exp < Integer.MIN_VALUE) {
            exp = Integer.MIN_VALUE;
        }
        
        return (int) Math.round(exp);    // operations on float point are not point-precise... thanks IxianMace for noticing -1 EXP gains
    }
    
    private void giveExpToCharacter(MapleCharacter attacker, Float personalExp, Float partyExp, boolean white, boolean hasPartySharers) {
        if (attacker.isAlive()) {
            if (personalExp != null) {
                personalExp *= getStatusExpMultiplier(attacker, hasPartySharers);
                personalExp *= attacker.getExpRate();
            } else {
                personalExp = 0.0f;
            }
            
            Integer expBonus = attacker.getBuffedValue(MapleBuffStat.EXP_INCREASE);
            if (expBonus != null) {     // exp increase player buff found thanks to HighKey21
                personalExp += expBonus;
            }

            int _personalExp = expValueToInteger(personalExp); // assuming no negative xp here
            
            if (partyExp != null) {
                partyExp *= getStatusExpMultiplier(attacker, hasPartySharers);
                partyExp *= attacker.getExpRate();
                partyExp *= YamlConfig.config.server.PARTY_BONUS_EXP_RATE;
            } else {
                partyExp = 0.0f;
            }
            
            int _partyExp = expValueToInteger(partyExp);
            
            attacker.gainExp(_personalExp, _partyExp, true, false, white);
            attacker.increaseEquipExp(_personalExp);
            attacker.raiseQuestMobCount(getId());
        }
    }
    
    public List<MonsterDropEntry> retrieveRelevantDrops() {
        if (this.getStats().isFriendly()) {     // thanks Conrad for noticing friendly mobs not spawning loots after a recent update
            return MapleMonsterInformationProvider.getInstance().retrieveEffectiveDrop(this.getId());
        }
        
        Map<Integer, MapleCharacter> pchars = map.getMapAllPlayers();
        
        List<MapleCharacter> lootChars = new LinkedList<>();
        for (Integer cid : takenDamage.keySet()) {
            MapleCharacter chr = pchars.get(cid);
            if (chr != null && chr.isLoggedinWorld()) {
                lootChars.add(chr);
            }
        }
        
        return MapleLootManager.retrieveRelevantDrops(this.getId(), lootChars);
    }
    
    public MapleCharacter killBy(final MapleCharacter killer) {
        distributeExperience(killer != null ? killer.getId() : 0);
        
        final Pair<MapleCharacter, Boolean> lastController = aggroRemoveController();
        final List<Integer> toSpawn = this.getRevives();
        if (toSpawn != null) {
            final MapleMap reviveMap = map;
            if (toSpawn.contains(9300216) && reviveMap.getId() > 925000000 && reviveMap.getId() < 926000000) {
                reviveMap.broadcastMessage(MaplePacketCreator.playSound("Dojang/clear"));
                reviveMap.broadcastMessage(MaplePacketCreator.showEffect("dojang/end/clear"));
            }
            Pair<Integer, String> timeMob = reviveMap.getTimeMob();
            if (timeMob != null) {
                if (toSpawn.contains(timeMob.getLeft())) {
                    reviveMap.broadcastMessage(MaplePacketCreator.serverNotice(6, timeMob.getRight()));
                }
            }
            
            if(toSpawn.size() > 0) {
                final EventInstanceManager eim = this.getMap().getEventInstance();
                
                TimerManager.getInstance().schedule(new Runnable() {
                    @Override
                    public void run() {
                        MapleCharacter controller = lastController.getLeft();
                        boolean aggro = lastController.getRight();
                        
                        for (Integer mid : toSpawn) {
                            final MapleMonster mob = MapleLifeFactory.getMonster(mid);
                            mob.setPosition(getPosition());
                            mob.setFh(getFh());
                            mob.setParentMobOid(getObjectId());
                            
                            if (dropsDisabled()) {
                                mob.disableDrops();
                            }
                            reviveMap.spawnMonster(mob);

                            if (mob.getId() >= 8810010 && mob.getId() <= 8810017 && reviveMap.isHorntailDefeated()) {
                                boolean htKilled = false;
                                MapleMonster ht = reviveMap.getMonsterById(8810018);
                                
                                if(ht != null) {
                                    ht.lockMonster();
                                    try {
                                        htKilled = ht.isAlive();
                                        ht.setHpZero();
                                    } finally {
                                        ht.unlockMonster();
                                    }
                                    
                                    if(htKilled) {
                                        reviveMap.killMonster(ht, killer, true);
                                    }
                                }
                                
                                for(int i = 8810017; i >= 8810010; i--) {
                                    reviveMap.killMonster(reviveMap.getMonsterById(i), killer, true);
                                }
                            } else if (controller != null) {
                                mob.aggroSwitchController(controller, aggro);
                            }
                            
                            if(eim != null) {
                                eim.reviveMonster(mob);
                            }
                        }
                    }
                }, 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 dropFromFriendlyMonster(long delay) {
        final MapleMonster m = this;
        monsterItemDrop = TimerManager.getInstance().register(new Runnable() {
            @Override
            public void run() {
                if (!m.isAlive()) {
                    if (monsterItemDrop != null) {
                        monsterItemDrop.cancel(false);
                    }
                    
                    return;
                }
                
                MapleMap map = m.getMap();
                List<MapleCharacter> chrList = map.getAllPlayers();
                if (!chrList.isEmpty()) {
                    MapleCharacter chr = (MapleCharacter) chrList.get(0);
                    
                    EventInstanceManager eim = map.getEventInstance();
                    if (eim != null) {
                        eim.friendlyItemDrop(m);
                    }
                    
                    map.dropFromFriendlyMonster(chr, m);
                }
            }
        }, delay, delay);
    }
    
    private void dispatchRaiseQuestMobCount() {
        Set<Integer> attackerChrids = takenDamage.keySet();
        if(!attackerChrids.isEmpty()) {
            Map<Integer, MapleCharacter> mapChars = map.getMapPlayers();
            if(!mapChars.isEmpty()) {
                int mobid = getId();
                
                for (Integer chrid : attackerChrids) {
                    MapleCharacter chr = mapChars.get(chrid);

                    if(chr != null && chr.isLoggedinWorld()) {
                        chr.raiseQuestMobCount(mobid);
                    }
                }
            }
        }
    }
    
    public void dispatchMonsterKilled(boolean hasKiller) {
        processMonsterKilled(hasKiller);
        
        EventInstanceManager eim = getMap().getEventInstance();
        if (eim != null) {
            if (!this.getStats().isFriendly()) {
                eim.monsterKilled(this, hasKiller);
            } else {
                eim.friendlyKilled(this, hasKiller);
            }
        }
    }
    
    private synchronized void processMonsterKilled(boolean hasKiller) {
        if(!hasKiller) {    // players won't gain EXP from a mob that has no killer, but a quest count they should
            dispatchRaiseQuestMobCount();
        }
        
        this.aggroClearDamages();
        this.dispatchClearSummons();
        
        MonsterListener[] listenersList;
        statiLock.lock();
        try {
            listenersList = listeners.toArray(new MonsterListener[listeners.size()]);
        } finally {
            statiLock.unlock();
        }
        
        for (MonsterListener listener : listenersList) {
            listener.monsterKilled(getAnimationTime("die1"));
        }
        
        statiLock.lock();
        try {
            stati.clear();
            alreadyBuffed.clear();
            listeners.clear();
        } finally {
            statiLock.unlock();
        }
    }
    
    private void dispatchMonsterDamaged(MapleCharacter from, int trueDmg) {
        MonsterListener[] listenersList;
        statiLock.lock();
        try {
            listenersList = listeners.toArray(new MonsterListener[listeners.size()]);
        } finally {
            statiLock.unlock();
        }
        
        for (MonsterListener listener : listenersList) {
            listener.monsterDamaged(from, trueDmg);
        }
    }
    
    private void dispatchMonsterHealed(int trueHeal) {
        MonsterListener[] listenersList;
        statiLock.lock();
        try {
            listenersList = listeners.toArray(new MonsterListener[listeners.size()]);
        } finally {
            statiLock.unlock();
        }
        
        for (MonsterListener listener : listenersList) {
            listener.monsterHealed(trueHeal);
        }
    }
    
    private void giveFamilyRep(MapleFamilyEntry entry) {
        if(entry != null) {
            int repGain = isBoss() ? YamlConfig.config.server.FAMILY_REP_PER_BOSS_KILL : YamlConfig.config.server.FAMILY_REP_PER_KILL;
            if(getMaxHp() <= 1) repGain = 0; //don't count trash mobs
            entry.giveReputationToSenior(repGain, true);
        }
    }

    public int getHighestDamagerId() {
        int curId = 0;
        long curDmg = 0;

        for (Entry<Integer, AtomicLong> 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.get() > 0;
    }
    
    public void addListener(MonsterListener listener) {
        statiLock.lock();
        try {
            listeners.add(listener);
        } finally {
            statiLock.unlock();
        }
    }

    public MapleCharacter getController() {
        return controller.get();
    }

    private void setController(MapleCharacter controller) {
        this.controller = new WeakReference<>(controller);
    }
    
    public boolean isControllerHasAggro() {
        return fake ? false : controllerHasAggro;
    }

    private void setControllerHasAggro(boolean controllerHasAggro) {
        if (!fake) {
            this.controllerHasAggro = controllerHasAggro;
        }
    }

    public boolean isControllerKnowsAboutAggro() {
        return fake ? false : controllerKnowsAboutAggro;
    }

    private void setControllerKnowsAboutAggro(boolean controllerKnowsAboutAggro) {
        if (!fake) {
            this.controllerKnowsAboutAggro = controllerKnowsAboutAggro;
        }
    }
    
    private void setControllerHasPuppet(boolean controllerHasPuppet) {
        this.controllerHasPuppet = controllerHasPuppet;
    }

    public byte[] makeBossHPBarPacket() {
        return MaplePacketCreator.showBossHP(getId(), getHp(), getMaxHp(), getTagColor(), getTagBgColor());
    }

    public boolean hasBossHPBar() {
        return isBoss() && getTagColor() > 0;
    }
    
    @Override
    public void sendSpawnData(MapleClient client) {
        if (hp.get() <= 0) { // mustn't monsterLock this function
            return;
        }
        if (fake) {
            client.announce(MaplePacketCreator.spawnFakeMonster(this, 0));
        } else {
            client.announce(MaplePacketCreator.spawnMonster(this, false));
        }
        
        if (hasBossHPBar()) {
            client.announceBossHpBar(this, this.hashCode(), makeBossHPBarPacket());
        }
    }

    @Override
    public void sendDestroyData(MapleClient client) {
        client.announce(MaplePacketCreator.killMonster(getObjectId(), false));
        client.announce(MaplePacketCreator.killMonster(getObjectId(), true));
    }

    @Override
    public MapleMapObjectType getType() {
        return MapleMapObjectType.MONSTER;
    }

    public boolean isMobile() {
        return stats.isMobile();
    }

    @Override
    public boolean isFacingLeft() {
        int fixedStance = stats.getFixedStance();    // thanks DimDiDima for noticing inconsistency on some AOE mobskills
        if (fixedStance != 0) {
            return Math.abs(fixedStance) % 2 == 1;
        }
        
        return super.isFacingLeft();
    }
    
    public ElementalEffectiveness getElementalEffectiveness(Element e) {
        statiLock.lock();
        try {
            if (stati.get(MonsterStatus.DOOM) != null) {
                return ElementalEffectiveness.NORMAL; // like blue snails
            }
        } finally {
            statiLock.unlock();
        }
        
        return getMonsterEffectiveness(e);
    }
    
    private ElementalEffectiveness getMonsterEffectiveness(Element e) {
        monsterLock.lock();
        try {
            return stats.getEffectiveness(e);
        } finally {
            monsterLock.unlock();
        }
    }

    private MapleCharacter getActiveController() {
        MapleCharacter chr = getController();
        
        if (chr != null && chr.isLoggedinWorld() && chr.getMap() == this.getMap()) {
            return chr;
        } else {
            return null;
        }
    }
    
    private void broadcastMonsterStatusMessage(byte[] packet) {
        map.broadcastMessage(packet, getPosition());
        
        MapleCharacter chrController = getActiveController();
        if (chrController != null && !chrController.isMapObjectVisible(MapleMonster.this)) {
            chrController.announce(packet);
        }
    }
    
    private int broadcastStatusEffect(final MonsterStatusEffect status) {
        int animationTime = status.getSkill().getAnimationTime();
        byte[] packet = MaplePacketCreator.applyMonsterStatus(getObjectId(), status, null);
        broadcastMonsterStatusMessage(packet);
        
        return animationTime;
    }
    
    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 (getMonsterEffectiveness(status.getSkill().getElement())) {
            case IMMUNE:
            case STRONG:
            case NEUTRAL:
                return false;
            case NORMAL:
            case WEAK:
                break;
            default: {
                System.out.println("Unknown elemental effectiveness: " + getMonsterEffectiveness(status.getSkill().getElement()));
                return false;
            }
        }

        if (status.getSkill().getId() == FPMage.ELEMENT_COMPOSITION) { // fp compo
            ElementalEffectiveness effectiveness = getMonsterEffectiveness(Element.POISON);
            if (effectiveness == ElementalEffectiveness.IMMUNE || effectiveness == ElementalEffectiveness.STRONG) {
                return false;
            }
        } else if (status.getSkill().getId() == ILMage.ELEMENT_COMPOSITION) { // il compo
            ElementalEffectiveness effectiveness = getMonsterEffectiveness(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 (getMonsterEffectiveness(Element.POISON) == ElementalEffectiveness.WEAK) {
                return false;
            }
        }
        if (poison && hp.get() <= 1) {
            return false;
        }

        final Map<MonsterStatus, Integer> statis = status.getStati();
        if (stats.isBoss()) {
            if (!(statis.containsKey(MonsterStatus.SPEED)
                    && statis.containsKey(MonsterStatus.NINJA_AMBUSH)
                    && statis.containsKey(MonsterStatus.WATK))) {
                return false;
            }
        }

        final Channel ch = map.getChannelServer();
        final int mapid = map.getId();
        if(statis.size() > 0) {
            statiLock.lock();
            try {
                for (MonsterStatus stat : statis.keySet()) {
                    final MonsterStatusEffect oldEffect = stati.get(stat);
                    if (oldEffect != null) {
                        oldEffect.removeActiveStatus(stat);
                        if (oldEffect.getStati().isEmpty()) {
                            MobStatusService service = (MobStatusService) map.getChannelServer().getServiceAccess(ChannelServices.MOB_STATUS);
                            service.interruptMobStatus(mapid, oldEffect);
                        }
                    }
                }
            } finally {
                statiLock.unlock();
            }
        }
        
        final Runnable cancelTask = new Runnable() {

            @Override
            public void run() {
                if (isAlive()) {
                    byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), status.getStati());
                    broadcastMonsterStatusMessage(packet);
                }
                
                statiLock.lock();
                try {
                    for (MonsterStatus stat : status.getStati().keySet()) {
                        stati.remove(stat);
                    }
                } finally {
                    statiLock.unlock();
                }
                
                setVenomMulti(0);
            }
        };
        
        Runnable overtimeAction = null;
        int overtimeDelay = -1;
        
        int animationTime;
        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));
            animationTime = broadcastStatusEffect(status);
            
            overtimeAction = new DamageTask(poisonDamage, from, status, 0);
            overtimeDelay = 1000;
        } else if (venom) {
            if (from.getJob() == MapleJob.NIGHTLORD || from.getJob() == MapleJob.SHADOWER || from.getJob().isA(MapleJob.NIGHTWALKER3)) {
                int poisonLevel, matk, jobid = from.getJob().getId();
                int skillid = (jobid == 412 ? NightLord.VENOMOUS_STAR : (jobid == 422 ? Shadower.VENOMOUS_STAB : NightWalker.VENOM));
                poisonLevel = from.getSkillLevel(SkillFactory.getSkill(skillid));
                if (poisonLevel <= 0) {
                    return false;
                }
                matk = SkillFactory.getSkill(skillid).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.setValue(MonsterStatus.POISON, Integer.valueOf(poisonDamage));
                animationTime = broadcastStatusEffect(status);
                
                overtimeAction = new DamageTask(poisonDamage, from, status, 0);
                overtimeDelay = 1000;
            } else {
                return false;
            }
            /*
        } else if (status.getSkill().getId() == Hermit.SHADOW_WEB || status.getSkill().getId() == NightWalker.SHADOW_WEB) { //Shadow Web
            int webDamage = (int) (getMaxHp() / 50.0 + 0.999);
            status.setValue(MonsterStatus.SHADOW_WEB, Integer.valueOf(webDamage));
            animationTime = broadcastStatusEffect(status);
            
            overtimeAction = new DamageTask(webDamage, from, status, 1);
            overtimeDelay = 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()) * ((3.7 * skill.getEffect(level).getDamage()) / 100));
            
            status.setValue(MonsterStatus.NINJA_AMBUSH, Integer.valueOf(damage));
            animationTime = broadcastStatusEffect(status);
            
            overtimeAction = new DamageTask(damage, from, status, 2);
            overtimeDelay = 1000;
        } else {
            animationTime = broadcastStatusEffect(status);
        }
        
        statiLock.lock();
        try {
            for (MonsterStatus stat : status.getStati().keySet()) {
                stati.put(stat, status);
                alreadyBuffed.add(stat);
            }
        } finally {
            statiLock.unlock();
        }
        
        MobStatusService service = (MobStatusService) map.getChannelServer().getServiceAccess(ChannelServices.MOB_STATUS);
        service.registerMobStatus(mapid, status, cancelTask, duration + animationTime - 100, overtimeAction, overtimeDelay);
        return true;
    }
    
    public final void dispelSkill(final MobSkill skillId) {
        List<MonsterStatus> toCancel = new ArrayList<MonsterStatus>();
        for (Entry<MonsterStatus, MonsterStatusEffect> effects : stati.entrySet()) {
            MonsterStatusEffect mse = effects.getValue();
            if (mse.getMobSkill() != null && mse.getMobSkill().getSkillId() == skillId.getSkillId()) { //not checking for level.
                toCancel.add(effects.getKey());
            }
        }
        for (MonsterStatus stat : toCancel) {
            debuffMobStat(stat);
        }
    }
    
    public void applyMonsterBuff(final Map<MonsterStatus, Integer> stats, final int x, int skillId, long duration, MobSkill skill, final List<Integer> reflection) {
        final Runnable cancelTask = new Runnable() {

            @Override
            public void run() {
                if (isAlive()) {
                    byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), stats);
                    broadcastMonsterStatusMessage(packet);
                    
                    statiLock.lock();
                    try {
                        for (final MonsterStatus stat : stats.keySet()) {
                            stati.remove(stat);
                        }
                    } finally {
                        statiLock.unlock();
                    }
                }
            }
        };
        final MonsterStatusEffect effect = new MonsterStatusEffect(stats, null, skill, true);
        byte[] packet = MaplePacketCreator.applyMonsterStatus(getObjectId(), effect, reflection);
        broadcastMonsterStatusMessage(packet);
        
        statiLock.lock();
        try {
            for (MonsterStatus stat : stats.keySet()) {
                stati.put(stat, effect);
                alreadyBuffed.add(stat);
            }
        } finally {
            statiLock.unlock();
        }
        
        MobStatusService service = (MobStatusService) map.getChannelServer().getServiceAccess(ChannelServices.MOB_STATUS);
        service.registerMobStatus(map.getId(), effect, cancelTask, duration);
    }
    
    public void refreshMobPosition() {
        resetMobPosition(getPosition());
    }
    
    public void resetMobPosition(Point newPoint) {
        aggroRemoveController();
        
        setPosition(newPoint);
        map.broadcastMessage(MaplePacketCreator.moveMonster(this.getObjectId(), false, -1, 0, 0, 0, this.getPosition(), this.getIdleMovement(), getIdleMovementDataLength()));
        map.moveMonster(this, this.getPosition());
        
        aggroUpdateController();
    }

    private void debuffMobStat(MonsterStatus stat) {
        MonsterStatusEffect oldEffect;
        statiLock.lock();
        try {
            oldEffect = stati.remove(stat);
        } finally {
            statiLock.unlock();
        }
        
        if (oldEffect != null) {
            byte[] packet = MaplePacketCreator.cancelMonsterStatus(getObjectId(), oldEffect.getStati());
            broadcastMonsterStatusMessage(packet);
        }
    }
    
    public void debuffMob(int skillid) {
        MonsterStatus[] statups = {MonsterStatus.WEAPON_ATTACK_UP, MonsterStatus.WEAPON_DEFENSE_UP, MonsterStatus.MAGIC_ATTACK_UP, MonsterStatus.MAGIC_DEFENSE_UP};
        statiLock.lock();
        try {
            if(skillid == Hermit.SHADOW_MESO) {
                debuffMobStat(statups[1]);
                debuffMobStat(statups[3]);
            } else if(skillid == Priest.DISPEL) {
                for(MonsterStatus ms : statups) {
                    debuffMobStat(ms);
                }
            } else {    // is a crash skill
                int i = (skillid == Crusader.ARMOR_CRASH ? 1 : (skillid == WhiteKnight.MAGIC_CRASH ? 2 : 0));
                debuffMobStat(statups[i]);

                if(YamlConfig.config.server.USE_ANTI_IMMUNITY_CRASH) {
                    if (skillid == Crusader.ARMOR_CRASH) {
                        if(!isBuffed(MonsterStatus.WEAPON_REFLECT)) {
                            debuffMobStat(MonsterStatus.WEAPON_IMMUNITY);
                        }
                        if(!isBuffed(MonsterStatus.MAGIC_REFLECT)) {
                            debuffMobStat(MonsterStatus.MAGIC_IMMUNITY);
                        }
                    } else if (skillid == WhiteKnight.MAGIC_CRASH) {
                        if(!isBuffed(MonsterStatus.MAGIC_REFLECT)) {
                            debuffMobStat(MonsterStatus.MAGIC_IMMUNITY);
                        }
                    } else {
                        if(!isBuffed(MonsterStatus.WEAPON_REFLECT)) {
                            debuffMobStat(MonsterStatus.WEAPON_IMMUNITY);
                        }
                    }
                }
            }
        } finally {
            statiLock.unlock();
        }
    }

    public boolean isBuffed(MonsterStatus status) {
        statiLock.lock();
        try {
            return stati.containsKey(status);
        } finally {
            statiLock.unlock();
        }
    }

    public void setFake(boolean fake) {
        monsterLock.lock();
        try {
            this.fake = fake;
        } finally {
            monsterLock.unlock();
        }
    }

    public boolean isFake() {
        monsterLock.lock();
        try {
            return fake;
        } finally {
            monsterLock.unlock();
        }
    }

    public MapleMap getMap() {
        return map;
    }
    
    public MapleMonsterAggroCoordinator getMapAggroCoordinator() {
        return map.getAggroCoordinator();
    }
    
    public List<Pair<Integer, Integer>> getSkills() {
        return stats.getSkills();
    }

    public boolean hasSkill(int skillId, int 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) {
        if (toUse == null) {
            return false;
        }
        
        int useSkillid = toUse.getSkillId();
        if (useSkillid >= 143 && useSkillid <= 145) {
            if (this.isBuffed(MonsterStatus.WEAPON_REFLECT) || this.isBuffed(MonsterStatus.MAGIC_REFLECT)) {
                return false;
            }
        }
        
        monsterLock.lock();
        try {
            for (Pair<Integer, Integer> skill : usedSkills) {   // thanks OishiiKawaiiDesu for noticing an issue with mobskill cooldown
                if (skill.getLeft() == useSkillid && skill.getRight() == toUse.getSkillLevel()) {
                    return false;
                }
            }
            
            int mpCon = toUse.getMpCon();
            if (mp < mpCon) {
                return false;
            }
            
            /*
            if (!this.applyAnimationIfRoaming(-1, toUse)) {
                return false;
            }
            */
            
            if (apply) {
                this.usedSkill(toUse);
            }
        } finally {
            monsterLock.unlock();
        }
        
        return true;
    }

    private void usedSkill(MobSkill skill) {
        final int skillId = skill.getSkillId(), level = skill.getSkillLevel();
        long cooltime = skill.getCoolTime();
        
        monsterLock.lock();
        try {
            mp -= skill.getMpCon();
            
            Pair<Integer, Integer> skillKey = new Pair<>(skillId, level);
            this.usedSkills.add(skillKey);
            
            Integer useCount = this.skillsUsed.remove(skillKey);
            if (useCount != null) {
                this.skillsUsed.put(skillKey, useCount + 1);
            } else {
                this.skillsUsed.put(skillKey, 1);
            }
        } finally {
            monsterLock.unlock();
        }
        
        final MapleMonster mons = this;
        MapleMap mmap = mons.getMap();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                mons.clearSkill(skillId, level);
            }
        };
        
        MobClearSkillService service = (MobClearSkillService) map.getChannelServer().getServiceAccess(ChannelServices.MOB_CLEAR_SKILL);
        service.registerMobClearSkillAction(mmap.getId(), r, cooltime);
    }

    private void clearSkill(int skillId, int level) {
        monsterLock.lock();
        try {
            int index = -1;
            for (Pair<Integer, Integer> skill : usedSkills) {
                if (skill.getLeft() == skillId && skill.getRight() == level) {
                    index = usedSkills.indexOf(skill);
                    break;
                }
            }
            if (index != -1) {
                usedSkills.remove(index);
            }
        } finally {
            monsterLock.unlock();
        }
    }
    
    public int canUseAttack(int attackPos, boolean isSkill) {
        monsterLock.lock();
        try {
            /*
            if (usedAttacks.contains(attackPos)) {
                return -1;
            }
            */
            
            Pair<Integer, Integer> attackInfo = MapleMonsterInformationProvider.getInstance().getMobAttackInfo(this.getId(), attackPos);
            if (attackInfo == null) {
                return -1;
            }
            
            int mpCon = attackInfo.getLeft();
            if (mp < mpCon) {
                return -1;
            }
            
            /*
            if (!this.applyAnimationIfRoaming(attackPos, null)) {
                return -1;
            }
            */
            
            usedAttack(attackPos, mpCon, attackInfo.getRight());
            return 1;
        } finally {
            monsterLock.unlock();
        }
    }
    
    private void usedAttack(final int attackPos, int mpCon, int cooltime) {
        monsterLock.lock();
        try {
            mp -= mpCon;
            usedAttacks.add(attackPos);

            final MapleMonster mons = this;
            MapleMap mmap = mons.getMap();
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    mons.clearAttack(attackPos);
                }
            };
            
            MobClearSkillService service = (MobClearSkillService) map.getChannelServer().getServiceAccess(ChannelServices.MOB_CLEAR_SKILL);
            service.registerMobClearSkillAction(mmap.getId(), r, cooltime);
        } finally {
            monsterLock.unlock();
        }
    }
    
    private void clearAttack(int attackPos) {
        monsterLock.lock();
        try {
            usedAttacks.remove(attackPos);
        } finally {
            monsterLock.unlock();
        }
    }
    
    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 int type;
        private final MapleMap map;

        private DamageTask(int dealDamage, MapleCharacter chr, MonsterStatusEffect status, int type) {
            this.dealDamage = dealDamage;
            this.chr = chr;
            this.status = status;
            this.type = type;
            this.map = chr.getMap();
        }

        @Override
        public void run() {
            int curHp = hp.get();
            if(curHp <= 1) {
                MobStatusService service = (MobStatusService) map.getChannelServer().getServiceAccess(ChannelServices.MOB_STATUS);
                service.interruptMobStatus(map.getId(), status);
                return;
            }
            
            int damage = dealDamage;
            if (damage >= curHp) {
                damage = curHp - 1;
                if (type == 1 || type == 2) {
                    MobStatusService service = (MobStatusService) map.getChannelServer().getServiceAccess(ChannelServices.MOB_STATUS);
                    service.interruptMobStatus(map.getId(), status);
                }
            }
            if (damage > 0) {
                lockMonster();
                try {
                    applyDamage(chr, damage, true, false);
                } finally {
                    unlockMonster();
                }
                
                if (type == 1) {
                    map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), damage), getPosition());
                } else if (type == 2) {
                    if(damage < dealDamage) {    // ninja ambush (type 2) is already displaying DOT to the caster
                        map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), damage), getPosition());
                    }
                }
            }
        }
    }

    public String getName() {
        return stats.getName();
    }

    public void addStolen(int itemId) {
        stolenItems.add(itemId);
    }

    public List<Integer> getStolen() {
        return stolenItems;
    }

    public void setTempEffectiveness(Element e, ElementalEffectiveness ee, long milli) {
        monsterLock.lock();
        try {
            final Element fE = e;
            final ElementalEffectiveness fEE = stats.getEffectiveness(e);
            if (!fEE.equals(ElementalEffectiveness.WEAK)) {
                stats.setEffectiveness(e, ee);
                
                MapleMap mmap = this.getMap();
                Runnable r = new Runnable() {
                    @Override
                    public void run() {
                        monsterLock.lock();
                        try {
                            stats.removeEffectiveness(fE);
                            stats.setEffectiveness(fE, fEE);
                        } finally {
                            monsterLock.unlock();
                        }
                    }
                };
                
                MobClearSkillService service = (MobClearSkillService) mmap.getChannelServer().getServiceAccess(ChannelServices.MOB_CLEAR_SKILL);
                service.registerMobClearSkillAction(mmap.getId(), r, milli);
            }
        } finally {
            monsterLock.unlock();
        }
    }

    public Collection<MonsterStatus> alreadyBuffedStats() {
        statiLock.lock();
        try {
            return Collections.unmodifiableCollection(alreadyBuffed);
        } finally {
            statiLock.unlock();
        }
    }

    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<MonsterStatus, MonsterStatusEffect> getStati() {
        statiLock.lock();
        try {
            return new HashMap<>(stati);
        } finally {
            statiLock.unlock();
        }
    }
    
    public MonsterStatusEffect getStati(MonsterStatus ms) {
        statiLock.lock();
        try {
            return stati.get(ms);
        } finally {
            statiLock.unlock();
        }
    }
    
    // ---- 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.set(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.set(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);
    }
    
    // ---------------------------------------------------------------------------------
    
    private boolean isPuppetInVicinity(MapleSummon summon) {
        return summon.getPosition().distanceSq(this.getPosition()) < 177777;
    }
    
    public boolean isCharacterPuppetInVicinity(MapleCharacter chr) {
        MapleStatEffect mse = chr.getBuffEffect(MapleBuffStat.PUPPET);
        if (mse != null) {
            MapleSummon summon = chr.getSummonByKey(mse.getSourceId());

            // check whether mob is currently under a puppet's field of action or not
            if (summon != null) {
                if (isPuppetInVicinity(summon)) {
                    return true;
                }
            } else {
                map.getAggroCoordinator().removePuppetAggro(chr.getId());
            }
        }
        
        return false;
    }
    
    public boolean isLeadingPuppetInVicinity() {
        MapleCharacter chrController = this.getActiveController();
        
        if (chrController != null) {
            if (this.isCharacterPuppetInVicinity(chrController)) {
                return true;
            }
        }
        
        return false;
    }
    
    private MapleCharacter getNextControllerCandidate() {
        int mincontrolled = Integer.MAX_VALUE;
        MapleCharacter newController = null;
        
        int mincontrolleddead = Integer.MAX_VALUE;
        MapleCharacter newControllerDead = null;
        
        MapleCharacter newControllerWithPuppet = null;
        
        for (MapleCharacter chr : getMap().getAllPlayers()) {
            if (!chr.isHidden()) {
                int ctrlMonsSize = chr.getNumControlledMonsters();

                if (isCharacterPuppetInVicinity(chr)) {
                    newControllerWithPuppet = chr;
                    break;
                } else if (chr.isAlive()) {
                    if (ctrlMonsSize < mincontrolled) {
                        mincontrolled = ctrlMonsSize;
                        newController = chr;
                    }
                } else {
                    if (ctrlMonsSize < mincontrolleddead) {
                        mincontrolleddead = ctrlMonsSize;
                        newControllerDead = chr;
                    }
                }
            }
        }
        
        if (newControllerWithPuppet != null) {
            return newControllerWithPuppet;
        } else if (newController != null) {
            return newController;
        } else {
            return newControllerDead;
        }
    }
    
    /**
     * Removes controllability status from the current controller of this mob.
     * 
     */
    public Pair<MapleCharacter, Boolean> aggroRemoveController() {
        MapleCharacter chrController;
        boolean hadAggro;
        
        aggroUpdateLock.lock();
        try {
            chrController = getActiveController();
            hadAggro = isControllerHasAggro();
            
            this.setController(null);
            this.setControllerHasAggro(false);
            this.setControllerKnowsAboutAggro(false);
        } finally {
            aggroUpdateLock.unlock();
        }
        
        if (chrController != null) { // this can/should only happen when a hidden gm attacks the monster
            if (!this.isFake()) chrController.announce(MaplePacketCreator.stopControllingMonster(this.getObjectId()));
            chrController.stopControllingMonster(this);
        }
        
        return new Pair<>(chrController, hadAggro);
    }
    
    /**
     * Pass over the mob controllability and updates aggro status on the new
     * player controller.
     * 
     */
    public void aggroSwitchController(MapleCharacter newController, boolean immediateAggro) {
        if (aggroUpdateLock.tryLock()) {
            try {
                MapleCharacter prevController = getController();
                if (prevController == newController) {
                    return;
                }
                
                aggroRemoveController();
                if (!(newController != null && newController.isLoggedinWorld() && newController.getMap() == this.getMap())) {
                    return;
                }
                
                this.setController(newController);
                this.setControllerHasAggro(immediateAggro);
                this.setControllerKnowsAboutAggro(false);
                this.setControllerHasPuppet(false);
            } finally {
                aggroUpdateLock.unlock();
            }
            
            this.aggroUpdatePuppetVisibility();
            aggroMonsterControl(newController.getClient(), this, immediateAggro);
            newController.controlMonster(this);
        }
    }
    
    public void aggroAddPuppet(MapleCharacter player) {
        MapleMonsterAggroCoordinator mmac = map.getAggroCoordinator();
        mmac.addPuppetAggro(player);
        
        aggroUpdatePuppetController(player);
        
        if (this.isControllerHasAggro()) {
            this.aggroUpdatePuppetVisibility();
        }
    }
    
    public void aggroRemovePuppet(MapleCharacter player) {
        MapleMonsterAggroCoordinator mmac = map.getAggroCoordinator();
        mmac.removePuppetAggro(player.getId());
        
        aggroUpdatePuppetController(null);
        
        if (this.isControllerHasAggro()) {
            this.aggroUpdatePuppetVisibility();
        }
    }
    
    /**
     * Automagically finds a new controller for the given monster from the chars
     * on the map it is from...
     * 
     */
    public void aggroUpdateController() {
        MapleCharacter chrController = this.getActiveController();
        if (chrController != null && chrController.isAlive()) {
            return;
        }
        
        MapleCharacter newController = getNextControllerCandidate();
        if (newController == null) {    // was a new controller found? (if not no one is on the map)
            return;
        }
        
        this.aggroSwitchController(newController, false);
    }
    
    /**
     * Finds a new controller for the given monster from the chars with deployed
     * puppet nearby on the map it is from...
     * 
     */
    private void aggroUpdatePuppetController(MapleCharacter newController) {
        MapleCharacter chrController = this.getActiveController();
        boolean updateController = false;
        
        if (chrController != null && chrController.isAlive()) {
            if (isCharacterPuppetInVicinity(chrController)) {
                return;
            }
        } else {
            updateController = true;
        }
        
        if (newController == null || !isCharacterPuppetInVicinity(newController)) {
            MapleMonsterAggroCoordinator mmac = map.getAggroCoordinator();
            
            List<Integer> puppetOwners = mmac.getPuppetAggroList();
            List<Integer> toRemovePuppets = new LinkedList<>();
        
            for (Integer cid : puppetOwners) {
                MapleCharacter chr = map.getCharacterById(cid);

                if (chr != null) {
                    if (isCharacterPuppetInVicinity(chr)) {
                        newController = chr;
                        break;
                    }
                } else {
                    toRemovePuppets.add(cid);
                }
            }

            for (Integer cid : toRemovePuppets) {
                mmac.removePuppetAggro(cid);
            }
            
            if (newController == null) {    // was a new controller found? (if not there's no puppet nearby)
                if (updateController) {
                    aggroUpdateController();
                }
                
                return;
            }
        } else if (chrController == newController) {
            this.aggroUpdatePuppetVisibility();
        }
        
        this.aggroSwitchController(newController, this.isControllerHasAggro());
    }
    
    /**
     * Ensures controllability removal of the current player controller, and
     * fetches for any player on the map to start controlling in place.
     * 
     */
    public void aggroRedirectController() {
        this.aggroRemoveController();   // don't care if new controller not found, at least remove current controller
        this.aggroUpdateController();
    }
    
    /**
     * Returns the current aggro status on the specified player, or null if the
     * specified player is currently not this mob's controller.
     * 
     */
    public Boolean aggroMoveLifeUpdate(MapleCharacter player) {
        MapleCharacter chrController = getController();
        if (chrController != null && player.getId() == chrController.getId()) {
            boolean aggro = this.isControllerHasAggro();
            if (aggro) {
                this.setControllerKnowsAboutAggro(true);
            }
            
            return aggro;
        } else {
            return null;
        }
    }
    
    /**
     * Refreshes auto aggro for the player passed as parameter, does nothing if
     * there is already an active controller for this mob.
     * 
     */
    public void aggroAutoAggroUpdate(MapleCharacter player) {
        MapleCharacter chrController = this.getActiveController();
        
        if (chrController == null) {
            this.aggroSwitchController(player, true);
        } else if (chrController.getId() == player.getId()) {
            this.setControllerHasAggro(true);
            if (!YamlConfig.config.server.USE_AUTOAGGRO_NEARBY) {   // thanks Lichtmager for noticing autoaggro not updating the player properly
                aggroMonsterControl(player.getClient(), this, true);
            }
        }
    }
    
    /**
     * Applied damage input for this mob, enough damage taken implies an aggro
     * target update for the attacker shortly.
     * 
     */
    public void aggroMonsterDamage(MapleCharacter attacker, int damage) {
        MapleMonsterAggroCoordinator mmac = this.getMapAggroCoordinator();
        mmac.addAggroDamage(this, attacker.getId(), damage);
        
        MapleCharacter chrController = this.getController();    // aggro based on DPS rather than first-come-first-served, now live after suggestions thanks to MedicOP, Thora, Vcoc
        if (chrController != attacker) {
            if (this.getMapAggroCoordinator().isLeadingCharacterAggro(this, attacker)) {
                this.aggroSwitchController(attacker, true);
            } else {
                this.setControllerHasAggro(true);
                this.aggroUpdatePuppetVisibility();
            }
            
            /*
            For some reason, some mobs loses aggro on controllers if other players also attacks them.
            Maybe Nexon intended to interchange controllers at every attack...
            
            else if (chrController != null) {
                chrController.announce(MaplePacketCreator.stopControllingMonster(this.getObjectId()));
                aggroMonsterControl(chrController.getClient(), this, true);
            }
            */
        } else {
            this.setControllerHasAggro(true);
            this.aggroUpdatePuppetVisibility();
        }
    }
    
    private static void aggroMonsterControl(MapleClient c, MapleMonster mob, boolean immediateAggro) {
        c.announce(MaplePacketCreator.controlMonster(mob, false, immediateAggro));
    }
    
    private void aggroRefreshPuppetVisibility(MapleCharacter chrController, MapleSummon puppet) {
        // lame patch for client to redirect all aggro to the puppet
        
        List<MapleMonster> puppetControlled = new LinkedList<>();
        for (MapleMonster mob : chrController.getControlledMonsters()) {
            if (mob.isPuppetInVicinity(puppet)) {
                puppetControlled.add(mob);
            }
        }
        
        for (MapleMonster mob : puppetControlled) {
            chrController.announce(MaplePacketCreator.stopControllingMonster(mob.getObjectId()));
        }
        chrController.announce(MaplePacketCreator.removeSummon(puppet, false));
        
        MapleClient c = chrController.getClient();
        for (MapleMonster mob : puppetControlled) { // thanks BHB for noticing puppets disrupting mobstatuses for bowmans
            aggroMonsterControl(c, mob, mob.isControllerKnowsAboutAggro());
        }
        chrController.announce(MaplePacketCreator.spawnSummon(puppet, false));
    }
    
    public void aggroUpdatePuppetVisibility() {
        if (!availablePuppetUpdate) {
            return;
        }
        
        availablePuppetUpdate = false;
        Runnable r = new Runnable() {
            @Override
            public void run() {
                try {
                    MapleCharacter chrController = MapleMonster.this.getActiveController();
                    if (chrController == null) {
                        return;
                    }

                    MapleStatEffect puppetEffect = chrController.getBuffEffect(MapleBuffStat.PUPPET);
                    if (puppetEffect != null) {
                        MapleSummon puppet = chrController.getSummonByKey(puppetEffect.getSourceId());

                        if (puppet != null && isPuppetInVicinity(puppet)) {
                            controllerHasPuppet = true;
                            aggroRefreshPuppetVisibility(chrController, puppet);
                            return;
                        }
                    }

                    if (controllerHasPuppet) {
                        controllerHasPuppet = false;

                        chrController.announce(MaplePacketCreator.stopControllingMonster(MapleMonster.this.getObjectId()));
                        aggroMonsterControl(chrController.getClient(), MapleMonster.this, MapleMonster.this.isControllerHasAggro());
                    }
                } finally {
                    availablePuppetUpdate = true;
                }
            }
        };
        
        // had to schedule this since mob wouldn't stick to puppet aggro who knows why
        OverallService service = (OverallService) this.getMap().getChannelServer().getServiceAccess(ChannelServices.OVERALL);
        service.registerOverallAction(this.getMap().getId(), r, YamlConfig.config.server.UPDATE_INTERVAL);
    }
    
    /**
     * Clears all applied damage input for this mob, doesn't refresh target
     * aggro.
     * 
     */
    public void aggroClearDamages() {
        this.getMapAggroCoordinator().removeAggroEntries(this);
    }

    /**
     * Clears this mob aggro on the current controller.
     * 
     */
    public void aggroResetAggro() {
        aggroUpdateLock.lock();
        try {
            this.setControllerHasAggro(false);
            this.setControllerKnowsAboutAggro(false);
        } finally {
            aggroUpdateLock.unlock();
        }
    }
    
    public final int getRemoveAfter() {
        return stats.removeAfter();
    }
    
    public void dispose() {
        if (monsterItemDrop != null) {
            monsterItemDrop.cancel(false);
        }
        
        this.getMap().dismissRemoveAfter(this);
        disposeLocks();
    }
    
    private void disposeLocks() {
        LockCollector.getInstance().registerDisposeAction(new Runnable() {
            @Override
            public void run() {
                emptyLocks();
            }
        });
    }
    
    private void emptyLocks() {
        externalLock = externalLock.dispose();
        monsterLock = monsterLock.dispose();
        statiLock = statiLock.dispose();
        animationLock = animationLock.dispose();
    }
}
 
Newbie Spellweaver
Joined
Aug 28, 2009
Messages
20
Reaction score
1
EXP_SPLIT_LEECH_INTERVAL and EXP_SPLIT_LEVEL_INTERVAL aren't functions, these are config "constants". A function has a return type or void, a name, and () or (parameters), and a block of code that executes when it is called, and is called by functionName();

These all caps things are constants, but they're read in through the configuration and set at start up, and are thus accessed via the YamlConfig.

Code:
    EXP_SPLIT_LEVEL_INTERVAL: 5            #Non-contributing players must be within N level between the mob to receive EXP.
    EXP_SPLIT_LEECH_INTERVAL: 5            #Non-contributing players must be within N level between any contributing party member to receive EXP.

My GOAL is so that it's "gms like" and you can only leech from a mob if you are at LEAST 5 levels below it or greater. Not dependent on party member's level, regardless. Can anyone help me with this?

To clarify, what you're saying here means that if I'm 4 levels below, I won't get any exp?

Your logic translates to:

if myLevel <= (monsterLevel - 5) then gainExp

Edit:

Code:
        if (YamlConfig.config.server.USE_ENFORCE_MOB_LEVEL_RANGE) {
            for (MapleCharacter member : partyParticipation.keySet().iterator().next().getPartyMembersOnSameMap()) {
                if (!leechInterval.inInterval(member.getLevel())) {
                    underleveled.add(member);
                    continue;
                }

                totalPartyLevel += member.getLevel();
                expMembers.add(member);
            }
        } else {    // thanks Ari for noticing unused server flag after EXP system overhaul
            for (MapleCharacter member : partyParticipation.keySet().iterator().next().getPartyMembersOnSameMap()) {
                totalPartyLevel += member.getLevel();
                expMembers.add(member);
            }
        }

See this part of the distributePartyExperience function. If you set:
USE_ENFORCE_MOB_LEVEL_RANGE = false
in the config, mob level will not have any impact on whether exp is not gained - exp will be gained no matter what the level of the mob is. The leechInterval is not used if this is set to false.

Consider adding
Code:
EXP_MOB_MIN_LEVEL_DIFF = 5

To your configuration file, and add this as an int to the ServerConfig class.

Then update to:

Code:
        if (YamlConfig.config.server.USE_ENFORCE_MOB_LEVEL_RANGE) {
            for (MapleCharacter member : partyParticipation.keySet().iterator().next().getPartyMembersOnSameMap()) {
                if (!leechInterval.inInterval(member.getLevel())) {
                    underleveled.add(member);
                    continue;
                }

                totalPartyLevel += member.getLevel();
                expMembers.add(member);
            }
        } else {    // thanks Ari for noticing unused server flag after EXP system overhaul
            for (MapleCharacter member : partyParticipation.keySet().iterator().next().getPartyMembersOnSameMap()) {
                if (member.getLevel() <= this.getLevel() - YamlConfig.config.server.EXP_MOB_MIN_LEVEL_DIFF) {
                    totalPartyLevel += member.getLevel();
                    expMembers.add(member);
                }
            }
        }

So this updated code snippet will only add the player as an exp member if: memberLevel <= mobLevel - EXP_MOB_MIN_DIFF

I would also advise using USE_MOB_MIN_LEVEL_DIFF as a boolean, set to true in config, add it to ServerConfig class, and add the extra logic as an else if check.
 
Last edited:
Upvote 0
Newbie Spellweaver
Joined
Mar 16, 2011
Messages
14
Reaction score
0
Nice. It seems to work just fine as long as you get rid of the split leech interval out of the config.yaml. Thank you very much!
If you wouldn't mind, (or anyone else viewing the thread,) would you be willing to help me with any small random questions I have? I would appreciate it if anyone would hit me up on discord @ Robby#8084 .... But again, thanks so much for the help!
 
Upvote 0
Junior Spellweaver
Joined
Sep 20, 2019
Messages
108
Reaction score
8
Excuse me but i tried to do the same and seems like i'm missing something, i understand the part of editing MapleMonster.java adding the lines to the distributePartyExperience function, adding the line into Serverconfig file into the project and adding the line EXP_MOB_MIN_LEVEL_DIFF: 5 into the yalmconfig but seems like i'm missing something to make it work.. i think i'm lost at this part
So this updated code snippet will only add the player as an exp member if: memberLevel <= mobLevel - EXP_MOB_MIN_DIFF

I would also advise using USE_MOB_MIN_LEVEL_DIFF as a boolean, set to true in config, add it to ServerConfig class, and add the extra logic as an else if check.

If i changed the MapleMonster.java inserting this
Code:
        } else {    // thanks Ari for noticing unused server flag after EXP system overhaul
            for (MapleCharacter member : partyParticipation.keySet().iterator().next().getPartyMembersOnSameMap()) {
                if (member.getLevel() <= this.getLevel() - YamlConfig.config.server.EXP_MOB_MIN_LEVEL_DIFF) {
                    totalPartyLevel += member.getLevel();
                    expMembers.add(member);
                }
            }

I can't see how the config will read commands like EXP_MOB_MIN_DIFF and USE_MOB_MIN_LEVEL_DIFF that are mentioned if these constants are not in the MapleMonster.java class..

I'm very noob at these things and maybe something thats too obviously for both of you are like invisible to noobs like me, would you mind telling me how to make it work? i'm really lost :(
 
Upvote 0
Newbie Spellweaver
Joined
Jul 31, 2013
Messages
30
Reaction score
21
I can't see how the config will read commands like EXP_MOB_MIN_DIFF and USE_MOB_MIN_LEVEL_DIFF that are mentioned if these constants are not in the MapleMonster.java class..

Those 2 constants are not defined in MapleMonster, they should be defined in the ServerConfig class because that's what you're calling.

Code:
if (member.getLevel() <= this.getLevel() - YamlConfig.config.server.EXP_MOB_MIN_LEVEL_DIFF) {

Take a look at the YamlConfig class. When the class is called, the declared fields will be initialized depending on the scope. There are 3 fields: 'config' is static, and 'worlds' and 'server' are instanced such that they cannot be accessed without an instantiated object of YamlConfig.

The static field 'config' is assigned a static method which has the return type YamlConfig. When the class is called the JVM will call this method once and assign the result to the static field. This gives us access to the fields 'worlds' and 'server' because the result of the method is an instantiated object of YamlConfig.

To give the instanced fields (i.e. 'worlds' and 'server') their values, the method YamlConfig.fromFile parses the config.yaml file, then uses the reflection api to create an instance of each field. The declared fields in those classes (i.e. WorldConfig and ServerConfig) are then assigned as defined in yaml.config.

, "All members of a list are lines beginning at the same indentation level starting with a dash and a space". Notice how in the YamlConfig class, the field 'worlds' is a List?

In the config.yaml file, each section under the 'worlds' property is essentially an instanced object of WorldConfig. A section being when a line starts with a dash and a space. The YamlReader will instantiate WorldConfig objects and assign fields as specified in the config.yaml file.

In a nutshell, this part of the yaml.config, 'worlds' at the top refers to the 'worlds' field in YamlConfig. Each member that starts with a dash and space will get converted into a WorldConfig object.

Code:
worlds:
    #Properties for Bera 1
  - flag: 0
    server_message: Welcome to Bera!
    event_message: Bera!
    why_am_i_recommended: Welcome to Bera!
    channels: 3

The above yaml for example, will be converted into this (notice how the names are the exact same due to how the reflection api works):

Code:
WorldConfig c = new WorldConfig();
c.flag = 0;
c.server_message = "Welcome to Bera!";
c.event_message = "Bera!";
c.why_am_i_recommended = "Welcome to Bera!";
c.channels = 3;

Anyways, in this case, EXP_MOB_MIN_LEVEL_DIFF is accessed from the YamlConfig.server field in your code which means you should create those properties under the 'server' section in the config.yaml file and define the field in the ServerConfig class named exactly the same way.

Code:
server:
    EXP_MOB_MIN_LEVEL_DIFF: 5

Code:
package config;

public class ServerConfig {
    public int EXP_MOB_MIN_LEVEL_DIFF;

You'll also probably want to make USE_ENFORCE_MOB_LEVEL_RANGE false in your config.yaml because the loop you're modifying is in the else block of that check.

From the looks of it, HeavenMS already implements a system to check if players are within a certain level range of the mob and other players in the party although it's a little more complicated and I'm too lazy to make time and understand it.
 
Upvote 0
Junior Spellweaver
Joined
Sep 20, 2019
Messages
108
Reaction score
8
So, in short words i have to add a line EXP_MOB_MIN_LEVEL_DIFF into ServerConfig class and config.yalm with the same name into my server files so upon the server starts these can be readed and executed properly..

Alright i understand the basics of what you've explained so far but even if i write down the lines EXP_MOB_MIN_LEVEL_DIFF as an int in both places (in the yalm and the ServerConfig.java) also i have USE_ENFORCE_MOB_LEVEL_RANGE in false and i've add this into MapleMonster.java
Code:
} else {    // thanks Ari for noticing unused server flag after EXP system overhaul
            for (MapleCharacter member : partyParticipation.keySet().iterator().next().getPartyMembersOnSameMap()) {
                if (member.getLevel() <= this.getLevel() - YamlConfig.config.server.EXP_MOB_MIN_LEVEL_DIFF) {
                    totalPartyLevel += member.getLevel();
                    expMembers.add(member);
                }
            }
still not working..

It's being supposed to allow the party member who is leeching in the party to recieve Exp only if the mob is 5 levels below the level of the mob or higher but it doesn't.. If you could tell me what i'm doing wrong would be greatfully appretiated. Thank you in advance.
 
Last edited:
Upvote 0
Newbie Spellweaver
Joined
Jul 31, 2013
Messages
30
Reaction score
21
You want to give EXP to the player if they're at most 5 levels below the monster level, right?
For example, a level 5 player can receive EXP from a level 10 mob.
If so then the check here is wrong, isn't it?

Code:
if (member.getLevel() <= this.getLevel() - YamlConfig.config.server.EXP_MOB_MIN_LEVEL_DIFF) {

If this.getLevel() refers to the monster, then as an example, say the monster is level 1 and
EXP_MOB_MIN_LEVEL_DIFF is 5. For the player to receive EXP, their level has to be less than or equal to -4 which is impossible.
What you should be doing is giving the member EXP if their level is larger than or equal to.

Code:
if (member.getLevel() >= this.getLevel() - YamlConfig.config.server.EXP_MOB_MIN_LEVEL_DIFF) {
 
Upvote 0
Junior Spellweaver
Joined
Sep 20, 2019
Messages
108
Reaction score
8
You want to give EXP to the player if they're at most 5 levels below the monster level, right?
For example, a level 5 player can receive EXP from a level 10 mob.
If so then the check here is wrong, isn't it?

Code:
if (member.getLevel() <= this.getLevel() - YamlConfig.config.server.EXP_MOB_MIN_LEVEL_DIFF) {

If this.getLevel() refers to the monster, then as an example, say the monster is level 1 and
EXP_MOB_MIN_LEVEL_DIFF is 5. For the player to receive EXP, their level has to be less than or equal to -4 which is impossible.
What you should be doing is giving the member EXP if their level is larger than or equal to.

Code:
if (member.getLevel() >= this.getLevel() - YamlConfig.config.server.EXP_MOB_MIN_LEVEL_DIFF) {


Wow it worked!

Now one more and last thing!
The player who is lower than 5 levels recieve exp as expected but if the player is 6 levels and below cant recieve exp of a monster wich is being killed for the player..

For example the Bigfoot is level 110, and if i kill it with a character level 104 or lower being in a party i dont recieve exp from it at all.. I need to be 105 (5 levels below the mob) yes or yes to recieve exp wich is not fair if that players is the one killing the bf..

My point is and what is my final goal is, to make players get exp if they are 5 levels below it or higher (wich is complete), and get exp from of those mobs that only the player hits no matter if you are 6 levels below the mob or less if being in a party..
 
Last edited:
Upvote 0
Newbie Spellweaver
Joined
Jul 31, 2013
Messages
30
Reaction score
21
You probably should check how much damage the player did relative to the monsters maximum hp. Or maybe how much damage they dealt vs. how much damage they could have dealt? There's a lot of things you can do.

Briefly looking at the code, exp is distributed to all players in the takenDamage HashMap, and if you want players to gain exp regardless of their level if they hit the mob, this conflicts with your 'within 5 levels' check.

What you could do is also give exp to players who dealt a minimum amount of damage although this will scale badly depending on how much hp a mob has.

You could also increase the level difference to 10 since I think that's the standard.

PHP:
for (MapleCharacter member : partyParticipation.keySet().iterator().next().getPartyMembersOnSameMap()) {
    int playerLvl = member.getLevel();
    AtomicLong dealt = takenDamage.get(member.getId());

    // if the damage dealt by the player is larger than 10% of the mob's max hp
    boolean sufficientDamage = (dealt != null && (float) dealt.get() / stats.getHp() > 0.10f);

    // if the player is within a certain amount of levels of the monster
    boolean reqLevelRange = Math.abs(getLevel() - playerLvl) <= YamlConfig.config.server.EXP_MOB_MIN_LEVEL_DIFF;

    if (sufficientDamage || reqLevelRange) {
        totalPartyLevel += playerLvl;
        expMembers.add(member);
    }
}
 
Upvote 0
Junior Spellweaver
Joined
Sep 20, 2019
Messages
108
Reaction score
8
Hmmm, i tried that and works but also works for members in the party who are above the mob and not only for players who are below the mob level

This may be the conflict you mentioned
and if you want players to gain exp regardless of their level if they hit the mob, this conflicts with your 'within 5 levels' check.
:/

Imma use the bf example once more

Bigfoot level is 110 and currently is working this way:

Party member level 105-110 = get exp without hitting
party member level 111 and above = wont get exp only if deal 10% damage
party member level 104 and below = wont get exp only if deal 10% damage

And what i trying to aim is:

Party member level 105-110 = get exp without hitting
party member level 111 and above = get exp without hitting
party member level 104 and below = wont get exp only if deal 10% damage

PD: i really appretiate you spending your time and sorry im being annoying in someway
 
Last edited:
Upvote 0
Newbie Spellweaver
Joined
Jul 31, 2013
Messages
30
Reaction score
21
Bigfoot level is 110 and currently is working this way:

Party member level 105-110 = get exp without hitting
party member level 111 and above = wont get exp only if deal 10% damage
party member level 104 and below = wont get exp only if deal 10% damage

And what i trying to aim is:

Party member level 105-110 = get exp without hitting
party member level 111 and above = get exp without hitting
party member level 104 and below = wont get exp only if deal 10% damage

That's pretty simple, considering most of it is done.
All you need to do is change the level requirement then:

PHP:
boolean reqLevelRange = getLevel() - playerLvl >= YamlConfig.config.server.EXP_MOB_MIN_LEVEL_DIFF;
 
Upvote 0
Junior Spellweaver
Joined
Sep 20, 2019
Messages
108
Reaction score
8
Yey! now it's finally working as it should!

The code ended like this:

Code:
      // thanks G h o s t, Alfred, Vcoc, BHB for poiting out a bug in detecting party members after membership transactions in a party took place
       if (YamlConfig.config.server.USE_ENFORCE_MOB_LEVEL_RANGE) {
          for (MapleCharacter member : partyParticipation.keySet().iterator().next().getPartyMembersOnSameMap()) {
               if (!leechInterval.inInterval(member.getLevel())) {
                 underleveled.add(member);
                 continue;
                 }

              totalPartyLevel += member.getLevel();
              expMembers.add(member);
         }

     } else {             // thanks Ari for noticing unused server flag after EXP system overhaul
        for (MapleCharacter member : partyParticipation.keySet().iterator().next().getPartyMembersOnSameMap()) {
              int playerLvl = member.getLevel();
              AtomicLong dealt = takenDamage.get(member.getId());

                          // if the damage dealt by the player is larger than 10% of the mob's max hp
                boolean sufficientDamage = (dealt != null && (float) dealt.get() / stats.getHp() > 0.10f);
                         
                         // if the player is within a certain amount of levels of the monster
                boolean reqLevelRange = getLevel() - playerLvl <= YamlConfig.config.server.EXP_MOB_MIN_LEVEL_DIFF;
                         if (sufficientDamage || reqLevelRange) {
            
                totalPartyLevel += playerLvl;
                expMembers.add(member);
            }
         } 
     }

Just in case someone in the future want to add it as well!

I haven't enough words to thank you for the help, please atleast recieve a virtual hug! :3
 
Last edited:
Upvote 0
Back
Top