package javastory.channel.life; import java.lang.ref.WeakReference; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ScheduledFuture; import javastory.channel.ChannelCharacter; import javastory.channel.ChannelClient; import javastory.channel.ChannelServer; import javastory.channel.Party; import javastory.channel.PartyMember; import javastory.channel.client.BuffStat; import javastory.channel.client.Disease; import javastory.channel.client.MonsterStatus; import javastory.channel.client.MonsterStatusEffect; import javastory.channel.maps.GameMap; import javastory.channel.maps.GameMapObjectType; import javastory.channel.packet.MobPacket; import javastory.game.AttackNature; import javastory.game.Effectiveness; import javastory.game.SkillLevelEntry; import javastory.game.data.MobInfo; import javastory.game.data.SkillInfoProvider; import javastory.scripting.EventInstanceManager; import javastory.server.TimerManager; import javastory.server.handling.ServerConstants; import javastory.tools.packets.ChannelPackets; import com.google.common.collect.Lists; import com.google.common.collect.Maps; public class Monster extends AbstractLoadedLife { private MobInfo stats; private OverrideMonsterStats statsOverride = null; private int hp, mp; // private short showdown; private byte venomCounter, carnivalTeam; private GameMap map; private Monster sponge; // Just a reference for monster EXP distribution after death private ChannelCharacter highestDamageChar; private WeakReference<ChannelCharacter> controller = new WeakReference<>(null); private boolean isFake, dropsDisabled, controllerHasAggro, controllerKnowsAboutAggro; private final Collection<AttackerEntry> attackers = Lists.newLinkedList(); private EventInstanceManager eventInstance; private MonsterListener listener = null; private EnumMap<AttackNature, Effectiveness> effectiveness = Maps.newEnumMap(AttackNature.class); private final Map<MonsterStatus, MonsterStatusEffect> statuses = Maps.newEnumMap(MonsterStatus.class); // private final List<MonsterStatusEffect> activeEffects = new ArrayList<MonsterStatusEffect>(); // private final List<MonsterStatus> monsterBuffs = new ArrayList<MonsterStatus>(); private Map<Integer, Long> usedSkills; public Monster(final MobInfo stats) { super(stats.getMobId()); this.initWithStats(stats); } public Monster(final Monster monster) { super(monster); this.initWithStats(monster.stats); } private void initWithStats(final MobInfo stats) { this.setStance(5); this.stats = stats; this.hp = stats.getHp(); this.mp = stats.getMp(); this.venomCounter = 0; // showdown = 100; this.carnivalTeam = -1; this.isFake = false; this.dropsDisabled = false; if (stats.getNoSkills() > 0) { this.usedSkills = Maps.newHashMap(); } this.effectiveness = Maps.newEnumMap(stats.getEffectivenessMap()); } public final MobInfo getStats() { return this.stats; } public final void disableDrops() { this.dropsDisabled = true; } public final boolean dropsDisabled() { return this.dropsDisabled; } public final void setSponge(final Monster mob) { this.sponge = mob; } public final void setMap(final GameMap map) { this.map = map; } public final int getHp() { return this.hp; } public final void setHp(final int hp) { this.hp = hp; } public final int getMobMaxHp() { if (this.statsOverride != null) { return this.statsOverride.getHp(); } return this.stats.getHp(); } public final int getMp() { return this.mp; } public final void setMp(int mp) { if (mp < 0) { mp = 0; } this.mp = mp; } public final int getMobMaxMp() { if (this.statsOverride != null) { return this.statsOverride.getMp(); } return this.stats.getMp(); } public final int getMobExp() { if (this.statsOverride != null) { return this.statsOverride.getExp(); } return this.stats.getExp(); } public final void setOverrideStats(final OverrideMonsterStats ostats) { this.statsOverride = ostats; this.hp = ostats.getHp(); this.mp = ostats.getMp(); } public final Monster getSponge() { return this.sponge; } public final byte getVenomMulti() { return this.venomCounter; } public final void setVenomMulti(final byte venom_counter) { this.venomCounter = venom_counter; } public final void damage(final ChannelCharacter from, final int damage, final boolean updateAttackTime) { if (damage <= 0 || !this.isAlive()) { return; } AttackerEntry attacker = null; final PartyMember member = from.getPartyMembership(); if (member != null) { member.getPartyId(); attacker = new PartyAttackerEntry(member.getPartyId()); } else { attacker = new SingleAttackerEntry(from); } boolean replaced = false; for (final AttackerEntry aentry : this.attackers) { if (aentry.equals(attacker)) { attacker = aentry; replaced = true; break; } } if (!replaced) { this.attackers.add(attacker); } final int rDamage = Math.max(0, Math.min(damage, this.hp)); attacker.addDamage(from, rDamage, updateAttackTime); if (this.stats.getSelfD() != -1) { this.hp -= rDamage; if (this.hp > 0) { if (this.hp < this.stats.getSelfDHp()) { // HP is below the self-destruction level this.map.killMonster(this, from, false, false, this.stats.getSelfD()); } else { // Show HP for (final AttackerEntry mattacker : this.attackers) { for (final AttackingMapleCharacter cattacker : mattacker.getAttackers()) { if (cattacker.getAttacker().getMapId() == from.getMapId()) { // current attacker is on the map of the monster if (cattacker.getLastAttackTime() >= System.currentTimeMillis() - 4000) { cattacker.getAttacker().getClient().write( MobPacket.showMonsterHP(this.getObjectId(), (int) Math.ceil(this.hp * 100.0 / this.getMobMaxHp()))); } } } } } } else { // Character killed it without explosing :( this.map.killMonster(this, from, true, false, (byte) 1); } } else { if (this.sponge != null) { if (this.sponge.hp > 0) { // If it's still alive, don't want double/triple rewards // Sponge are always in the same map, so we can use this.map // The only mob that uses sponge are PB/HT this.sponge.hp -= rDamage; if (this.sponge.hp <= 0) { this.map.killMonster(this.sponge, from, true, false, (byte) 1); } else { this.map.broadcastMessage(MobPacket.showBossHP(this.sponge)); } } } if (this.hp > 0) { this.hp -= rDamage; switch (this.stats.getHpDisplayType()) { case 0: this.map.broadcastMessage(MobPacket.showBossHP(this), this.getPosition()); break; case 1: this.map.broadcastMessage(MobPacket.damageFriendlyMob(this, damage), this.getPosition()); break; case 2: this.map.broadcastMessage(MobPacket.showMonsterHP(this.getObjectId(), (int) Math.ceil(this.hp * 100.0 / this.getMobMaxHp()))); from.mulung_EnergyModify(true); break; case 3: for (final AttackerEntry mattacker : this.attackers) { for (final AttackingMapleCharacter cattacker : mattacker.getAttackers()) { if (cattacker.getAttacker().getMap() == from.getMap()) { // current attacker is on the map of the monster if (cattacker.getLastAttackTime() >= System.currentTimeMillis() - 4000) { cattacker.getAttacker().getClient().write( MobPacket.showMonsterHP(this.getObjectId(), (int) Math.ceil(this.hp * 100.0 / this.getMobMaxHp()))); } } } } break; } if (this.hp <= 0) { this.map.killMonster(this, from, true, false, (byte) 1); } } } } public final void heal(final int hp, final int mp, final boolean broadcast) { final int TotalHP = this.getHp() + hp; final int TotalMP = this.getMp() + mp; if (TotalHP >= this.getMobMaxHp()) { this.setHp(this.getMobMaxHp()); } else { this.setHp(TotalHP); } if (TotalMP >= this.getMp()) { this.setMp(this.getMp()); } else { this.setMp(TotalMP); } if (broadcast) { this.map.broadcastMessage(MobPacket.healMonster(this.getObjectId(), hp)); } else if (this.sponge != null) { // else if, since only sponge doesn't broadcast this.sponge.hp += hp; } } private void giveExpToCharacter(final ChannelCharacter attacker, int exp, final boolean highestDamage, final int numExpSharers, final byte pty, final byte CLASS_EXP_PERCENT) { if (highestDamage) { if (this.eventInstance != null) { this.eventInstance.monsterKilled(attacker, this); } else { final EventInstanceManager em = attacker.getEventInstance(); if (em != null) { em.monsterKilled(attacker, this); } } this.highestDamageChar = attacker; } if (exp > 0) { final Integer holySymbol = attacker.getBuffedValue(BuffStat.HOLY_SYMBOL); if (holySymbol != null) { if (numExpSharers == 1) { exp *= 1.0 + holySymbol.doubleValue() / 500.0; } else { exp *= 1.0 + holySymbol.doubleValue() / 100.0; } } int CLASS_EXP = 0; if (CLASS_EXP_PERCENT > 0) { CLASS_EXP = (int) ((exp / 100.0f) * CLASS_EXP_PERCENT); } if (attacker.hasDisease(Disease.CURSE)) { exp /= 2; } attacker.gainExpMonster(exp, true, highestDamage, pty, CLASS_EXP); } attacker.mobKilled(this.getId()); } public final ChannelCharacter killBy(final ChannelCharacter killer) { final int totalBaseExp = Math.min(Integer.MAX_VALUE, (int) (this.getMobExp() * ChannelServer.getInstance().getExpRate())); AttackerEntry highest = null; int highdamage = 0; for (final AttackerEntry attackEntry : this.attackers) { if (attackEntry.getDamage() > highdamage) { highest = attackEntry; highdamage = attackEntry.getDamage(); } } int baseExp; for (final AttackerEntry attackEntry : this.attackers) { baseExp = (int) Math.ceil(totalBaseExp * ((double) attackEntry.getDamage() / this.getMobMaxHp())); attackEntry.killedMob(killer.getMap(), baseExp, attackEntry == highest); } final ChannelCharacter controll = this.controller.get(); if (controll != null) { // this can/should only happen when a hidden gm // attacks the monster controll.getClient().write(MobPacket.stopControllingMonster(this.getObjectId())); controll.stopControllingMonster(this); } this.spawnRevives(killer.getMap()); if (this.eventInstance != null) { this.eventInstance.unregisterMonster(this); this.eventInstance = null; } this.sponge = null; if (this.listener != null) { this.listener.monsterKilled(); } final ChannelCharacter ret = this.highestDamageChar; this.highestDamageChar = null; // may not keep hard references to chars // outside of PlayerStorage or MapleMap return ret; } public final void spawnRevives(final GameMap map) { final List<Integer> toSpawn = this.stats.getRevives(); if (toSpawn == null) { return; } switch (this.getId()) { case 8810026: case 8820009: case 8820010: case 8820011: case 8820012: case 8820013: { final List<Monster> mobs = Lists.newArrayList(); Monster spongy = null; for (final int i : toSpawn) { final Monster mob = LifeFactory.getMonster(i); mob.setPosition(this.getPosition()); switch (mob.getId()) { case 8810018: // Horntail Sponge case 8820010: // PinkBeanSponge1 case 8820011: // PinkBeanSponge2 case 8820012: // PinkBeanSponge3 case 8820013: // PinkBeanSponge4 case 8820014: // PinkBeanSponge5 spongy = mob; break; default: mobs.add(mob); break; } } if (spongy != null) { map.spawnRevives(spongy, this.getObjectId()); for (final Monster i : mobs) { i.setSponge(spongy); map.spawnRevives(i, this.getObjectId()); } } break; } default: { for (final int i : toSpawn) { final Monster mob = LifeFactory.getMonster(i); if (this.eventInstance != null) { this.eventInstance.registerMonster(mob); } mob.setPosition(this.getPosition()); if (this.dropsDisabled()) { mob.disableDrops(); } map.spawnRevives(mob, this.getObjectId()); if (mob.getId() == 9300216) { map.broadcastMessage(ChannelPackets.environmentChange("Dojang/clear", 4)); map.broadcastMessage(ChannelPackets.environmentChange("dojang/end/clear", 3)); } } break; } } } public final boolean isAlive() { return this.hp > 0; } public final void setCarnivalTeam(final byte team) { this.carnivalTeam = team; } public final byte getCarnivalTeam() { return this.carnivalTeam; } public final ChannelCharacter getController() { return this.controller.get(); } public final void setController(final ChannelCharacter controller) { this.controller = new WeakReference<>(controller); } public final void switchController(final ChannelCharacter newController, final boolean immediateAggro) { final ChannelCharacter controllers = this.getController(); if (controllers == newController) { return; } else if (controllers != null) { controllers.stopControllingMonster(this); controllers.getClient().write(MobPacket.stopControllingMonster(this.getObjectId())); } newController.controlMonster(this, immediateAggro); this.setController(newController); if (immediateAggro) { this.setControllerHasAggro(true); } this.setControllerKnowsAboutAggro(false); } public final void addListener(final MonsterListener listener) { this.listener = listener; } public final boolean isControllerHasAggro() { return this.controllerHasAggro; } public final void setControllerHasAggro(final boolean controllerHasAggro) { this.controllerHasAggro = controllerHasAggro; } public final boolean isControllerKnowsAboutAggro() { return this.controllerKnowsAboutAggro; } public final void setControllerKnowsAboutAggro(final boolean controllerKnowsAboutAggro) { this.controllerKnowsAboutAggro = controllerKnowsAboutAggro; } @Override public final void sendSpawnData(final ChannelClient client) { if (!this.isAlive()) { return; } client.write(MobPacket.spawnMonster(this, -1, this.isFake ? 0xfc : 0, 0)); if (this.statuses.size() > 0) { for (final MonsterStatusEffect mse : this.statuses.values()) { client.write(MobPacket.applyMonsterStatus(this.getObjectId(), mse)); } } } @Override public final void sendDestroyData(final ChannelClient client) { client.write(MobPacket.killMonster(this.getObjectId(), 0)); } @Override public final String toString() { final StringBuilder sb = new StringBuilder(); sb.append(this.stats.getName()); sb.append("("); sb.append(this.getId()); sb.append(") at X"); sb.append(this.getPosition().x); sb.append("/ Y"); sb.append(this.getPosition().y); sb.append(" with "); sb.append(this.getHp()); sb.append("/ "); sb.append(this.getMobMaxHp()); sb.append("hp, "); sb.append(this.getMp()); sb.append("/ "); sb.append(this.getMobMaxMp()); sb.append(" mp (alive: "); sb.append(this.isAlive()); sb.append(" oid: "); sb.append(this.getObjectId()); sb.append(") || Controller name : "); final ChannelCharacter chr = this.controller.get(); sb.append(chr != null ? chr.getName() : "null"); return sb.toString(); } @Override public final GameMapObjectType getType() { return GameMapObjectType.MONSTER; } public final EventInstanceManager getEventInstance() { return this.eventInstance; } public final void setEventInstance(final EventInstanceManager eventInstance) { this.eventInstance = eventInstance; } public final int getStatusSourceID(final MonsterStatus status) { final MonsterStatusEffect effect = this.statuses.get(status); if (effect != null) { return effect.getSkill().getId(); } return -1; } public final Effectiveness getEffectiveness(final AttackNature e) { if (this.statuses.size() > 0 && this.statuses.get(MonsterStatus.DOOM) != null) { return Effectiveness.NORMAL; // like blue snails } return this.effectiveness.get(e); } public final void applyStatus(final ChannelCharacter from, final MonsterStatusEffect status, final boolean poison, final long duration, final boolean venom) { if (!this.isAlive()) { return; } final AttackNature element = status.getSkill().getElement(); switch (this.effectiveness.get(element)) { case IMMUNE: case STRONG: return; case NORMAL: case WEAK: break; default: return; } // compos don't have an elemental // (they have 2 - so we have to hack here...) final int statusSkill = status.getSkill().getId(); switch (statusSkill) { case 2111006: { // FP compo switch (this.effectiveness.get(AttackNature.POISON)) { case IMMUNE: case STRONG: return; } break; } case 2211006: { // IL compo switch (this.effectiveness.get(AttackNature.ICE)) { case IMMUNE: case STRONG: return; } break; } case 4120005: case 4220005: case 14110004: { switch (this.effectiveness.get(AttackNature.POISON)) { case WEAK: return; } break; } } final Map<MonsterStatus, Integer> statusEffects = status.getEffects(); if (this.stats.isBoss()) { final boolean hasSpeed = statusEffects.containsKey(MonsterStatus.SPEED); final boolean hasNinjaAmbush = statusEffects.containsKey(MonsterStatus.NINJA_AMBUSH); final boolean hasWatk = statusEffects.containsKey(MonsterStatus.WATK); if (hasSpeed || hasNinjaAmbush || hasWatk) { return; } } for (final MonsterStatus stat : statusEffects.keySet()) { final MonsterStatusEffect oldEffect = this.statuses.get(stat); if (oldEffect != null) { oldEffect.removeActiveStatus(stat); if (oldEffect.getEffects().isEmpty()) { oldEffect.cancelTask(); oldEffect.cancelPoisonSchedule(); } } } final TimerManager timerManager = TimerManager.getInstance(); final Runnable cancelTask = new Runnable() { @Override public final void run() { if (Monster.this.isAlive()) { Monster.this.map.broadcastMessage(MobPacket.cancelMonsterStatus(Monster.this.getObjectId(), statusEffects), Monster.this.getPosition()); if (Monster.this.getController() != null && !Monster.this.getController().isMapObjectVisible(Monster.this)) { Monster.this.getController().getClient().write(MobPacket.cancelMonsterStatus(Monster.this.getObjectId(), statusEffects)); } for (final MonsterStatus stat : statusEffects.keySet()) { Monster.this.statuses.remove(stat); } Monster.this.setVenomMulti((byte) 0); } status.cancelPoisonSchedule(); } }; if (poison && this.getHp() > 1) { final int poisonDamage = Math.min(Short.MAX_VALUE, (int) (this.getMobMaxHp() / (70.0 - from.getCurrentSkillLevel(status.getSkill())) + 0.999)); status.setEffect(MonsterStatus.POISON, Integer.valueOf(poisonDamage)); status.setPoisonSchedule(timerManager.register(new PoisonTask(poisonDamage, from, status, cancelTask, false), 1000, 1000)); } else if (venom) { int poisonLevel = 0; int matk = 0; switch (from.getJobId()) { case 412: poisonLevel = from.getCurrentSkillLevel(SkillInfoProvider.getSkill(4120005)); if (poisonLevel <= 0) { return; } matk = SkillInfoProvider.getSkill(4120005).getEffect(poisonLevel).getMatk(); break; case 422: poisonLevel = from.getCurrentSkillLevel(SkillInfoProvider.getSkill(4220005)); if (poisonLevel <= 0) { return; } matk = SkillInfoProvider.getSkill(4220005).getEffect(poisonLevel).getMatk(); break; case 1411: case 1412: poisonLevel = from.getCurrentSkillLevel(SkillInfoProvider.getSkill(14110004)); if (poisonLevel <= 0) { return; } matk = SkillInfoProvider.getSkill(14110004).getEffect(poisonLevel).getMatk(); break; default: return; // Hack, using venom without the job required } final int luk = from.getStats().getLuk(); final int maxDmg = (int) Math.ceil(Math.min(Short.MAX_VALUE, 0.2 * luk * matk)); final 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 < this.getVenomMulti(); i++) { poisonDamage = poisonDamage + (int) (gap * Math.random()) + minDmg; } poisonDamage = Math.min(Short.MAX_VALUE, poisonDamage); status.setEffect(MonsterStatus.POISON, Integer.valueOf(poisonDamage)); status.setPoisonSchedule(timerManager.register(new PoisonTask(poisonDamage, from, status, cancelTask, false), 1000, 1000)); } else if (statusSkill == 4111003 || statusSkill == 14111001) { // shadow // web status.setPoisonSchedule(timerManager.schedule(new PoisonTask((int) (this.getMobMaxHp() / 50.0 + 0.999), from, status, cancelTask, true), 3500)); } else if (statusSkill == 4121004 || statusSkill == 4221004) { final int damage = (from.getStats().getStr() + from.getStats().getLuk()) * 2 * (60 / 100); status.setPoisonSchedule(timerManager.register(new PoisonTask(damage, from, status, cancelTask, false), 1000, 1000)); } for (final MonsterStatus stat : statusEffects.keySet()) { this.statuses.put(stat, status); } this.map.broadcastMessage(MobPacket.applyMonsterStatus(this.getObjectId(), status), this.getPosition()); if (this.getController() != null && !this.getController().isMapObjectVisible(this)) { this.getController().getClient().write(MobPacket.applyMonsterStatus(this.getObjectId(), status)); } final ScheduledFuture<?> schedule = timerManager.schedule(cancelTask, duration + status.getSkill().getAnimationTime()); status.setCancelTask(schedule); } public final void applyMonsterBuff(final Map<MonsterStatus, Integer> stats, final int x, final int skillId, final long duration, final MobSkill skill, final List<Integer> reflection) { final TimerManager timerManager = TimerManager.getInstance(); final Runnable cancelTask = new Runnable() { @Override public final void run() { if (Monster.this.isAlive()) { Monster.this.map.broadcastMessage(MobPacket.cancelMonsterStatus(Monster.this.getObjectId(), stats), Monster.this.getPosition()); if (Monster.this.getController() != null && !Monster.this.getController().isMapObjectVisible(Monster.this)) { Monster.this.getController().getClient().write(MobPacket.cancelMonsterStatus(Monster.this.getObjectId(), stats)); } for (final MonsterStatus stat : stats.keySet()) { Monster.this.statuses.remove(stat); } } } }; final MonsterStatusEffect effect = new MonsterStatusEffect(stats, null, skill, true); for (final MonsterStatus stat : stats.keySet()) { this.statuses.put(stat, effect); } if (reflection.size() > 0) { this.map.broadcastMessage(MobPacket.applyMonsterStatus(this.getObjectId(), effect, reflection), this.getPosition()); if (this.getController() != null && !this.getController().isMapObjectVisible(this)) { this.getController().getClient().write(MobPacket.applyMonsterStatus(this.getObjectId(), effect, reflection)); } } else { this.map.broadcastMessage(MobPacket.applyMonsterStatus(this.getObjectId(), effect), this.getPosition()); if (this.getController() != null && !this.getController().isMapObjectVisible(this)) { this.getController().getClient().write(MobPacket.applyMonsterStatus(this.getObjectId(), effect)); } } timerManager.schedule(cancelTask, duration); } public final void setTempEffectiveness(final AttackNature element, final long milli) { this.effectiveness.put(element, Effectiveness.WEAK); TimerManager.getInstance().schedule(new EffectivenessExpiration(element), milli); } public final boolean isBuffed(final MonsterStatus status) { return this.statuses.containsKey(status); } public final void setFake(final boolean fake) { this.isFake = fake; } public final boolean isFake() { return this.isFake; } public final GameMap getMap() { return this.map; } public final List<SkillLevelEntry> getSkills() { return this.stats.getSkills(); } public final boolean hasSkill(final int skillId, final int level) { return this.stats.hasSkill(skillId, level); } public final long getLastSkillUsed(final int skillId) { if (this.usedSkills.containsKey(skillId)) { return this.usedSkills.get(skillId); } return 0; } public final void setLastSkillUsed(final int skillId, final long now, final long cooltime) { switch (skillId) { case 140: this.usedSkills.put(skillId, now + cooltime * 2); this.usedSkills.put(141, now); break; case 141: this.usedSkills.put(skillId, now + cooltime * 2); this.usedSkills.put(140, now + cooltime); break; default: this.usedSkills.put(skillId, now + cooltime); break; } } public final byte getNoSkills() { return this.stats.getNoSkills(); } public final boolean isFirstAttack() { return this.stats.isFirstAttack(); } public final int getBuffToGive() { return this.stats.getBuffToGive(); } private final class EffectivenessExpiration implements Runnable { private final AttackNature e; private EffectivenessExpiration(final AttackNature e) { this.e = e; } @Override public void run() { Monster.this.effectiveness.remove(this.e); } } private final class PoisonTask implements Runnable { private final int poisonDamage; private final ChannelCharacter chr; private final MonsterStatusEffect status; private final Runnable cancelTask; private final boolean shadowWeb; private final GameMap map; private PoisonTask(final int poisonDamage, final ChannelCharacter chr, final MonsterStatusEffect status, final Runnable cancelTask, final boolean shadowWeb) { this.poisonDamage = poisonDamage; this.chr = chr; this.status = status; this.cancelTask = cancelTask; this.shadowWeb = shadowWeb; this.map = chr.getMap(); } @Override public void run() { int damage = this.poisonDamage; if (damage >= Monster.this.hp) { damage = Monster.this.hp - 1; if (!this.shadowWeb) { this.cancelTask.run(); this.status.cancelTask(); } } if (Monster.this.hp > 1 && damage > 0) { Monster.this.damage(this.chr, damage, false); if (this.shadowWeb) { this.map.broadcastMessage(MobPacket.damageMonster(Monster.this.getObjectId(), damage), Monster.this.getPosition()); } } } } private static class AttackingMapleCharacter { private final ChannelCharacter attacker; private final long lastAttackTime; public AttackingMapleCharacter(final ChannelCharacter attacker, final long lastAttackTime) { super(); this.attacker = attacker; this.lastAttackTime = lastAttackTime; } public final long getLastAttackTime() { return this.lastAttackTime; } public final ChannelCharacter getAttacker() { return this.attacker; } } private interface AttackerEntry { List<AttackingMapleCharacter> getAttackers(); public void addDamage(ChannelCharacter from, int damage, boolean updateAttackTime); public int getDamage(); public boolean contains(ChannelCharacter chr); public void killedMob(GameMap map, int baseExp, boolean mostDamage); } private final class SingleAttackerEntry implements AttackerEntry { private int damage; private final int chrid; private long lastAttackTime; public SingleAttackerEntry(final ChannelCharacter from) { this.chrid = from.getId(); } @Override public void addDamage(final ChannelCharacter from, final int damage, final boolean updateAttackTime) { if (this.chrid == from.getId()) { this.damage += damage; if (updateAttackTime) { this.lastAttackTime = System.currentTimeMillis(); } } } @Override public final List<AttackingMapleCharacter> getAttackers() { final ChannelCharacter chr = ChannelServer.getPlayerStorage().getCharacterById(this.chrid); if (chr != null) { return Collections.singletonList(new AttackingMapleCharacter(chr, this.lastAttackTime)); } else { return Collections.emptyList(); } } @Override public boolean contains(final ChannelCharacter chr) { return this.chrid == chr.getId(); } @Override public int getDamage() { return this.damage; } @Override public void killedMob(final GameMap map, final int baseExp, final boolean mostDamage) { final ChannelCharacter chr = ChannelServer.getPlayerStorage().getCharacterById(this.chrid); if (chr != null && chr.getMap() == map && chr.isAlive()) { Monster.this.giveExpToCharacter(chr, baseExp, mostDamage, 1, (byte) 0, (byte) 0); } } @Override public int hashCode() { return this.chrid; } @Override public final boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (this.getClass() != obj.getClass()) { return false; } final SingleAttackerEntry other = (SingleAttackerEntry) obj; return this.chrid == other.chrid; } } private static final class ExpMap { public final int exp; public final byte ptysize; public final byte CLASS_EXP; public ExpMap(final int exp, final byte ptysize, final byte CLASS_EXP) { super(); this.exp = exp; this.ptysize = ptysize; this.CLASS_EXP = CLASS_EXP; } } private static final class OnePartyAttacker { public Party lastKnownParty; public int damage; public long lastAttackTime; public OnePartyAttacker(final Party lastKnownParty, final int damage) { super(); this.lastKnownParty = lastKnownParty; this.damage = damage; this.lastAttackTime = System.currentTimeMillis(); } } private class PartyAttackerEntry implements AttackerEntry { private int totDamage; private final Map<Integer, OnePartyAttacker> attackers = Maps.newHashMapWithExpectedSize(6); private final int partyid; public PartyAttackerEntry(final int partyid) { this.partyid = partyid; } @Override public List<AttackingMapleCharacter> getAttackers() { final List<AttackingMapleCharacter> ret = Lists.newArrayListWithCapacity(this.attackers.size()); for (final Entry<Integer, OnePartyAttacker> entry : this.attackers.entrySet()) { final ChannelCharacter chr = ChannelServer.getPlayerStorage().getCharacterById(entry.getKey()); if (chr != null) { ret.add(new AttackingMapleCharacter(chr, entry.getValue().lastAttackTime)); } } return ret; } private Map<ChannelCharacter, OnePartyAttacker> resolveAttackers() { final Map<ChannelCharacter, OnePartyAttacker> ret = Maps.newHashMapWithExpectedSize(this.attackers.size()); for (final Entry<Integer, OnePartyAttacker> aentry : this.attackers.entrySet()) { final ChannelCharacter chr = ChannelServer.getPlayerStorage().getCharacterById(aentry.getKey()); if (chr != null) { ret.put(chr, aentry.getValue()); } } return ret; } @Override public final boolean contains(final ChannelCharacter chr) { return this.attackers.containsKey(chr.getId()); } @Override public final int getDamage() { return this.totDamage; } @Override public void addDamage(final ChannelCharacter from, final int damage, final boolean updateAttackTime) { final OnePartyAttacker oldPartyAttacker = this.attackers.get(from.getId()); if (oldPartyAttacker != null) { oldPartyAttacker.damage += damage; oldPartyAttacker.lastKnownParty = from.getParty(); if (updateAttackTime) { oldPartyAttacker.lastAttackTime = System.currentTimeMillis(); } } else { // TODO actually this causes wrong behavior when the party // changes between attacks only the last setup will get exp - // but otherwise we'd have to store the full party constellation // for every attack or every time it changes, might be // wanted/needed in the future but not now final OnePartyAttacker onePartyAttacker = new OnePartyAttacker(from.getParty(), damage); this.attackers.put(from.getId(), onePartyAttacker); if (!updateAttackTime) { onePartyAttacker.lastAttackTime = 0; } } this.totDamage += damage; } @Override public final void killedMob(final GameMap map, final int baseExp, final boolean mostDamage) { ChannelCharacter pchr, highest = null; int iDamage, iexp, highestDamage = 0; Party party; double averagePartyLevel, expWeight, levelMod, innerBaseExp, expFraction; List<ChannelCharacter> expApplicable; final Map<ChannelCharacter, ExpMap> expMap = Maps.newHashMapWithExpectedSize(6); byte CLASS_EXP; for (final Entry<ChannelCharacter, OnePartyAttacker> attacker : this.resolveAttackers().entrySet()) { party = attacker.getValue().lastKnownParty; averagePartyLevel = 0; CLASS_EXP = 0; expApplicable = Lists.newArrayList(); for (final PartyMember partychar : party.getMembers()) { if (attacker.getKey().getLevel() - partychar.getLevel() <= 5 || Monster.this.stats.getLevel() - partychar.getLevel() <= 5) { pchr = ChannelServer.getPlayerStorage().getCharacterByName(partychar.getName()); if (pchr != null) { if (pchr.isAlive() && pchr.getMap() == map) { expApplicable.add(pchr); averagePartyLevel += pchr.getLevel(); if (CLASS_EXP == 0) { CLASS_EXP = ServerConstants.CLASS_EXP(pchr.getJobId()); } } } } } if (expApplicable.size() > 1) { averagePartyLevel /= expApplicable.size(); } iDamage = attacker.getValue().damage; if (iDamage > highestDamage) { highest = attacker.getKey(); highestDamage = iDamage; } innerBaseExp = baseExp * ((double) iDamage / this.totDamage); expFraction = innerBaseExp / (expApplicable.size() + 1); for (final ChannelCharacter expReceiver : expApplicable) { iexp = expMap.get(expReceiver) == null ? 0 : expMap.get(expReceiver).exp; expWeight = expReceiver == attacker.getKey() ? 2.0 : 0.7; levelMod = expReceiver.getLevel() / averagePartyLevel; if (levelMod > 1.0 || this.attackers.containsKey(expReceiver.getId())) { levelMod = 1.0; } iexp += (int) Math.round(expFraction * expWeight * levelMod); expMap.put(expReceiver, new ExpMap(iexp, (byte) expApplicable.size(), CLASS_EXP)); } } ExpMap expmap; for (final Entry<ChannelCharacter, ExpMap> expReceiver : expMap.entrySet()) { expmap = expReceiver.getValue(); Monster.this.giveExpToCharacter(expReceiver.getKey(), expmap.exp, mostDamage ? expReceiver.getKey() == highest : false, expMap.size(), expmap.ptysize, expmap.CLASS_EXP); } } @Override public final int hashCode() { final int prime = 31; int result = 1; result = prime * result + this.partyid; return result; } @Override public final boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (this.getClass() != obj.getClass()) { return false; } final PartyAttackerEntry other = (PartyAttackerEntry) obj; if (this.partyid != other.partyid) { return false; } return true; } } }