/* * This file is part of the L2J Mobius project. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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 * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.l2jmobius.gameserver.model; import java.util.concurrent.ScheduledFuture; import java.util.logging.Level; import java.util.logging.Logger; import com.l2jmobius.Config; import com.l2jmobius.commons.util.Rnd; import com.l2jmobius.gameserver.GeoData; import com.l2jmobius.gameserver.ThreadPoolManager; import com.l2jmobius.gameserver.data.xml.impl.FishingData; import com.l2jmobius.gameserver.enums.ShotType; import com.l2jmobius.gameserver.instancemanager.ZoneManager; import com.l2jmobius.gameserver.model.actor.instance.L2PcInstance; import com.l2jmobius.gameserver.model.events.EventDispatcher; import com.l2jmobius.gameserver.model.events.impl.character.player.OnPlayerFishing; import com.l2jmobius.gameserver.model.interfaces.ILocational; import com.l2jmobius.gameserver.model.itemcontainer.Inventory; import com.l2jmobius.gameserver.model.items.instance.L2ItemInstance; import com.l2jmobius.gameserver.model.items.type.WeaponType; import com.l2jmobius.gameserver.model.zone.L2ZoneType; import com.l2jmobius.gameserver.model.zone.ZoneId; import com.l2jmobius.gameserver.model.zone.type.L2FishingZone; import com.l2jmobius.gameserver.model.zone.type.L2WaterZone; import com.l2jmobius.gameserver.network.SystemMessageId; import com.l2jmobius.gameserver.network.serverpackets.ActionFailed; import com.l2jmobius.gameserver.network.serverpackets.PlaySound; import com.l2jmobius.gameserver.network.serverpackets.SystemMessage; import com.l2jmobius.gameserver.network.serverpackets.fishing.ExFishingEnd; import com.l2jmobius.gameserver.network.serverpackets.fishing.ExFishingEnd.FishingEndReason; import com.l2jmobius.gameserver.network.serverpackets.fishing.ExFishingEnd.FishingEndType; import com.l2jmobius.gameserver.network.serverpackets.fishing.ExFishingStart; import com.l2jmobius.gameserver.network.serverpackets.fishing.ExUserInfoFishing; import com.l2jmobius.gameserver.util.Util; /** * @author bit */ public class Fishing { protected static final Logger LOGGER = Logger.getLogger(Fishing.class.getName()); private volatile ILocational _baitLocation = new Location(0, 0, 0); private final L2PcInstance _player; private ScheduledFuture _reelInTask; private ScheduledFuture _startFishingTask; private boolean _isFishing = false; public Fishing(L2PcInstance player) { _player = player; } public synchronized boolean isFishing() { return _isFishing; } public boolean isAtValidLocation() { // TODO: implement checking direction return _player.isInsideZone(ZoneId.FISHING); } public boolean canFish() { return !_player.isDead() && !_player.isAlikeDead() && !_player.hasBlockActions(); } private FishingBaitData getCurrentBaitData() { final L2ItemInstance bait = _player.getInventory().getPaperdollItem(Inventory.PAPERDOLL_LHAND); return bait != null ? FishingData.getInstance().getBaitData(bait.getId()) : null; } private void cancelTasks() { if (_reelInTask != null) { _reelInTask.cancel(false); _reelInTask = null; } if (_startFishingTask != null) { _startFishingTask.cancel(false); _startFishingTask = null; } } public synchronized void startFishing() { if (isFishing()) { return; } _isFishing = true; castLine(); } private void castLine() { if (!Config.ALLOWFISHING && !_player.canOverrideCond(PcCondOverride.ZONE_CONDITIONS)) { _player.sendMessage("Fishing is disabled."); _player.sendPacket(ActionFailed.STATIC_PACKET); stopFishing(FishingEndType.ERROR); return; } cancelTasks(); if (!canFish()) { if (isFishing()) { _player.sendPacket(SystemMessageId.YOUR_ATTEMPT_AT_FISHING_HAS_BEEN_CANCELLED); } stopFishing(FishingEndType.ERROR); return; } final int minPlayerLevel = FishingData.getInstance().getMinPlayerLevel(); if (_player.getLevel() < minPlayerLevel) { if (minPlayerLevel == 85) { _player.sendPacket(SystemMessageId.FISHING_IS_AVAILABLE_TO_CHARACTERS_LV_85_OR_ABOVE); } else // In case of custom fishing level requirement set in config. { _player.sendPacket(SystemMessageId.YOU_DO_NOT_MEET_THE_FISHING_LEVEL_REQUIREMENTS); } _player.sendPacket(ActionFailed.STATIC_PACKET); stopFishing(FishingEndType.ERROR); return; } final L2ItemInstance rod = _player.getActiveWeaponInstance(); if ((rod == null) || (rod.getItemType() != WeaponType.FISHINGROD)) { _player.sendPacket(SystemMessageId.YOU_DO_NOT_HAVE_A_FISHING_POLE_EQUIPPED); _player.sendPacket(ActionFailed.STATIC_PACKET); stopFishing(FishingEndType.ERROR); return; } final FishingBaitData baitData = getCurrentBaitData(); if (baitData == null) { _player.sendPacket(SystemMessageId.YOU_MUST_PUT_BAIT_ON_YOUR_HOOK_BEFORE_YOU_CAN_FISH); _player.sendPacket(ActionFailed.STATIC_PACKET); stopFishing(FishingEndType.ERROR); return; } if (_player.isTransformed() || _player.isInBoat()) { _player.sendPacket(SystemMessageId.YOU_CANNOT_FISH_WHEN_TRANSFORMED_OR_WHILE_RIDING_AS_A_PASSENGER_OF_A_BOAT_IT_S_AGAINST_THE_RULES); _player.sendPacket(ActionFailed.STATIC_PACKET); stopFishing(FishingEndType.ERROR); return; } if (_player.isInCraftMode() || _player.isInStoreMode()) { _player.sendPacket(SystemMessageId.YOU_CANNOT_FISH_WHILE_USING_A_RECIPE_BOOK_PRIVATE_WORKSHOP_OR_PRIVATE_STORE); _player.sendPacket(ActionFailed.STATIC_PACKET); stopFishing(FishingEndType.ERROR); return; } if (_player.isInsideZone(ZoneId.WATER) || _player.isInWater()) { _player.sendPacket(SystemMessageId.YOU_CANNOT_FISH_WHILE_UNDER_WATER); _player.sendPacket(ActionFailed.STATIC_PACKET); stopFishing(FishingEndType.ERROR); return; } _baitLocation = calculateBaitLocation(); if (!isAtValidLocation() || (_baitLocation == null)) { if (isFishing()) { _player.sendPacket(SystemMessageId.YOUR_ATTEMPT_AT_FISHING_HAS_BEEN_CANCELLED); _player.sendPacket(ActionFailed.STATIC_PACKET); } else { _player.sendPacket(SystemMessageId.YOU_CAN_T_FISH_HERE); _player.sendPacket(ActionFailed.STATIC_PACKET); } stopFishing(FishingEndType.ERROR); return; } if (!_player.isChargedShot(ShotType.FISH_SOULSHOTS)) { _player.rechargeShots(false, false, true); } _reelInTask = ThreadPoolManager.getInstance().scheduleGeneral(() -> { _player.getFishing().reelInWithReward(); _startFishingTask = ThreadPoolManager.getInstance().scheduleGeneral(() -> _player.getFishing().castLine(), Rnd.get(FishingData.getInstance().getFishingTimeWaitMin(), FishingData.getInstance().getFishingTimeWaitMax())); }, Rnd.get(FishingData.getInstance().getFishingTimeMin(), FishingData.getInstance().getFishingTimeMax())); _player.stopMove(null); _player.broadcastPacket(new ExFishingStart(_player, -1, baitData.getLevel(), _baitLocation)); _player.sendPacket(new ExUserInfoFishing(_player, true, _baitLocation)); _player.sendPacket(new PlaySound("SF_P_01")); _player.sendPacket(SystemMessageId.YOU_CAST_YOUR_LINE_AND_START_TO_FISH); } public void reelInWithReward() { // Fish may or may not eat the hook. If it does - it consumes fishing bait and fishing shot. // Then player may or may not catch the fish. Using fishing shots increases chance to win. final FishingBaitData baitData = getCurrentBaitData(); if (baitData == null) { reelIn(FishingEndReason.LOSE, false); LOGGER.warning("Player " + _player + " is fishing with unhandled bait: " + _player.getInventory().getPaperdollItem(Inventory.PAPERDOLL_LHAND)); return; } double chance = baitData.getChance(); if (_player.isChargedShot(ShotType.FISH_SOULSHOTS)) { chance *= 1.25; // +25 % chance to win _player.setChargedShot(ShotType.FISH_SOULSHOTS, false); } if (Rnd.get(0, 100) <= chance) { reelIn(FishingEndReason.WIN, true); } else { reelIn(FishingEndReason.LOSE, true); } } private void reelIn(FishingEndReason reason, boolean consumeBait) { if (!isFishing()) { return; } cancelTasks(); try { final L2ItemInstance bait = _player.getInventory().getPaperdollItem(Inventory.PAPERDOLL_LHAND); if (consumeBait) { if ((bait == null) || !_player.getInventory().updateItemCount(null, bait, -1, _player, null)) { reason = FishingEndReason.LOSE; // no bait - no reward return; } } if ((reason == FishingEndReason.WIN) && (bait != null)) { final FishingBaitData baitData = FishingData.getInstance().getBaitData(bait.getId()); final int numRewards = baitData.getRewards().size(); if (numRewards > 0) { // TODO: verify, totally guessed final FishingData fishingData = FishingData.getInstance(); final int lvlModifier = _player.getLevel() * _player.getLevel(); _player.addExpAndSp(Rnd.get(fishingData.getExpRateMin(), fishingData.getExpRateMax()) * lvlModifier, Rnd.get(fishingData.getSpRateMin(), fishingData.getSpRateMax()) * lvlModifier); final int fishId = baitData.getRewards().get(Rnd.get(0, numRewards - 1)); _player.getInventory().addItem("Fishing Reward", fishId, 1, _player, null); final SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.YOU_HAVE_EARNED_S1); msg.addItemName(fishId); _player.sendPacket(msg); } else { LOGGER.log(Level.WARNING, "Could not find fishing rewards for bait ", bait.getId()); } } else if (reason == FishingEndReason.LOSE) { _player.sendPacket(SystemMessageId.THE_BAIT_HAS_BEEN_LOST_BECAUSE_THE_FISH_GOT_AWAY); } if (consumeBait) { EventDispatcher.getInstance().notifyEventAsync(new OnPlayerFishing(_player, reason), _player); } } finally { _player.broadcastPacket(new ExFishingEnd(_player, reason)); _player.sendPacket(new ExUserInfoFishing(_player, false)); } } public void stopFishing() { stopFishing(FishingEndType.PLAYER_STOP); } public synchronized void stopFishing(FishingEndType endType) { if (isFishing()) { reelIn(FishingEndReason.STOP, false); _isFishing = false; switch (endType) { case PLAYER_STOP: { _player.sendPacket(SystemMessageId.YOU_REEL_YOUR_LINE_IN_AND_STOP_FISHING); break; } case PLAYER_CANCEL: { _player.sendPacket(SystemMessageId.YOUR_ATTEMPT_AT_FISHING_HAS_BEEN_CANCELLED); break; } } } } public ILocational getBaitLocation() { return _baitLocation; } private Location calculateBaitLocation() { // calculate a position in front of the player with a random distance final int distMin = FishingData.getInstance().getBaitDistanceMin(); final int distMax = FishingData.getInstance().getBaitDistanceMax(); int distance = Rnd.get(distMin, distMax); final double angle = Util.convertHeadingToDegree(_player.getHeading()); final double radian = Math.toRadians(angle); final double sin = Math.sin(radian); final double cos = Math.cos(radian); int baitX = (int) (_player.getX() + (cos * distance)); int baitY = (int) (_player.getY() + (sin * distance)); // search for fishing and water zone L2FishingZone fishingZone = null; L2WaterZone waterZone = null; for (L2ZoneType zone : ZoneManager.getInstance().getZones(baitX, baitY)) { if (zone instanceof L2FishingZone) { fishingZone = (L2FishingZone) zone; } else if (zone instanceof L2WaterZone) { waterZone = (L2WaterZone) zone; } if ((fishingZone != null) && (waterZone != null)) { break; } } int baitZ = computeBaitZ(_player, baitX, baitY, fishingZone, waterZone); if (baitZ == Integer.MIN_VALUE) { for (distance = distMax; distance >= distMin; --distance) { baitX = (int) (_player.getX() + (cos * distance)); baitY = (int) (_player.getY() + (sin * distance)); // search for fishing and water zone again fishingZone = null; waterZone = null; for (L2ZoneType zone : ZoneManager.getInstance().getZones(baitX, baitY)) { if (zone instanceof L2FishingZone) { fishingZone = (L2FishingZone) zone; } else if (zone instanceof L2WaterZone) { waterZone = (L2WaterZone) zone; } if ((fishingZone != null) && (waterZone != null)) { break; } } baitZ = computeBaitZ(_player, baitX, baitY, fishingZone, waterZone); if (baitZ != Integer.MIN_VALUE) { break; } } if (baitZ == Integer.MIN_VALUE) { if (_player.isGM()) { baitZ = _player.getZ(); } else { _player.sendPacket(SystemMessageId.YOU_CAN_T_FISH_HERE); return null; } } } return new Location(baitX, baitY, baitZ); } /** * Computes the Z of the bait. * @param player the player * @param baitX the bait x * @param baitY the bait y * @param fishingZone the fishing zone * @param waterZone the water zone * @return the bait z or {@link Integer#MIN_VALUE} when you cannot fish here */ private static int computeBaitZ(L2PcInstance player, int baitX, int baitY, L2FishingZone fishingZone, L2WaterZone waterZone) { if ((fishingZone == null)) { return Integer.MIN_VALUE; } if ((waterZone == null)) { return Integer.MIN_VALUE; } // always use water zone, fishing zone high z is high in the air... final int baitZ = waterZone.getWaterZ(); if (!GeoData.getInstance().canSeeTarget(player.getX(), player.getY(), player.getZ(), baitX, baitY, baitZ)) { return Integer.MIN_VALUE; } if (GeoData.getInstance().hasGeo(baitX, baitY)) { if (GeoData.getInstance().getHeight(baitX, baitY, baitZ) > baitZ) { return Integer.MIN_VALUE; } if (GeoData.getInstance().getHeight(baitX, baitY, player.getZ()) > baitZ) { return Integer.MIN_VALUE; } } return baitZ; } }