From f6096e3020aa0d54c519e56d0ed893d4970ff9d4 Mon Sep 17 00:00:00 2001 From: Ceikry Date: Thu, 20 Apr 2023 12:16:21 +0000 Subject: [PATCH] Rewrote impling system Fixed static impling spawns Fixed impling spawn ratio Reworked Puro-Puro, much more authentic Imp defenders now work properly Crop circles now appear Added admin command ::implings to list active implings (excludes implings from Puro-Puro) Fixed bug where you could add to a finished compost bin --- Server/data/configs/npc_configs.json | 60 ++++++ Server/data/configs/npc_spawns.json | 60 +++--- .../global/skill/farming/UseWithBinHandler.kt | 8 +- .../skill/hunter/ImpetuousImpulses.java | 152 --------------- .../global/skill/hunter/ImplingNPC.java | 120 ------------ .../skill/hunter/implings/ImplingBehavior.kt | 84 +++++++++ .../hunter/implings/ImplingController.kt | 90 +++++++++ .../skill/hunter/implings/ImplingData.kt | 178 ++++++++++++++++++ .../hunter/implings/ImplingSpawnerBehavior.kt | 52 +++++ .../global/skill/magic/spellconsts/Modern.kt | 5 +- .../global/skill/slayer/NechryaelBehavior.kt | 5 +- .../minigame/puropuro/CropCircleController.kt | 97 ++++++++++ .../minigame/puropuro/ImpDefenderBehavior.kt | 93 +++++++++ .../minigame/puropuro/ImpDefenderNPC.java | 112 ----------- .../minigame/puropuro/PuroPuroPlugin.java | 52 +++-- Server/src/main/core/api/ContentAPI.kt | 31 +++ Server/src/main/core/api/utils/Vector.kt | 35 ++++ .../src/main/core/api/utils/WeightedTable.kt | 61 ++++++ .../main/core/game/node/entity/npc/NPC.java | 18 +- .../core/game/node/entity/npc/NPCBehavior.kt | 26 ++- .../player/info/login/PlayerSaveParser.kt | 4 +- .../entity/player/info/login/PlayerSaver.kt | 2 + .../main/core/game/world/map/Location.java | 18 +- .../core/game/world/map/path/Pathfinder.java | 9 +- 24 files changed, 915 insertions(+), 457 deletions(-) delete mode 100644 Server/src/main/content/global/skill/hunter/ImpetuousImpulses.java delete mode 100644 Server/src/main/content/global/skill/hunter/ImplingNPC.java create mode 100644 Server/src/main/content/global/skill/hunter/implings/ImplingBehavior.kt create mode 100644 Server/src/main/content/global/skill/hunter/implings/ImplingController.kt create mode 100644 Server/src/main/content/global/skill/hunter/implings/ImplingData.kt create mode 100644 Server/src/main/content/global/skill/hunter/implings/ImplingSpawnerBehavior.kt create mode 100644 Server/src/main/content/minigame/puropuro/CropCircleController.kt create mode 100644 Server/src/main/content/minigame/puropuro/ImpDefenderBehavior.kt delete mode 100644 Server/src/main/content/minigame/puropuro/ImpDefenderNPC.java create mode 100644 Server/src/main/core/api/utils/Vector.kt create mode 100644 Server/src/main/core/api/utils/WeightedTable.kt diff --git a/Server/data/configs/npc_configs.json b/Server/data/configs/npc_configs.json index 594087c21..c6a89905d 100644 --- a/Server/data/configs/npc_configs.json +++ b/Server/data/configs/npc_configs.json @@ -71544,5 +71544,65 @@ "bonuses": "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0", "range_level": "1", "attack_level": "1" + }, + { + "name": "Puro Spawn (Low)", + "id": "6065", + "respawn_delay": "1" + }, + { + "name": "Puro Spawn (Mid)", + "id": "6066", + "respawn_delay": "1" + }, + { + "name": "Puro Spawn (High)", + "id": "6067", + "respawn_delay": "1" + }, + { + "name": "Baby Impling Puro", + "id": "6055", + "respawn_delay": "7" + }, + { + "name": "Young Impling Puro", + "id": "6056", + "respawn_delay": "7" + }, + { + "name": "Gourmet Impling Puro", + "id": "6057", + "respawn_delay": "7" + }, + { + "name": "Earth Impling Puro", + "id": "6058", + "respawn_delay": "50" + }, + { + "name": "Essence Impling Puro", + "id": "6059", + "respawn_delay": "50" + }, + { + "name": "Eclectic Impling Puro", + "id": "6060", + "respawn_delay": "7" + }, + { + "name": "Ninja Impling Puro", + "id": "6063", + "respawn_delay": "9999" + }, + { + "name": "Nature Impling Puro", + "id": "6061", + "respawn_delay": "9999" + }, + { + "name": "Magpie Impling Puro", + "id": "6062", + "respawn_delay": "9999" } ] \ No newline at end of file diff --git a/Server/data/configs/npc_spawns.json b/Server/data/configs/npc_spawns.json index 195f51a94..e6bda8222 100644 --- a/Server/data/configs/npc_spawns.json +++ b/Server/data/configs/npc_spawns.json @@ -2511,30 +2511,6 @@ "npc_id": "1027", "loc_data": "{3278,3155,0,0,0}-{2354,3608,0,0,0}" }, - { - "npc_id": "1028", - "loc_data": "{2778,3436,0,0,0}" - }, - { - "npc_id": "1029", - "loc_data": "{2920,3499,0,0,0}-{2784,3480,0,0,0}" - }, - { - "npc_id": "1030", - "loc_data": "{2768,3470,0,1,0}-{2843,3155,0,1,0}" - }, - { - "npc_id": "1032", - "loc_data": "{2865,3504,0,0,0}" - }, - { - "npc_id": "1033", - "loc_data": "{2606,2967,0,0,0}" - }, - { - "npc_id": "1035", - "loc_data": "{2599,2954,0,0,0}" - }, { "npc_id": "1036", "loc_data": "{3514,3478,0,0,0}-{3514,3479,0,0,0}-{3514,3480,0,0,0}-{3514,3481,0,0,0}-{3514,3482,0,0,0}-{3514,3483,0,0,0}" @@ -8695,6 +8671,42 @@ "npc_id": "6051", "loc_data": "{3292,3069,0,1,1}-{3285,3071,0,1,1}-{3279,3078,0,1,4}-{3273,3065,0,1,3}-{3288,3066,0,1,6}-{3281,3057,0,1,5}-{3294,3056,0,1,5}-{3305,3064,0,1,3}-{3312,3075,0,1,6}-{3306,3086,0,1,4}-{3272,3082,0,1,4}" }, + { + "npc_id": "6055", + "loc_data": "{2563,4348,0,1,0}-{2620,4348,0,1,0}-{2620,4291,0,1,0}-{2563,4291,0,1,0}-{2572,4339,0,1,0}-{2565,4327,0,1,0}-{2575,4315,0,1,0}-{2586,4306,0,1,0}-{2593,4304,0,1,0}-{2605,4312,0,1,0}-{2614,4330,0,1,0}-" + }, + { + "npc_id": "6056", + "loc_data": "{2565,4320,0,1,0}-{2592,4348,0,1,0}-{2596,4346,0,1,0}-{2576,4333,0,1,0}-{2576,4324,0,1,0}-{2612,4333,0,1,0}-{2617,4329,0,1,0}-{2608,4316,0,1,0}-{2593,4295,0,1,0}-{2587,4307,0,1,0}-" + }, + { + "npc_id": "6057", + "loc_data": "{2604,4348,0,1,0}-{2613,4346,0,1,0}-{2582,4347,0,1,0}-{2568,4333,0,1,0}-{2576,4321,0,1,0}-{2571,4302,0,1,0}-{2587,4296,0,1,0}-{2598,4301,0,1,0}-{2612,4303,0,1,0}-{2607,4321,0,1,0}-{2617,4325,0,1,0}-{2617,4346,0,1,0}-" + }, + { + "npc_id": "6058", + "loc_data": "{2585,4343,0,1,0}-{2599,4338,0,1,0}-{2612,4338,0,1,0}-{2573,4329,0,1,0}-{2572,4320,0,1,0}-{2575,4309,0,1,0}-{2587,4302,0,1,0}-{2606,4319,0,1,0}-{2608,4336,0,1,0}-" + }, + { + "npc_id": "6059", + "loc_data": "{2579,4340,0,1,0}-{2578,4323,0,1,0}-{2580,4302,0,1,0}-{2611,4324,0,1,0}-{2594,4338,0,1,0}-" + }, + { + "npc_id": "6060", + "loc_data": "{2589,4341,0,1,0}-{2569,4315,0,1,0}-{2591,4299,0,1,0}-{2611,4330,0,1,0}-" + }, + { + "npc_id": "6065", + "loc_data": "{2612,4348,0,1,0}-{2606,4340,0,1,0}-{2592,4333,0,1,0}-{2579,4336,0,1,0}-{2565,4340,0,1,0}-{2569,4329,0,1,0}-{2574,4311,0,1,0}-{2570,4304,0,1,0}-{2567,4294,0,1,0}-{2604,4305,0,1,0}-{2610,4293,0,1,0}-{2612,4320,0,1,0}-" + }, + { + "npc_id": "6066", + "loc_data": "{2601,4334,0,1,0}-{2586,4335,0,1,0}-{2572,4334,0,1,0}-{2574,4323,0,1,0}-{2579,4295,0,1,0}-{2588,4298,0,1,0}-{2600,4304,0,1,0}-{2612,4312,0,1,0}-{2607,4329,0,1,0}-{2616,4336,0,1,0}-" + }, + { + "npc_id": "6067", + "loc_data": "{2570,4341,0,0,0}-{2613,4298,0,0,0}-" + }, { "npc_id": "6070", "loc_data": "{2589,4313,0,1,6}" diff --git a/Server/src/main/content/global/skill/farming/UseWithBinHandler.kt b/Server/src/main/content/global/skill/farming/UseWithBinHandler.kt index f3d5f7bbf..50f53434f 100644 --- a/Server/src/main/content/global/skill/farming/UseWithBinHandler.kt +++ b/Server/src/main/content/global/skill/farming/UseWithBinHandler.kt @@ -1,6 +1,6 @@ package content.global.skill.farming -import core.api.toIntArray +import core.api.* import core.game.node.item.Item import core.game.system.task.Pulse import core.game.world.update.flag.context.Animation @@ -65,7 +65,7 @@ class UseWithBinHandler : InteractionListener { if(bin.isFull()){ player.sendMessage("This compost bin is already full.") return@onUseWith true - } else { + } else if (!bin.isFinished) { player.pulseManager.run(object : Pulse(fillAnim.duration){ override fun pulse(): Boolean { player.animator.animate(fillAnim) @@ -75,6 +75,8 @@ class UseWithBinHandler : InteractionListener { return bin.isFull() || player.inventory.getAmount(usedNode.asItem()) == 0 } }) + } else { + sendMessage (player, "You should empty the remaining compost first.") } } return@onUseWith true @@ -100,4 +102,4 @@ class UseWithBinHandler : InteractionListener { if(this != 6476) return this + 2 else return Items.VIAL_229 } -} \ No newline at end of file +} diff --git a/Server/src/main/content/global/skill/hunter/ImpetuousImpulses.java b/Server/src/main/content/global/skill/hunter/ImpetuousImpulses.java deleted file mode 100644 index 2ce0c9526..000000000 --- a/Server/src/main/content/global/skill/hunter/ImpetuousImpulses.java +++ /dev/null @@ -1,152 +0,0 @@ -package content.global.skill.hunter; - -import content.global.skill.hunter.bnet.BNetTypes; -import content.global.skill.hunter.bnet.ImplingNode; -import core.api.StartupListener; -import core.game.world.GameWorld; -import core.game.world.map.Location; -import core.tools.RandomFunction; - -import java.util.Random; - -/** - * Handles the impetuous impulse minigame. - * @author Vexia - * @author Emperor - */ -public final class ImpetuousImpulses implements StartupListener { - - /** - * The random instance used. - */ - private static final Random RANDOM = new Random(); - - /** - * The puro puro locations. - */ - private static final Location[] PURO_LOCATIONS = new Location[] { new Location(2575, 4314, 0), new Location(2572, 4302, 0), new Location(2575, 4297, 0), new Location(2589, 4297, 0), new Location(2603, 4297, 0), new Location(2612, 4301, 0), new Location(2619, 4296, 0), new Location(2616, 4297, 0), new Location(2620, 4308, 0), new Location(2617, 4313, 0), new Location(2620, 4317, 0), new Location(2611, 4312, 0), new Location(2607, 4305, 0), new Location(2593, 4303, 0), new Location(2578, 4318, 0), new Location(2578, 4330, 0), new Location(2581, 4338, 0), new Location(2586, 4342, 0), Location.create(2585, 4344, 0), Location.create(2573, 4335, 0), Location.create(2570, 4325, 0), Location.create(2567, 4313, 0), Location.create(2566, 4299, 0), Location.create(2573, 4292, 0), Location.create(2603, 4342, 0), Location.create(2616, 4318, 0), Location.create(2616, 4337, 0), Location.create(2584, 4297, 0), Location.create(2596, 4310, 0), Location.create(2596, 4310, 0), Location.create(2588, 4303, 0) }; - - /** - * The possible locations. - */ - public static final Location[] LOCATIONS = new Location[] { - /* - * Camelot - */ - Location.create(2718 + RANDOM.nextInt(50), 3441 + RANDOM.nextInt(20), 0), - /* - * Duel arena entrance - */ - Location.create(3300 + RANDOM.nextInt(10), 3250 + RANDOM.nextInt(10), 0), - /* - * Castle wars entrance - */ - Location.create(2478 + RANDOM.nextInt(20), 3068 + RANDOM.nextInt(20), 0), - /* - * Lumbridge - */ - Location.create(3175 + RANDOM.nextInt(10), 3261 + RANDOM.nextInt(10), 0), - /* - * Draynor - */ - Location.create(3092 + RANDOM.nextInt(5), 3227 + RANDOM.nextInt(5), 0), - /* - * Falador (east) - */ - Location.create(3071 + RANDOM.nextInt(10), 3355 + RANDOM.nextInt(10), 0), - /* - * Varrock (west) - */ - Location.create(3131 + RANDOM.nextInt(5), 3401 + RANDOM.nextInt(5), 0), - /* - * Ardougne (south) - */ - Location.create(2630 + RANDOM.nextInt(10), 3217 + RANDOM.nextInt(10), 0), - /* - * Yanille (north) - */ - Location.create(2594 + RANDOM.nextInt(20), 3121 + RANDOM.nextInt(20), 0), - /* - * Red salamander hunting place (ZMI altar) - */ - Location.create(2436 + RANDOM.nextInt(15), 3206 + RANDOM.nextInt(15), 0), - /* - * Shilo hunting area (graahk/larupia/..) - */ - Location.create(2763 + RANDOM.nextInt(20), 3003 + RANDOM.nextInt(20), 0), - /* - * Rimmington - */ - Location.create(2947 + RANDOM.nextInt(20), 3219 + RANDOM.nextInt(20), 0), - /* - * Karamja volcano - */ - Location.create(2856 + RANDOM.nextInt(15), 3150 + RANDOM.nextInt(15), 0), - /* - * Brimhaven dungeon entrance - */ - Location.create(2733 + RANDOM.nextInt(10), 3146 + RANDOM.nextInt(10), 0), - /* - * Sophanem - */ - Location.create(3299 + RANDOM.nextInt(5), 2784 + RANDOM.nextInt(5), 0), - /* - * Tree gnome stronghold - */ - Location.create(2438 + RANDOM.nextInt(20), 3420 + RANDOM.nextInt(20), 0), - /* - * Zanaris (center, just north of grain field) - */ - Location.create(2416 + RANDOM.nextInt(3), 4456 + RANDOM.nextInt(3), 0), }; - - /** - * Spawns the imps. - */ - private void spawnImps() { - int amount = BNetTypes.getImplings().size() + 2; - for (ImplingNode impling : BNetTypes.getImplings()) { - if (impling.getLevel() == 83) { - int rand = RandomFunction.random(1, 2); - for (int i = 0; i < rand; i++) { - createImp(impling, true); - } - int randAmt = RandomFunction.random(2, 5); - for (int i = 0; i < randAmt; i++) { - createImp(impling, false); - } - continue; - } - for (int i = 0; i < amount; i++) { - if (RANDOM.nextInt(10) < 4) { - createImp(impling, false); - continue; - } - createImp(impling, true); - } - amount--; - } - } - - /** - * Creates the impling. - * @param impling the impling. - * @param puro the puro. - */ - private void createImp(ImplingNode impling, boolean puro) { - ImplingNPC npc = null; - if (puro) { - npc = new ImplingNPC(impling.getNpcs()[1], PURO_LOCATIONS[RANDOM.nextInt(PURO_LOCATIONS.length)], impling); - npc.setAttribute("puroPuro", true); - } else { - npc = new ImplingNPC(impling.getNpcs()[0], LOCATIONS[RANDOM.nextInt(LOCATIONS.length)], impling); - npc.setAttribute("nextTeleport", GameWorld.getTicks() + 600); - } - npc.init(); - } - - @Override - public void startup() { - spawnImps(); - } - -} diff --git a/Server/src/main/content/global/skill/hunter/ImplingNPC.java b/Server/src/main/content/global/skill/hunter/ImplingNPC.java deleted file mode 100644 index f9f7fff71..000000000 --- a/Server/src/main/content/global/skill/hunter/ImplingNPC.java +++ /dev/null @@ -1,120 +0,0 @@ -package content.global.skill.hunter; - -import content.global.skill.hunter.bnet.ImplingNode; -import core.game.node.entity.Entity; -import core.game.node.entity.combat.BattleState; -import core.game.node.entity.combat.CombatStyle; -import core.game.node.entity.npc.AbstractNPC; -import core.game.node.entity.player.Player; -import core.game.system.task.Pulse; -import core.game.world.map.Location; -import core.game.world.map.path.Pathfinder; -import core.game.world.update.flag.context.Graphics; -import core.plugin.Initializable; -import core.tools.RandomFunction; -import core.game.system.config.NPCConfigParser; -import core.game.world.GameWorld; - -/** - * Handles an impling npc. - * @author Vexia - */ -@Initializable -public final class ImplingNPC extends AbstractNPC { - - /** - * The impling node. - */ - private final ImplingNode impling; - - /** - * Constructs a new {@code ImplingNPC} {@code Object}. - */ - public ImplingNPC() { - super(0, null); - this.impling = null; - this.setWalks(true); - this.setWalkRadius(20); - } - - /** - * Constructs a new {@code ImplingNPC} {@code Object}. - * @param id the id. - * @param location the location. - */ - public ImplingNPC(int id, Location location, ImplingNode impling) { - super(id, location); - this.impling = impling; - if (impling != null) { - this.getDefinition().getHandlers().put(NPCConfigParser.RESPAWN_DELAY, impling.getRespawnTime()); - } - } - - @Override - public AbstractNPC construct(int id, Location location, Object... objects) { - return new ImplingNPC(id, location, null); - } - - @Override - public void handleTickActions() { - if (!getLocks().isMovementLocked()) { - if (isWalks() && !getPulseManager().hasPulseRunning() && nextWalk < GameWorld.getTicks()) { - setNextWalk(); - Location l = getLocation().transform(-5 + RandomFunction.random(getWalkRadius()), -5 + RandomFunction.random(getWalkRadius()), 0); - if (canMove(l)) { - Pathfinder.find(this, l, true, Pathfinder.PROJECTILE).walk(this); - } - } - } - if (RandomFunction.random(100) < 4) { - sendChat("Tee hee!"); - } - int nextTeleport = getAttribute("nextTeleport", -1); - if (nextTeleport > -1 && GameWorld.getTicks() > nextTeleport) { - setAttribute("nextTeleport", GameWorld.getTicks() + 600); - graphics(new Graphics(590)); - GameWorld.getPulser().submit(new Pulse(1) { - @Override - public boolean pulse() { - teleport(ImpetuousImpulses.LOCATIONS[RandomFunction.random(ImpetuousImpulses.LOCATIONS.length)]); - return true; - } - }); - return; - } - } - - @Override - public boolean isAttackable(Entity entity, CombatStyle style, boolean message) { - if (style != CombatStyle.MAGIC) { - return false; - } - if (entity.getProperties().getSpell().getSpellId() == 12 || entity.getProperties().getSpell().getSpellId() == 30 || entity.getProperties().getSpell().getSpellId() == 56) { - return true; - } - return super.isAttackable(entity, style, message); - } - - @Override - public void handleDrops(Player p, Entity killer) { - getProperties().setTeleportLocation(getProperties().getSpawnLocation()); - } - - @Override - public void checkImpact(BattleState state) { - } - - @Override - public int[] getIds() { - return new int[] { 1028, 6055, 1029, 6056, 1030, 6057, 1031, 6058, 1032, 6059, 1033, 6060, 1034, 6061, 1035, 6062, 6053, 6063}; - } - - /** - * Gets the impling. - * @return The impling. - */ - public ImplingNode getImpling() { - return impling; - } - -} diff --git a/Server/src/main/content/global/skill/hunter/implings/ImplingBehavior.kt b/Server/src/main/content/global/skill/hunter/implings/ImplingBehavior.kt new file mode 100644 index 000000000..6f880d86d --- /dev/null +++ b/Server/src/main/content/global/skill/hunter/implings/ImplingBehavior.kt @@ -0,0 +1,84 @@ +package content.global.skill.hunter.implings + +import core.api.* +import core.tools.* +import core.game.world.map.RegionManager +import core.game.world.map.path.ClipMaskSupplier +import core.game.node.entity.npc.* +import core.game.node.entity.Entity +import core.game.node.entity.player.Player +import core.game.node.entity.player.link.SpellBookManager; +import core.game.node.item.Item +import core.game.node.entity.combat.CombatStyle +import content.global.skill.magic.spellconsts.Modern + +/** + * Manages the behavior for implings themselves. +*/ +class ImplingBehavior : NPCBehavior (*Impling.getIds()) { + override fun onCreation (self: NPC) { + self.isWalks = true + self.isNeverWalks = false + self.properties.spawnLocation = self.location + } + + override fun tick(self: NPC) : Boolean { + if (RandomFunction.roll(10)) + sendChat(self, "Tee-hee!") + return true + } + + override fun onRespawn(self: NPC) { + if (!isPuroImpling(self)) + log (this::class.java, Log.ERR, "Non-puro impling has respawned!") + sendGraphics(1119, self.properties.teleportLocation) + } + + override fun canBeAttackedBy(self: NPC, attacker: Entity, style: CombatStyle, shouldSendMessage: Boolean) : Boolean { + if (attacker !is Player) return false; + + if (style != CombatStyle.MAGIC) { + if (shouldSendMessage) + sendMessage (attacker, "You can't do that.") + return false; + } + + val spellBook = attacker.spellBookManager.spellBook + if (spellBook != SpellBookManager.SpellBook.MODERN.interfaceId) { + if (shouldSendMessage) + sendMessage (attacker, "The impling is too fast for that.") + return false; + } + + val spellId = attacker.properties.spell.spellId + if (spellId != Modern.BIND && spellId != Modern.SNARE && spellId != Modern.ENTANGLE) { + if (shouldSendMessage) + sendMessage (attacker, "The impling is immune to that magic.") + return false + } + + return true + } + + override fun onDeathFinished (self: NPC, killer: Entity) { + if (!isPuroImpling(self)) + ImplingController.deregister(self) + else if (self.originalId != self.id) { //if this is a spawner that transformed + self.reTransform() //turn back into spawner NPC + self.behavior = forId(self.id) + } + } + + //manually clear any rolled drops so any drops set by JSON are properly ignored. + override fun onDropTableRolled (self: NPC, drops: ArrayList) { + drops.clear() + } + + override fun getClippingSupplier (self: NPC) : ClipMaskSupplier { + return ImplingClipper + } + + private fun isPuroImpling (self: NPC) : Boolean { + return self.id > 6054 + } +} diff --git a/Server/src/main/content/global/skill/hunter/implings/ImplingController.kt b/Server/src/main/content/global/skill/hunter/implings/ImplingController.kt new file mode 100644 index 000000000..c00604ab4 --- /dev/null +++ b/Server/src/main/content/global/skill/hunter/implings/ImplingController.kt @@ -0,0 +1,90 @@ +package content.global.skill.hunter.implings + +import core.api.* +import core.tools.* +import core.game.world.map.Location +import core.game.node.entity.npc.NPC +import core.game.system.command.* +import kotlin.math.* + +/** + * Manages the high-level behavior of implings in the overworld: Spawning them, clearing them, etc. + * NOTE: This does not manage the spawns INSIDE puro-puro. Those are actually handled by JSON. +*/ +class ImplingController : TickListener, Commands { + override fun tick() { + if (--nextCycle > getTicksBeforeNextCycleToDespawn()) + return + if (activeImplings.size > 0) { + clearSomeImplings(implingsClearedPerTick) + return + } + generateSpawners() + nextCycle = secondsToTicks(60 * 30) // 30 minutes + } + + override fun defineCommands() { + define ("implings", Privilege.ADMIN, "", "Lists the currently active implings/spawners") { player, _ -> + for (i in 0..310) + setInterfaceText(player, "", 275, i) + setInterfaceText(player, "Implings", 275, 2) + for ((index, impling) in activeImplings.withIndex()) { + var text = "This shouldn't be here -> ${impling.id}" + if (impling.id < 1028) { + val table = ImplingSpawner.forId(impling.id) + if (table != null) + text = table.name + } + else text = impling.name + setInterfaceText(player, "$text -> ${impling.location}", 275, index + 11) + } + openInterface(player, 275) + } + } + + companion object { + val implingsClearedPerTick = 5 + + var nextCycle = 0 + var activeImplings = ArrayList() + + fun clearSomeImplings (amount: Int) { + for (i in 0 until amount) { + val impling = activeImplings.removeAt(0) + poofClear(impling) + } + } + + fun generateSpawners () { + val typeLocations = ImplingSpawnLocations.values() + for (set in typeLocations) { + val locations = set.locations + val type = set.type + locations.forEach { generateSpawnersAt(it, type) } + } + } + + fun generateSpawnersAt(location: Location, type: ImplingSpawnTypes) { + for (i in 0 until type.spawnRolls) { + val spawner = type.table.roll() ?: continue + if (spawner == ImplingSpawner.Nothing) continue + val npc = NPC.create (spawner.npcId, location) + npc.init() + activeImplings.add(npc) + } + } + + fun getTicksBeforeNextCycleToDespawn() : Int { + return ceil (activeImplings.size / implingsClearedPerTick.toDouble()).toInt() + } + + fun deregister (impling: NPC, graceful: Boolean = false) : Boolean { + activeImplings.remove(impling) + if (graceful) + poofClear(impling) + else + impling.clear() + return true + } + } +} diff --git a/Server/src/main/content/global/skill/hunter/implings/ImplingData.kt b/Server/src/main/content/global/skill/hunter/implings/ImplingData.kt new file mode 100644 index 000000000..a0777d980 --- /dev/null +++ b/Server/src/main/content/global/skill/hunter/implings/ImplingData.kt @@ -0,0 +1,178 @@ +package content.global.skill.hunter.implings + +import core.api.utils.* +import core.game.world.map.* +import core.game.world.map.path.* +import core.game.world.map.build.* + +enum class Impling (val npcId: Int, val puroId: Int) { + Baby (1028, 6055), + Young (1029, 6056), + Gourmet (1030, 6057), + Earth (1031, 6058), + Essence (1032, 6059), + Eclectic (1033, 6060), + Ninja (6053, 6063), + Nature (1034, 6061), + Magpie (1035, 6062), + Dragon (6054, 6064); + + companion object { + fun getIds() : IntArray { + val list = ArrayList() + for (imp in values()){ + list.add(imp.npcId) + list.add(imp.puroId) + } + return list.toIntArray() + } + } +} + +enum class ImplingSpawner (val npcId: Int, val table: WeightedTable) { + LowTier (npcId = 1024, WeightedTable.create + ( + Pair (Impling.Baby, 20.0), + Pair (Impling.Young, 20.0), + Pair (Impling.Gourmet, 20.0), + Pair (Impling.Earth, 20.0), + Pair (Impling.Essence, 10.0), + Pair (Impling.Eclectic,10.0) + )), + MidTier (npcId = 1025, WeightedTable.create + ( + Pair (Impling.Gourmet, 10.0), + Pair (Impling.Earth, 10.0), + Pair (Impling.Essence, 20.0), + Pair (Impling.Eclectic, 37.0), + Pair (Impling.Nature, 20.0), + Pair (Impling.Magpie, 2.0), + Pair (Impling.Ninja, 1.0) + )), + HighTier (npcId = 1026, WeightedTable.create + ( + Pair (Impling.Nature, 10.0), + Pair (Impling.Magpie, 50.0), + Pair (Impling.Ninja, 30.0), + Pair (Impling.Dragon, 10.0) + )), + LowPuroTier (npcId = 6065, WeightedTable.create + ( + Pair (Impling.Baby, 20.0), + Pair (Impling.Young, 20.0), + Pair (Impling.Gourmet, 20.0), + Pair (Impling.Earth, 20.0), + Pair (Impling.Essence, 10.0), + Pair (Impling.Eclectic,10.0) + )), + MidPuroTier (npcId = 6066, WeightedTable.create + ( + Pair (Impling.Gourmet, 10.0), + Pair (Impling.Earth, 10.0), + Pair (Impling.Essence, 20.0), + Pair (Impling.Eclectic, 37.0), + Pair (Impling.Nature, 20.0), + Pair (Impling.Magpie, 2.0), + Pair (Impling.Ninja, 1.0) + )), + HighPuroTier (npcId = 6067, WeightedTable.create + ( + Pair (Impling.Nature, 150.0), + Pair (Impling.Magpie, 114.0), + Pair (Impling.Ninja, 37.0), + Pair (Impling.Dragon, 10.0), + )), + Nothing (npcId = -1, WeightedTable()); + + companion object { + private val idMap = values().map { it.npcId to it }.toMap() + + fun forId (id: Int) : ImplingSpawner? { + return idMap[id] + } + + fun getIds() : IntArray { + return idMap.keys.toIntArray() + } + } +} + +enum class ImplingSpawnTypes (val table: WeightedTable, val spawnRolls: Int) { + Standard (WeightedTable.create + ( + Pair (ImplingSpawner.LowTier, 14.0), + Pair (ImplingSpawner.MidTier, 7.0), + Pair (ImplingSpawner.HighTier, 4.0), + Pair (ImplingSpawner.Nothing, 75.0) + ), spawnRolls = 3), + LowTierOnly (WeightedTable.create + ( + Pair (ImplingSpawner.LowTier, 100.0) + ), spawnRolls = 1), + MidTierOnly (WeightedTable.create + ( + Pair (ImplingSpawner.MidTier, 100.0) + ), spawnRolls = 1), + HighTierOnly (WeightedTable.create + ( + Pair (ImplingSpawner.HighTier, 100.0) + ), spawnRolls = 1), + HighPuroTierOnly (WeightedTable.create + ( + Pair (ImplingSpawner.HighPuroTier, 100.0) + ), spawnRolls = 1) +} + +enum class ImplingSpawnLocations (val type: ImplingSpawnTypes, vararg val locations: Location) { + StandardSpawns (ImplingSpawnTypes.Standard, + Location.create(2204, 3232, 0), + Location.create(2582, 2974, 0), + Location.create(2522, 3105, 0), + Location.create(2470, 3221, 0), + Location.create(2593, 3251, 0), + Location.create(2735, 3354, 0), + Location.create(2646, 3424, 0), + Location.create(2462, 3429, 0), + Location.create(2386, 3513, 0), + Location.create(2335, 3649, 0), + Location.create(2740, 3536, 0), + Location.create(2654, 3609, 0), + Location.create(2724, 3769, 0), + Location.create(2817, 3513, 0), + Location.create(2844, 3154, 0), + Location.create(2844, 3033, 0), + Location.create(2841, 2926, 0), + Location.create(2907, 3491, 0), + Location.create(3020, 3525, 0), + Location.create(3021, 3424, 0), + Location.create(2981, 3276, 0), + Location.create(3135, 3377, 0), + Location.create(3149, 3233, 0), + Location.create(3170, 3004, 0), + Location.create(3239, 3289, 0), + Location.create(3287, 3271, 0), + Location.create(3418, 3124, 0), + Location.create(3356, 3010, 0), + Location.create(3550, 3529, 0), + Location.create(3449, 3488, 0), + Location.create(3441, 3352, 0) + ), + LowTierOnlySpawns (ImplingSpawnTypes.LowTierOnly, + Location.create(2348, 3610, 0), + Location.create(2277, 3186, 0), + Location.create(2459, 3085, 0), + Location.create(2564, 3393, 0), + Location.create(2780, 3463, 0), + Location.create(2966, 3411, 0), + Location.create(3094, 3237, 0), + Location.create(3281, 3427, 0), + Location.create(3278, 3160, 0) + ) +} + +object ImplingClipper : ClipMaskSupplier { + override fun getClippingFlag (z: Int, x: Int, y: Int) : Int { + var flag = RegionManager.getClippingFlag(z, x, y); + return flag and (RegionFlags.SOLID_TILE.inv()) and (RegionFlags.TILE_OBJECT.inv()) //Allow walking on water and flying over small objects, but keep all other tile flags the same. + } +} diff --git a/Server/src/main/content/global/skill/hunter/implings/ImplingSpawnerBehavior.kt b/Server/src/main/content/global/skill/hunter/implings/ImplingSpawnerBehavior.kt new file mode 100644 index 000000000..d00142473 --- /dev/null +++ b/Server/src/main/content/global/skill/hunter/implings/ImplingSpawnerBehavior.kt @@ -0,0 +1,52 @@ +package content.global.skill.hunter.implings + +import core.api.* +import core.tools.* +import core.game.node.entity.npc.* +import core.game.world.map.path.ClipMaskSupplier + +/** + * Manages the behavior for impling spawners (the invisible NPCs that turn into random implings). +*/ +class ImplingSpawnerBehavior : NPCBehavior (*ImplingSpawner.getIds()) { + override fun onCreation (self: NPC) { + val isPuro = isPuroSpawner(self) + val delay = if (isPuro) 120 else 180 + setAttribute(self, "transformTime", getWorldTicks() + secondsToTicks(delay)) + self.isRespawn = isPuro + self.walkRadius = if (isPuro) 20 else 100 + self.isWalks = true + self.isNeverWalks = false + self.setInvisible(true) + self.properties.spawnLocation = self.location + } + + override fun onRespawn (self: NPC) { + if (!isPuroSpawner(self)) + log (this::class.java, Log.ERR, "Non-puro spawner has respawned!") + this.onCreation(self) + } + + override fun tick (self: NPC) : Boolean { + var transformTime = getAttribute(self, "transformTime", 0) + if (transformTime != 0 && transformTime <= getWorldTicks()) { + val table = ImplingSpawner.forId(self.id)?.table ?: return ImplingController.deregister(self) + val impling = table.roll() ?: return ImplingController.deregister(self) + val targetId = if (isPuroSpawner(self)) impling.puroId else impling.npcId + self.transform(targetId) + self.behavior = forId(self.id) + self.setInvisible(false) + removeAttribute(self, "transformTime") + sendGraphics(1119, self.location) + } + return true + } + + override fun getClippingSupplier (self: NPC) : ClipMaskSupplier { + return ImplingClipper + } + + private fun isPuroSpawner(self: NPC) : Boolean { + return self.id > 6000 + } +} diff --git a/Server/src/main/content/global/skill/magic/spellconsts/Modern.kt b/Server/src/main/content/global/skill/magic/spellconsts/Modern.kt index 8ca73ab62..6696eb19b 100644 --- a/Server/src/main/content/global/skill/magic/spellconsts/Modern.kt +++ b/Server/src/main/content/global/skill/magic/spellconsts/Modern.kt @@ -5,6 +5,7 @@ object Modern { const val CONFUSE = 2 const val BONES_TO_BANANAS = 9 const val WEAKEN = 11 + const val BIND = 12 const val LOW_ALCHEMY = 13 const val VARROCK_TELEPORT = 15 const val LUMBRIDGE_TELEPORT = 18 @@ -13,6 +14,7 @@ object Modern { const val SUPERHEAT = 25 const val CAMELOT_TELEPORT = 26 const val TELEPORT_TO_HOUSE = 23 + const val SNARE = 30 const val ARDOUGNE_TELEPORT = 32 const val HIGH_ALCHEMY = 34 const val WATCHTOWER_TELEPORT = 37 @@ -21,5 +23,6 @@ object Modern { const val APE_ATOLL_TELEPORT = 47 const val VULNERABILITY = 50 const val ENFEEBLE = 53 + const val ENTANGLE = 56 const val STUN = 57 -} \ No newline at end of file +} diff --git a/Server/src/main/content/global/skill/slayer/NechryaelBehavior.kt b/Server/src/main/content/global/skill/slayer/NechryaelBehavior.kt index 3920cbbeb..da44565a0 100644 --- a/Server/src/main/content/global/skill/slayer/NechryaelBehavior.kt +++ b/Server/src/main/content/global/skill/slayer/NechryaelBehavior.kt @@ -83,8 +83,7 @@ class DeathspawnBehavior : NPCBehavior(NPCs.DEATH_SPAWN_1614) { override fun onRemoval(self: NPC) { val parent = getAttribute(self, "parent", null) ?: return - if (parent.behavior !is NechryaelBehavior) return - parent.behavior.removeSpawn(parent, self) + (parent.behavior as? NechryaelBehavior)?.let { it.removeSpawn (parent, self) } } override fun tick(self: NPC): Boolean { @@ -102,4 +101,4 @@ class DeathspawnBehavior : NPCBehavior(NPCs.DEATH_SPAWN_1614) { override fun canBeAttackedBy(self: NPC, attacker: Entity, style: CombatStyle, shouldSendMessage: Boolean): Boolean { return attacker == getAttribute(self, "target", null) } -} \ No newline at end of file +} diff --git a/Server/src/main/content/minigame/puropuro/CropCircleController.kt b/Server/src/main/content/minigame/puropuro/CropCircleController.kt new file mode 100644 index 000000000..0584e4af8 --- /dev/null +++ b/Server/src/main/content/minigame/puropuro/CropCircleController.kt @@ -0,0 +1,97 @@ +package content.minigame.puropuro + +import core.api.* +import core.game.interaction.* +import core.game.world.map.Location +import core.game.node.scenery.Scenery +import core.game.node.entity.player.Player +import core.game.node.entity.player.link.TeleportManager.TeleportType + +class CropCircleController : TickListener, InteractionListener, LoginListener { + override fun tick () { + if (getWorldTicks() < nextCircle) + return + deconstructOldCircle() + + val (name, loc) = possibleLocations.random() + constructCircle(loc) + + sendNews("A crop circle has appeared near $name.") + nextCircle = getWorldTicks() + 1500 + currentLocName = name + } + + override fun defineListeners() { + on (center, IntType.SCENERY, "enter") {player, _ -> + if (hasImpBox(player)) { + sendDialogue (player, "Something prevents you from entering. You think the portal is offended by your imp box. They are not popular on imp and impling planes.") + return@on true + } + teleport (player, puroLocation, TeleportType.PURO_PURO) + setAttribute (player, exitLocation, Location.create(player.location)) + return@on true + } + + on (puroExit, IntType.SCENERY, "leave", "quick-leave") {player, _ -> + var exit = getAttribute (player, exitLocation, Location.create(3158, 3300, 0)) + teleport (player, exit, TeleportType.PURO_PURO) + return@on true + } + } + + override fun login (player: Player) { + sendMessage (player, "A crop circle is active near $currentLocName.") + } + + private fun constructCircle (location: Location) { + activeObjects.add ( + addScenery ( + center, + location, + rotation = 0, + type = 10 + ) + ) + for ((index, tile) in location.surroundingTiles.withIndex()) { + activeObjects.add ( + addScenery ( + surrounding[index % 4], + tile, + rotation = (index / 4) * 2, + type = 10 + ) + ) + } + } + + private fun deconstructOldCircle() { + for (scenery in activeObjects) + removeScenery(scenery) + activeObjects.clear() + } + + private fun hasImpBox (player: Player) : Boolean { + return inInventory(player, 10025) || inInventory(player, 10027) || inInventory(player, 10028) + } + + companion object { + var currentLocName = "" + val exitLocation = "/save:puro-exit" + val possibleLocations = arrayOf ( + Pair ("Doric's Hut", Location.create(2953, 3444, 0)), + Pair ("Yanille", Location.create(2583, 3104, 0)), + Pair ("Draynor", Location.create(3113, 3274, 0)), + Pair ("Rimmington", Location.create(2978, 3216, 0)), + Pair ("The Grand Exchange", Location.create(3141, 3461, 0)), + Pair ("Northern Lumbridge", Location.create(3160, 3296, 0)), + Pair ("Southern Varrock", Location.create(3218, 3348, 0)), + Pair ("Northern Ardougne", Location.create(2644, 3350, 0)) + ) + val activeObjects = ArrayList() + val surrounding = arrayOf (24984, 24985, 24986, 24987) + val center = 24991 + val puroExit = 25014 + val puroLocation = Location.create(2591, 4320, 0) + var nextCircle = 0 + } +} diff --git a/Server/src/main/content/minigame/puropuro/ImpDefenderBehavior.kt b/Server/src/main/content/minigame/puropuro/ImpDefenderBehavior.kt new file mode 100644 index 000000000..2c1bb509e --- /dev/null +++ b/Server/src/main/content/minigame/puropuro/ImpDefenderBehavior.kt @@ -0,0 +1,93 @@ +package content.minigame.puropuro + +import core.api.* +import core.tools.* +import core.game.node.entity.npc.* +import core.game.node.entity.player.Player +import core.game.node.entity.skill.Skills +import core.game.node.item.GroundItemManager +import core.game.node.item.Item +import core.game.world.map.zone.ZoneBorders +import core.game.world.map.RegionManager +import core.game.system.task.Pulse +import content.global.skill.hunter.bnet.BNetTypes + +import org.rs09.consts.Items + +class ImpDefenderBehavior : NPCBehavior(6074) { + override fun onCreation (self: NPC) { + self.isWalks = true + self.isNeverWalks = false + self.walkRadius = 30 + } + + override fun tick (self: NPC) : Boolean { + if (!RandomFunction.roll(10)) return true + + var nextCaptureTick = getAttribute(self, "next-capture-tick", 0) + if (getWorldTicks() < nextCaptureTick) return true + + var players = RegionManager.getLocalPlayers(self, 2) + for (player in players) { + var lowestTierImpling = BNetTypes.getImpling(player) + if (lowestTierImpling == null) continue + var jarItem = lowestTierImpling.reward + setAttribute(self, "capture-target", player) + setAttribute(self, "capture-item", jarItem) + setAttribute(self, "next-capture-tick", getWorldTicks() + RandomFunction.random(25, 100)) + submitIndividualPulse(self, TryReleasePulse(self)) + break + } + + return true + } + + private class TryReleasePulse(val self: NPC) : Pulse() { + companion object { + const val catchPlayerLow = 35.0 //14% chance to avoid catch at level 1 + const val catchPlayerHigh = 280.0 //100% chance to avoid catch at level 90 (level 82 with imp repellent) + const val impRepellentBonus = 20.0 //10% bonus if player has imp repellent + } + + var counter = 0 + override fun pulse() : Boolean { + val player: Player? = getAttribute(self, "capture-target", null) + val jarItem: Item? = getAttribute(self, "capture-item", null) + if (player == null || jarItem == null) return true + when (counter++) { + 0 -> { + face (self, player) + animate (self, 6628) + setDelay(animationDuration(getAnimation(6628))) + return false + } + 1 -> { + var hasRepellent = inInventory(player, Items.IMP_REPELLENT_11262) + var baseRoll = RandomFunction.randomDouble(100.0) + var playerRoll = RandomFunction.getSkillSuccessChance( + catchPlayerLow + if (hasRepellent) impRepellentBonus else 0.0, + catchPlayerHigh + if (hasRepellent) impRepellentBonus else 0.0, + getStatLevel(player, Skills.THIEVING) + ) + if (playerRoll < baseRoll) { + sendChat (self, "Be free!") + animate (self, 6629) + removeItem (player, jarItem) + var loc = ZoneBorders ( + self.location.x - 2, + self.location.y - 2, + self.location.x + 2, + self.location.y + 2 + ).randomLoc + GroundItemManager.create( + Item(Items.IMPLING_JAR_11260), + loc, player + ) + } + resetFace (self) + } + } + return true + } + } +} diff --git a/Server/src/main/content/minigame/puropuro/ImpDefenderNPC.java b/Server/src/main/content/minigame/puropuro/ImpDefenderNPC.java deleted file mode 100644 index 948a6f4ac..000000000 --- a/Server/src/main/content/minigame/puropuro/ImpDefenderNPC.java +++ /dev/null @@ -1,112 +0,0 @@ -package content.minigame.puropuro; - -import java.util.List; -import java.util.Random; - -import core.game.node.entity.skill.Skills; -import content.global.skill.hunter.bnet.BNetTypes; -import content.global.skill.hunter.bnet.ImplingNode; -import core.game.node.entity.npc.AbstractNPC; -import core.game.node.entity.player.Player; -import core.game.node.item.Item; -import core.game.world.GameWorld; -import core.game.world.map.Location; -import core.game.world.map.RegionManager; -import core.tools.RandomFunction; - -/** - * Handles the imp defender npc. - * @author Vexia - */ -public final class ImpDefenderNPC extends AbstractNPC { - - /** - * Constructs a new {@code ImpDefenderNPC} {@code Object}. - */ - public ImpDefenderNPC() { - super(0, null); - } - - /** - * Constructs a new {@code ImpDefenderNPC} {@code Object}. - * @param id the id. - * @param location the location. - */ - public ImpDefenderNPC(int id, Location location) { - super(id, location); - } - - @Override - public void handleTickActions() { - super.handleTickActions(); - if (RandomFunction.random(250) <= 3) { - stealImp(); - } - } - - /** - * Attempts to steal an imp. - */ - private void stealImp() { - List players = RegionManager.getLocalPlayers(this, 3); - for (Player player : players) { - if (canSteal(player)) { - steal(player); - } - break; - } - } - - /** - * Steals an imp from a player. - * @param player the player. - */ - private void steal(Player player) { - if (player == null) { - return; - } - ImplingNode node = BNetTypes.getImpling(player); - if (node != null) { - faceTemporary(player, 1); - sendChat("Be free!"); - player.getInventory().remove(node.getReward()); - player.setAttribute("imp-steal", GameWorld.getTicks() + 500); - } - } - - /** - * Checks if the imp can steal from the player. - * @param player the player. - * @return {@code True} if so. - */ - private boolean canSteal(Player player) { - if (!GameWorld.getSettings().isDevMode() && player.getAttribute("imp-steal", 0) > GameWorld.getTicks()) { - return false; - } - int thievingLevel = player.getSkills().getLevel(Skills.THIEVING); - if (thievingLevel >= 60) { - return false; - } - int level = thievingLevel - 10; - if (level <= 0) { - level = 5; - } - if (player.getInventory().containsItem(new Item(11262))) { - thievingLevel += 15; - } - int currentLevel = RandomFunction.random(thievingLevel) + 1; - double ratio = (double) currentLevel / (new Random().nextInt(level + 5) + 1); - return Math.round(ratio * thievingLevel) < level; - } - - @Override - public AbstractNPC construct(int id, Location location, Object... objects) { - return new ImpDefenderNPC(id, location); - } - - @Override - public int[] getIds() { - return new int[] { 6074 }; - } - -} diff --git a/Server/src/main/content/minigame/puropuro/PuroPuroPlugin.java b/Server/src/main/content/minigame/puropuro/PuroPuroPlugin.java index 4afa73a7e..08a535fed 100644 --- a/Server/src/main/content/minigame/puropuro/PuroPuroPlugin.java +++ b/Server/src/main/content/minigame/puropuro/PuroPuroPlugin.java @@ -35,6 +35,10 @@ import core.plugin.Plugin; import core.plugin.ClassScanner; import core.tools.RandomFunction; +import static core.api.ContentAPIKt.animateScenery; +import static core.api.ContentAPIKt.submitWorldPulse; +import static core.api.ContentAPIKt.animationDuration; + /** * Handles the puro puro activity. * @author Vexia @@ -78,7 +82,6 @@ public final class PuroPuroPlugin extends MapZone implements Plugin { ClassScanner.definePlugin(new WanderingImplingDialogue()); ClassScanner.definePlugin(new ElnockInquisitorDialogue()); ClassScanner.definePlugin(new PuroOptionHandler()); - ClassScanner.definePlugin(new ImpDefenderNPC()); return this; } @@ -128,10 +131,7 @@ public final class PuroPuroPlugin extends MapZone implements Plugin { case 25021: pushThrough(p, (Scenery) target); return true; - case 25014: - p.getTeleporter().send(Location.create(2427, 4446, 0), TeleportType.PURO_PURO); - return true; - } + } } return super.interact(e, target, option); } @@ -213,10 +213,6 @@ public final class PuroPuroPlugin extends MapZone implements Plugin { WHEAT.add(new WheatSet(1, Location.create(2601, 4293, 0), Location.create(2600, 4293, 0))); WHEAT.add(new WheatSet(0, Location.create(2565, 4310, 0), Location.create(2565, 4311, 0))); WHEAT.add(new WheatSet(1, Location.create(2582, 4346, 0), Location.create(2583, 4346, 0))); - WHEAT.add(new WheatSet(0, Location.create(2621, 4328, 0), Location.create(2621, 4329, 0))); - WHEAT.add(new WheatSet(1, Location.create(2602, 4349, 0), Location.create(2603, 4349, 0))); - WHEAT.add(new WheatSet(0, Location.create(2562, 4334, 0), Location.create(2562, 4333, 0))); - WHEAT.add(new WheatSet(1, Location.create(2582, 4290, 0), Location.create(2581, 4290, 0))); WHEAT.add(new WheatSet(0, Location.create(2568, 4348, 0), Location.create(2568, 4347, 0))); WHEAT.add(new WheatSet(0, Location.create(2615, 4347, 0), Location.create(2615, 4348, 0))); WHEAT.add(new WheatSet(0, Location.create(2612, 4345, 0), Location.create(2612, 4344, 0))); @@ -241,7 +237,6 @@ public final class PuroPuroPlugin extends MapZone implements Plugin { @Override public Plugin newInstance(Object arg) throws Throwable { NPCDefinition.forId(6070).getHandlers().put("option:trade", this); - SceneryDefinition.forId(24991).getHandlers().put("option:enter", this); ItemDefinition.forId(11273).getHandlers().put("option:toggle-view", this); ItemDefinition.forId(11258).getHandlers().put("option:butterfly-jar", this); ItemDefinition.forId(11258).getHandlers().put("option:impling-jar", this); @@ -258,14 +253,6 @@ public final class PuroPuroPlugin extends MapZone implements Plugin { case 6070: ElnockInquisitorDialogue.openShop(player); break; - case 24991: - if (hasImplingBox(player)) { - player.getDialogueInterpreter().sendDialogue("Something prevents you from entering. You think the portal is offended by", "your imp boxes. They are not popular on imp and impling planes."); - break; - } - player.sendMessage("Puro Puro is disabled until its rewrite/bugfix update is complete."); - //player.getTeleporter().send(Location.create(2591, 4320, 0), TeleportType.PURO_PURO); - return true; case 11273: if (!player.getZoneMonitor().isInZone("puro puro")) { player.sendMessage("You can only use this in the Puro Puro Maze."); @@ -367,7 +354,6 @@ public final class PuroPuroPlugin extends MapZone implements Plugin { /** * A wheat set. - * @author Vexia */ public static class WheatSet { @@ -434,10 +420,36 @@ public final class PuroPuroPlugin extends MapZone implements Plugin { continue; } if (removed) { + submitWorldPulse(new Pulse() { + int counter = 0; + + @Override + public boolean pulse() { + if (counter++ == 0) { + animateScenery (object, 6596); + setDelay(animationDuration(new Animation(6596))); + return false; + } + return true; + } + }); SceneryBuilder.add(object); continue; } - SceneryBuilder.remove(object); + submitWorldPulse(new Pulse() { + int counter = 0; + + @Override + public boolean pulse() { + if (counter++ == 0) { + animateScenery (object, 6599); + setDelay(animationDuration(new Animation(6599))); + return false; + } + SceneryBuilder.remove(object); + return true; + } + }); } removed = !removed; setNextWhilt(); diff --git a/Server/src/main/core/api/ContentAPI.kt b/Server/src/main/core/api/ContentAPI.kt index 633f6a965..e2bab4e26 100644 --- a/Server/src/main/core/api/ContentAPI.kt +++ b/Server/src/main/core/api/ContentAPI.kt @@ -427,6 +427,28 @@ fun replaceScenery(toReplace: Scenery, with: Int, forTicks: Int, loc: Location? toReplace.isActive = false } +/** + * Add a scenery to the world + * @param sceneryId the ID of the scenery to add + * @param location the location to place it at + * @param rotation the rotation of the scenery (default 0) + * @param type the type of the scenery (default 22) + * @return the created scenery +*/ +fun addScenery (sceneryId: Int, location: Location, rotation: Int = 0, type: Int = 22) : Scenery { + val scenery = Scenery(sceneryId, location, type, rotation) + SceneryBuilder.add(scenery) + return scenery +} + +/** + * Remove a scenery from the world + * @param scenery the Scenery object to remove. +*/ +fun removeScenery (scenery: Scenery) { + SceneryBuilder.remove(scenery) +} + /** * Replace an object with the given revert timer with the given rotation * @param toReplace the Scenery instance we are replacing @@ -642,6 +664,15 @@ fun face(entity: Entity, toFace: Node, duration: Int = -1) { } } +/** + * Causes the given entity to reset its face direction. + * @param entity the entity to reset. +*/ +fun resetFace (entity: Entity) { + entity.face(null) + entity.faceLocation(entity.location) +} + /** * Opens the given interface for the given player * @param player the player to open the interface for diff --git a/Server/src/main/core/api/utils/Vector.kt b/Server/src/main/core/api/utils/Vector.kt new file mode 100644 index 000000000..8c3365f22 --- /dev/null +++ b/Server/src/main/core/api/utils/Vector.kt @@ -0,0 +1,35 @@ +package core.api.utils + +import core.game.world.map.Location +import kotlin.math.* + +class Vector (val x: Double, val y: Double) { + fun normalized() : Vector { + val magnitude = magnitude() + val xComponent = x / magnitude + val yComponent = y / magnitude + return Vector(xComponent, yComponent) + } + + fun magnitude() : Double { + return sqrt(x.pow(2.0) + y.pow(2.0)) + } + + operator fun Vector.unaryMinus() = Vector(-x, -y) + + operator fun times (other: Double) : Vector { + return Vector(this.x * other, this.y * other) + } + + operator fun times (other: Int) : Vector { + return Vector(this.x * other, this.y * other) + } + + companion object { + @JvmStatic fun betweenLocs (from: Location, to: Location) : Vector { + val xDiff = to.x - from.x + val yDiff = to.y - from.y + return Vector (xDiff.toDouble(), yDiff.toDouble()) + } + } +} diff --git a/Server/src/main/core/api/utils/WeightedTable.kt b/Server/src/main/core/api/utils/WeightedTable.kt new file mode 100644 index 000000000..a399b8e91 --- /dev/null +++ b/Server/src/main/core/api/utils/WeightedTable.kt @@ -0,0 +1,61 @@ +package core.api.utils + +import core.tools.* + +/** + * Implementation of a weighted table that supports generic (particularly non-Item) types. +**/ +class WeightedTable : ArrayList>() { + var totalWeight: Double = 0.0 + + fun add (element: T?, weight: Double) : Boolean { + totalWeight += weight + return super.add(Pair(element, weight)) + } + + fun remove (element: T?) : Boolean { + var index = -1 + for ((i, pair) in this.withIndex()) { + val (elem, _) = pair + if (element == elem) { + index = i + break + } + } + if (index == -1) return false + + this.removeAt(index) + return true + } + + override fun removeAt (index: Int) : Pair { + val (_, weight) = this[index] + totalWeight -= weight + return super.removeAt(index) + } + + fun roll() : T? { + if (this.size == 1) return this[0].component1() + else if (this.size == 0) return null + + var shuffled = this.shuffled() + var randWeight = RandomFunction.random(0.0, totalWeight) + + for ((element, weight) in shuffled) { + randWeight -= weight + if (randWeight <= 0) + return element + } + + return null + } + + companion object { + fun create (vararg elements: Pair) : WeightedTable { + var table = WeightedTable() + for ((element, weight) in elements) + table.add (element, weight) + return table + } + } +} diff --git a/Server/src/main/core/game/node/entity/npc/NPC.java b/Server/src/main/core/game/node/entity/npc/NPC.java index e5dd73219..bb3324f47 100644 --- a/Server/src/main/core/game/node/entity/npc/NPC.java +++ b/Server/src/main/core/game/node/entity/npc/NPC.java @@ -32,6 +32,7 @@ import core.game.world.update.flag.npc.NPCForceChat; import core.game.world.update.flag.npc.NPCSwitchId; import core.tools.RandomFunction; import core.api.utils.GlobalKillCounter; +import core.api.utils.Vector; import core.game.shops.Shops; import core.game.node.entity.combat.CombatSwingHandler; import core.game.system.config.NPCConfigParser; @@ -142,7 +143,7 @@ public class NPC extends Entity { */ private String forceTalk; - public final NPCBehavior behavior; + public NPCBehavior behavior; /** * Constructs a new {@code NPC} {@code Object}. @@ -373,7 +374,7 @@ public class NPC extends Entity { @Override public boolean isAttackable(Entity entity, CombatStyle style, boolean message) { - if ((entity instanceof Player && !definition.hasAction("attack")) || isInvisible()) { + if (isInvisible()) { return false; } if (task != null && entity instanceof Player && task.levelReq > entity.getSkills().getStaticLevel(Skills.SLAYER)) { @@ -719,15 +720,10 @@ public class NPC extends Entity { Location returnToSpawnLocation = getProperties().getSpawnLocation().transform(-5 + RandomFunction.random(getWalkRadius()), -5 + RandomFunction.random(getWalkRadius()), 0); int dist = (int) Location.getDistance(location, returnToSpawnLocation); int pathLimit = 15; - if(dist > pathLimit || dist < -pathLimit) - { - int diffX = returnToSpawnLocation.getX() - location.getX(); - int diffY = returnToSpawnLocation.getY() - location.getY(); - returnToSpawnLocation = location.transform( - Math.min(Math.max(-pathLimit, diffX), pathLimit), //Try to path no more than pathLimit and no less than -pathLimit tiles away in the X and Y directions - Math.min(Math.max(-pathLimit, diffY), pathLimit), - 0); - } + if (dist > pathLimit) { + Vector normalizedDir = Vector.betweenLocs(this.location, returnToSpawnLocation).normalized(); + returnToSpawnLocation = this.location.transform (normalizedDir.times(pathLimit)); + } return returnToSpawnLocation; } Location l = movementPath[movementIndex++]; diff --git a/Server/src/main/core/game/node/entity/npc/NPCBehavior.kt b/Server/src/main/core/game/node/entity/npc/NPCBehavior.kt index 8ddfdef4e..f35c41f0a 100644 --- a/Server/src/main/core/game/node/entity/npc/NPCBehavior.kt +++ b/Server/src/main/core/game/node/entity/npc/NPCBehavior.kt @@ -1,11 +1,14 @@ package core.game.node.entity.npc +import core.game.node.item.Item import core.api.ContentInterface import core.game.node.entity.Entity +import core.game.world.map.RegionManager +import core.game.node.entity.player.Player import core.game.node.entity.combat.BattleState import core.game.node.entity.combat.CombatStyle import core.game.node.entity.combat.CombatSwingHandler -import core.game.node.item.Item +import core.game.world.map.path.ClipMaskSupplier open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface { companion object { @@ -19,6 +22,12 @@ open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface { ids.forEach { idMap[it] = behavior } } } + + object StandardClipMaskSupplier : ClipMaskSupplier { + override fun getClippingFlag (z: Int, x: Int, y: Int) : Int { + return RegionManager.getClippingFlag(z,x,y) + } + } /** * Called every tick, before the base NPC tick() method. @@ -102,7 +111,11 @@ open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface { * @param shouldSendMessage whether the core combat code believes you should send a message e.g. "You can't attack this NPC with that weapon" * @return whether the attacker should be able to attack this NPC. */ - open fun canBeAttackedBy(self: NPC, attacker: Entity, style: CombatStyle, shouldSendMessage: Boolean) : Boolean {return true} + open fun canBeAttackedBy(self: NPC, attacker: Entity, style: CombatStyle, shouldSendMessage: Boolean) : Boolean { + if (attacker is Player && !self.definition.hasAction("attack")) + return false + return true + } /** * Called by combat-related code to check if this NPC should ignore multi-combat rules when attempting to attack the given victim. @@ -119,4 +132,11 @@ open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface { * @return the SwingHandler instance to be used for this cycle of combat */ open fun getSwingHandlerOverride(self: NPC, original: CombatSwingHandler) : CombatSwingHandler {return original} -} \ No newline at end of file + + /** + * Called by pathfinding code to determine the clipping mask supplier this NPC should use. You can use this to ignore water, etc. + */ + open fun getClippingSupplier(self: NPC) : ClipMaskSupplier? { + return StandardClipMaskSupplier + } +} diff --git a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt index 5b4e37007..005873719 100644 --- a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt +++ b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt @@ -10,6 +10,7 @@ import core.game.node.entity.player.link.IronmanMode import core.game.node.entity.player.link.SpellBookManager import core.game.node.entity.player.link.emote.Emotes import core.game.node.entity.player.link.music.MusicEntry +import core.game.world.map.Location import org.json.simple.JSONArray import org.json.simple.JSONObject import org.json.simple.parser.JSONParser @@ -122,6 +123,7 @@ class PlayerSaveParser(val player: Player) { "long" -> attr["value"].toString().toLong() "bool" -> attr["value"] as Boolean "byte" -> Base64.getDecoder().decode(attr["value"].toString())[0] + "location" -> Location.fromString(attr["value"].toString()) else -> null.also { log(this::class.java, Log.WARN, "Invalid data type for key: $key in PlayerSaveParser.kt Line 115") } }) } @@ -363,4 +365,4 @@ class PlayerSaveParser(val player: Player) { } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt index 0d7df18c4..70097f229 100644 --- a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt +++ b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt @@ -9,6 +9,7 @@ import core.game.container.Container import core.game.node.entity.player.Player import core.game.node.entity.player.link.IronmanMode import core.game.node.entity.skill.Skills +import core.game.world.map.Location import core.tools.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -116,6 +117,7 @@ class PlayerSaver (val player: Player){ is Short -> "short" is String -> "str" is Byte -> "byte" + is Location -> "location" else -> "null".also { log(this::class.java, Log.WARN, "Invalid attribute type for key: $key in PlayerSaver.kt Line 115") } } attr.put("type",type) diff --git a/Server/src/main/core/game/world/map/Location.java b/Server/src/main/core/game/world/map/Location.java index cfeb61f6a..013c81ff0 100644 --- a/Server/src/main/core/game/world/map/Location.java +++ b/Server/src/main/core/game/world/map/Location.java @@ -6,9 +6,11 @@ import core.game.world.map.path.Path; import core.game.world.map.path.Pathfinder; import core.tools.RandomFunction; import org.jetbrains.annotations.NotNull; +import core.api.utils.Vector; import java.util.ArrayList; import java.util.List; +import java.lang.Math; /** * Represents a location on the world map. @@ -277,14 +279,14 @@ public final class Location extends Node { public ArrayList getSurroundingTiles() { ArrayList locs = new ArrayList<>(); - locs.add(transform(0,1,0)); //N - locs.add(transform(1,1,0)); //NE - locs.add(transform(1,0,0)); //E - locs.add(transform(1,-1,0)); //SE - locs.add(transform(0,-1,0)); //S locs.add(transform(-1,-1,0));//SW - locs.add(transform(-1,0,0));//W + locs.add(transform(0,-1,0)); //S + locs.add(transform(1,-1,0)); //SE + locs.add(transform(1,0,0)); //E + locs.add(transform(1,1,0)); //NE + locs.add(transform(0,1,0)); //N locs.add(transform(-1,1,0));//NW + locs.add(transform(-1,0,0));//W return locs; } @@ -554,4 +556,8 @@ public final class Location extends Node { if (sb.length() == 0) return null; return Direction.valueOf(sb.toString()); } + + public Location transform (Vector vector) { + return Location.create(this.x + (int) Math.floor(vector.getX()), this.y + (int) Math.floor(vector.getY())); + } } diff --git a/Server/src/main/core/game/world/map/path/Pathfinder.java b/Server/src/main/core/game/world/map/path/Pathfinder.java index 1b0dd6d2c..72261150a 100644 --- a/Server/src/main/core/game/world/map/path/Pathfinder.java +++ b/Server/src/main/core/game/world/map/path/Pathfinder.java @@ -2,6 +2,7 @@ package core.game.world.map.path; import core.game.node.Node; import core.game.node.entity.Entity; +import core.game.node.entity.npc.NPC; import core.game.node.item.GroundItem; import core.game.node.scenery.Scenery; import core.game.world.map.Direction; @@ -124,7 +125,13 @@ public abstract class Pathfinder { * @return The path. */ public static Path find(Entity mover, Node destination, boolean near, Pathfinder finder) { - return find(mover.getLocation(), mover.size(), destination, near, finder, RegionManager::getClippingFlag); + ClipMaskSupplier cms = null; + if (mover instanceof NPC) { + cms = ((NPC) mover).behavior.getClippingSupplier(((NPC) mover)); + } + if (cms == null) + cms = RegionManager::getClippingFlag; + return find(mover.getLocation(), mover.size(), destination, near, finder, cms); } public static Path findWater(Entity mover, Node destination, boolean near, Pathfinder finder){