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
This commit is contained in:
Ceikry
2023-04-20 12:16:21 +00:00
committed by Ryan
parent 41c970efd4
commit f6096e3020
24 changed files with 915 additions and 457 deletions
+60
View File
@@ -71544,5 +71544,65 @@
"bonuses": "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0", "bonuses": "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0",
"range_level": "1", "range_level": "1",
"attack_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"
} }
] ]
+36 -24
View File
@@ -2511,30 +2511,6 @@
"npc_id": "1027", "npc_id": "1027",
"loc_data": "{3278,3155,0,0,0}-{2354,3608,0,0,0}" "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", "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}" "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", "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}" "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", "npc_id": "6070",
"loc_data": "{2589,4313,0,1,6}" "loc_data": "{2589,4313,0,1,6}"
@@ -1,6 +1,6 @@
package content.global.skill.farming package content.global.skill.farming
import core.api.toIntArray import core.api.*
import core.game.node.item.Item import core.game.node.item.Item
import core.game.system.task.Pulse import core.game.system.task.Pulse
import core.game.world.update.flag.context.Animation import core.game.world.update.flag.context.Animation
@@ -65,7 +65,7 @@ class UseWithBinHandler : InteractionListener {
if(bin.isFull()){ if(bin.isFull()){
player.sendMessage("This compost bin is already full.") player.sendMessage("This compost bin is already full.")
return@onUseWith true return@onUseWith true
} else { } else if (!bin.isFinished) {
player.pulseManager.run(object : Pulse(fillAnim.duration){ player.pulseManager.run(object : Pulse(fillAnim.duration){
override fun pulse(): Boolean { override fun pulse(): Boolean {
player.animator.animate(fillAnim) player.animator.animate(fillAnim)
@@ -75,6 +75,8 @@ class UseWithBinHandler : InteractionListener {
return bin.isFull() || player.inventory.getAmount(usedNode.asItem()) == 0 return bin.isFull() || player.inventory.getAmount(usedNode.asItem()) == 0
} }
}) })
} else {
sendMessage (player, "You should empty the remaining compost first.")
} }
} }
return@onUseWith true return@onUseWith true
@@ -100,4 +102,4 @@ class UseWithBinHandler : InteractionListener {
if(this != 6476) return this + 2 if(this != 6476) return this + 2
else return Items.VIAL_229 else return Items.VIAL_229
} }
} }
@@ -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();
}
}
@@ -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;
}
}
@@ -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<Item>) {
drops.clear()
}
override fun getClippingSupplier (self: NPC) : ClipMaskSupplier {
return ImplingClipper
}
private fun isPuroImpling (self: NPC) : Boolean {
return self.id > 6054
}
}
@@ -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<NPC>()
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
}
}
}
@@ -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<Int>()
for (imp in values()){
list.add(imp.npcId)
list.add(imp.puroId)
}
return list.toIntArray()
}
}
}
enum class ImplingSpawner (val npcId: Int, val table: WeightedTable<Impling>) {
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<Impling>());
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<ImplingSpawner>, 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.
}
}
@@ -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
}
}
@@ -5,6 +5,7 @@ object Modern {
const val CONFUSE = 2 const val CONFUSE = 2
const val BONES_TO_BANANAS = 9 const val BONES_TO_BANANAS = 9
const val WEAKEN = 11 const val WEAKEN = 11
const val BIND = 12
const val LOW_ALCHEMY = 13 const val LOW_ALCHEMY = 13
const val VARROCK_TELEPORT = 15 const val VARROCK_TELEPORT = 15
const val LUMBRIDGE_TELEPORT = 18 const val LUMBRIDGE_TELEPORT = 18
@@ -13,6 +14,7 @@ object Modern {
const val SUPERHEAT = 25 const val SUPERHEAT = 25
const val CAMELOT_TELEPORT = 26 const val CAMELOT_TELEPORT = 26
const val TELEPORT_TO_HOUSE = 23 const val TELEPORT_TO_HOUSE = 23
const val SNARE = 30
const val ARDOUGNE_TELEPORT = 32 const val ARDOUGNE_TELEPORT = 32
const val HIGH_ALCHEMY = 34 const val HIGH_ALCHEMY = 34
const val WATCHTOWER_TELEPORT = 37 const val WATCHTOWER_TELEPORT = 37
@@ -21,5 +23,6 @@ object Modern {
const val APE_ATOLL_TELEPORT = 47 const val APE_ATOLL_TELEPORT = 47
const val VULNERABILITY = 50 const val VULNERABILITY = 50
const val ENFEEBLE = 53 const val ENFEEBLE = 53
const val ENTANGLE = 56
const val STUN = 57 const val STUN = 57
} }
@@ -83,8 +83,7 @@ class DeathspawnBehavior : NPCBehavior(NPCs.DEATH_SPAWN_1614) {
override fun onRemoval(self: NPC) { override fun onRemoval(self: NPC) {
val parent = getAttribute<NPC?>(self, "parent", null) ?: return val parent = getAttribute<NPC?>(self, "parent", null) ?: return
if (parent.behavior !is NechryaelBehavior) return (parent.behavior as? NechryaelBehavior)?.let { it.removeSpawn (parent, self) }
parent.behavior.removeSpawn(parent, self)
} }
override fun tick(self: NPC): Boolean { 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 { override fun canBeAttackedBy(self: NPC, attacker: Entity, style: CombatStyle, shouldSendMessage: Boolean): Boolean {
return attacker == getAttribute<Player?>(self, "target", null) return attacker == getAttribute<Player?>(self, "target", null)
} }
} }
@@ -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<Scenery>()
val surrounding = arrayOf (24984, 24985, 24986, 24987)
val center = 24991
val puroExit = 25014
val puroLocation = Location.create(2591, 4320, 0)
var nextCircle = 0
}
}
@@ -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
}
}
}
@@ -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<Player> 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 };
}
}
@@ -35,6 +35,10 @@ import core.plugin.Plugin;
import core.plugin.ClassScanner; import core.plugin.ClassScanner;
import core.tools.RandomFunction; 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. * Handles the puro puro activity.
* @author Vexia * @author Vexia
@@ -78,7 +82,6 @@ public final class PuroPuroPlugin extends MapZone implements Plugin<Object> {
ClassScanner.definePlugin(new WanderingImplingDialogue()); ClassScanner.definePlugin(new WanderingImplingDialogue());
ClassScanner.definePlugin(new ElnockInquisitorDialogue()); ClassScanner.definePlugin(new ElnockInquisitorDialogue());
ClassScanner.definePlugin(new PuroOptionHandler()); ClassScanner.definePlugin(new PuroOptionHandler());
ClassScanner.definePlugin(new ImpDefenderNPC());
return this; return this;
} }
@@ -128,10 +131,7 @@ public final class PuroPuroPlugin extends MapZone implements Plugin<Object> {
case 25021: case 25021:
pushThrough(p, (Scenery) target); pushThrough(p, (Scenery) target);
return true; return true;
case 25014: }
p.getTeleporter().send(Location.create(2427, 4446, 0), TeleportType.PURO_PURO);
return true;
}
} }
return super.interact(e, target, option); return super.interact(e, target, option);
} }
@@ -213,10 +213,6 @@ public final class PuroPuroPlugin extends MapZone implements Plugin<Object> {
WHEAT.add(new WheatSet(1, Location.create(2601, 4293, 0), Location.create(2600, 4293, 0))); 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(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(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(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(2615, 4347, 0), Location.create(2615, 4348, 0)));
WHEAT.add(new WheatSet(0, Location.create(2612, 4345, 0), Location.create(2612, 4344, 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<Object> {
@Override @Override
public Plugin<Object> newInstance(Object arg) throws Throwable { public Plugin<Object> newInstance(Object arg) throws Throwable {
NPCDefinition.forId(6070).getHandlers().put("option:trade", this); 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(11273).getHandlers().put("option:toggle-view", this);
ItemDefinition.forId(11258).getHandlers().put("option:butterfly-jar", this); ItemDefinition.forId(11258).getHandlers().put("option:butterfly-jar", this);
ItemDefinition.forId(11258).getHandlers().put("option:impling-jar", this); ItemDefinition.forId(11258).getHandlers().put("option:impling-jar", this);
@@ -258,14 +253,6 @@ public final class PuroPuroPlugin extends MapZone implements Plugin<Object> {
case 6070: case 6070:
ElnockInquisitorDialogue.openShop(player); ElnockInquisitorDialogue.openShop(player);
break; 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: case 11273:
if (!player.getZoneMonitor().isInZone("puro puro")) { if (!player.getZoneMonitor().isInZone("puro puro")) {
player.sendMessage("You can only use this in the Puro Puro Maze."); player.sendMessage("You can only use this in the Puro Puro Maze.");
@@ -367,7 +354,6 @@ public final class PuroPuroPlugin extends MapZone implements Plugin<Object> {
/** /**
* A wheat set. * A wheat set.
* @author Vexia
*/ */
public static class WheatSet { public static class WheatSet {
@@ -434,10 +420,36 @@ public final class PuroPuroPlugin extends MapZone implements Plugin<Object> {
continue; continue;
} }
if (removed) { 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); SceneryBuilder.add(object);
continue; 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; removed = !removed;
setNextWhilt(); setNextWhilt();
+31
View File
@@ -427,6 +427,28 @@ fun replaceScenery(toReplace: Scenery, with: Int, forTicks: Int, loc: Location?
toReplace.isActive = false 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 * Replace an object with the given revert timer with the given rotation
* @param toReplace the Scenery instance we are replacing * @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 * Opens the given interface for the given player
* @param player the player to open the interface for * @param player the player to open the interface for
+35
View File
@@ -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())
}
}
}
@@ -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<T> : ArrayList<Pair<T?, Double>>() {
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<T?, Double> {
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 <T> create (vararg elements: Pair<T?, Double>) : WeightedTable<T> {
var table = WeightedTable<T>()
for ((element, weight) in elements)
table.add (element, weight)
return table
}
}
}
@@ -32,6 +32,7 @@ import core.game.world.update.flag.npc.NPCForceChat;
import core.game.world.update.flag.npc.NPCSwitchId; import core.game.world.update.flag.npc.NPCSwitchId;
import core.tools.RandomFunction; import core.tools.RandomFunction;
import core.api.utils.GlobalKillCounter; import core.api.utils.GlobalKillCounter;
import core.api.utils.Vector;
import core.game.shops.Shops; import core.game.shops.Shops;
import core.game.node.entity.combat.CombatSwingHandler; import core.game.node.entity.combat.CombatSwingHandler;
import core.game.system.config.NPCConfigParser; import core.game.system.config.NPCConfigParser;
@@ -142,7 +143,7 @@ public class NPC extends Entity {
*/ */
private String forceTalk; private String forceTalk;
public final NPCBehavior behavior; public NPCBehavior behavior;
/** /**
* Constructs a new {@code NPC} {@code Object}. * Constructs a new {@code NPC} {@code Object}.
@@ -373,7 +374,7 @@ public class NPC extends Entity {
@Override @Override
public boolean isAttackable(Entity entity, CombatStyle style, boolean message) { public boolean isAttackable(Entity entity, CombatStyle style, boolean message) {
if ((entity instanceof Player && !definition.hasAction("attack")) || isInvisible()) { if (isInvisible()) {
return false; return false;
} }
if (task != null && entity instanceof Player && task.levelReq > entity.getSkills().getStaticLevel(Skills.SLAYER)) { 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); Location returnToSpawnLocation = getProperties().getSpawnLocation().transform(-5 + RandomFunction.random(getWalkRadius()), -5 + RandomFunction.random(getWalkRadius()), 0);
int dist = (int) Location.getDistance(location, returnToSpawnLocation); int dist = (int) Location.getDistance(location, returnToSpawnLocation);
int pathLimit = 15; int pathLimit = 15;
if(dist > pathLimit || dist < -pathLimit) if (dist > pathLimit) {
{ Vector normalizedDir = Vector.betweenLocs(this.location, returnToSpawnLocation).normalized();
int diffX = returnToSpawnLocation.getX() - location.getX(); returnToSpawnLocation = this.location.transform (normalizedDir.times(pathLimit));
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);
}
return returnToSpawnLocation; return returnToSpawnLocation;
} }
Location l = movementPath[movementIndex++]; Location l = movementPath[movementIndex++];
@@ -1,11 +1,14 @@
package core.game.node.entity.npc package core.game.node.entity.npc
import core.game.node.item.Item
import core.api.ContentInterface import core.api.ContentInterface
import core.game.node.entity.Entity 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.BattleState
import core.game.node.entity.combat.CombatStyle import core.game.node.entity.combat.CombatStyle
import core.game.node.entity.combat.CombatSwingHandler 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 { open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface {
companion object { companion object {
@@ -19,6 +22,12 @@ open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface {
ids.forEach { idMap[it] = behavior } 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. * 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" * @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. * @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. * 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 * @return the SwingHandler instance to be used for this cycle of combat
*/ */
open fun getSwingHandlerOverride(self: NPC, original: CombatSwingHandler) : CombatSwingHandler {return original} open fun getSwingHandlerOverride(self: NPC, original: CombatSwingHandler) : CombatSwingHandler {return original}
}
/**
* 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
}
}
@@ -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.SpellBookManager
import core.game.node.entity.player.link.emote.Emotes import core.game.node.entity.player.link.emote.Emotes
import core.game.node.entity.player.link.music.MusicEntry import core.game.node.entity.player.link.music.MusicEntry
import core.game.world.map.Location
import org.json.simple.JSONArray import org.json.simple.JSONArray
import org.json.simple.JSONObject import org.json.simple.JSONObject
import org.json.simple.parser.JSONParser import org.json.simple.parser.JSONParser
@@ -122,6 +123,7 @@ class PlayerSaveParser(val player: Player) {
"long" -> attr["value"].toString().toLong() "long" -> attr["value"].toString().toLong()
"bool" -> attr["value"] as Boolean "bool" -> attr["value"] as Boolean
"byte" -> Base64.getDecoder().decode(attr["value"].toString())[0] "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") } 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) {
} }
} }
@@ -9,6 +9,7 @@ import core.game.container.Container
import core.game.node.entity.player.Player import core.game.node.entity.player.Player
import core.game.node.entity.player.link.IronmanMode import core.game.node.entity.player.link.IronmanMode
import core.game.node.entity.skill.Skills import core.game.node.entity.skill.Skills
import core.game.world.map.Location
import core.tools.Log import core.tools.Log
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -116,6 +117,7 @@ class PlayerSaver (val player: Player){
is Short -> "short" is Short -> "short"
is String -> "str" is String -> "str"
is Byte -> "byte" 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") } else -> "null".also { log(this::class.java, Log.WARN, "Invalid attribute type for key: $key in PlayerSaver.kt Line 115") }
} }
attr.put("type",type) attr.put("type",type)
@@ -6,9 +6,11 @@ import core.game.world.map.path.Path;
import core.game.world.map.path.Pathfinder; import core.game.world.map.path.Pathfinder;
import core.tools.RandomFunction; import core.tools.RandomFunction;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import core.api.utils.Vector;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.lang.Math;
/** /**
* Represents a location on the world map. * Represents a location on the world map.
@@ -277,14 +279,14 @@ public final class Location extends Node {
public ArrayList<Location> getSurroundingTiles() { public ArrayList<Location> getSurroundingTiles() {
ArrayList<Location> locs = new ArrayList<>(); ArrayList<Location> 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,-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,1,0));//NW
locs.add(transform(-1,0,0));//W
return locs; return locs;
} }
@@ -554,4 +556,8 @@ public final class Location extends Node {
if (sb.length() == 0) return null; if (sb.length() == 0) return null;
return Direction.valueOf(sb.toString()); 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()));
}
} }
@@ -2,6 +2,7 @@ package core.game.world.map.path;
import core.game.node.Node; import core.game.node.Node;
import core.game.node.entity.Entity; import core.game.node.entity.Entity;
import core.game.node.entity.npc.NPC;
import core.game.node.item.GroundItem; import core.game.node.item.GroundItem;
import core.game.node.scenery.Scenery; import core.game.node.scenery.Scenery;
import core.game.world.map.Direction; import core.game.world.map.Direction;
@@ -124,7 +125,13 @@ public abstract class Pathfinder {
* @return The path. * @return The path.
*/ */
public static Path find(Entity mover, Node destination, boolean near, Pathfinder finder) { 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){ public static Path findWater(Entity mover, Node destination, boolean near, Pathfinder finder){