diff --git a/Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt b/Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt index 579bb9d3b..929375a65 100644 --- a/Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt +++ b/Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt @@ -14,6 +14,7 @@ import core.game.node.item.Item import core.game.system.command.Privilege import core.game.system.timer.PersistTimer import core.game.system.timer.impl.Disease +import core.game.world.map.path.Pathfinder import core.game.world.map.zone.impl.WildernessZone import core.game.world.update.flag.context.Graphics import core.tools.RandomFunction @@ -45,6 +46,7 @@ class DWThreatTimer : PersistTimer(1, "dw-threat"), Commands { var ticksLeft = 0 var lastMessage = 0 var currentRev: NPC? = null + var forceSpawn = false override fun run(entity: Entity): Boolean { if (ticksLeft-- <= 0) return false @@ -61,7 +63,8 @@ class DWThreatTimer : PersistTimer(1, "dw-threat"), Commands { else if (ticksLeft >= 500) 1500 else 2_000_000 - if ((currentRev == null || DeathTask.isDead(currentRev) || !currentRev!!.isActive) && RandomFunction.roll(rollchance)) { + if ((currentRev == null || DeathTask.isDead(currentRev) || !currentRev!!.isActive) && (forceSpawn || RandomFunction.roll(rollchance))) { + forceSpawn = false val type = RevenantType.getClosestHigherOrEqual(entity.properties.currentCombatLevel) val npc = NPC.create(type.ids.random(), entity.location) npc.isRespawn = false @@ -80,10 +83,12 @@ class DWThreatTimer : PersistTimer(1, "dw-threat"), Commands { override fun save(root: JSONObject, entity: Entity) { root["threat-time-remaining"] = ticksLeft.toString() + root["threat-forceSpawn"] = (currentRev != null).toString() } override fun parse(root: JSONObject, entity: Entity) { ticksLeft = root["threat-time-remaining"]?.toString()?.toIntOrNull() ?: 0 + forceSpawn = root["threat-forceSpawn"]?.toString()?.toBoolean() ?: false } override fun defineCommands() { @@ -140,4 +145,8 @@ class RevGuardianBehavior : NPCBehavior() { val timer = getOrStartTimer(target) timer.ticksLeft = 0 } -} \ No newline at end of file + + override fun getPathfinderOverride(self: NPC): Pathfinder? { + return Pathfinder.SMART + } +} diff --git a/Server/src/main/core/game/interaction/MovementPulse.java b/Server/src/main/core/game/interaction/MovementPulse.java index 3339645da..4d4e5f459 100644 --- a/Server/src/main/core/game/interaction/MovementPulse.java +++ b/Server/src/main/core/game/interaction/MovementPulse.java @@ -4,6 +4,7 @@ import core.game.node.Node; import core.game.node.entity.Entity; import core.game.node.entity.impl.WalkingQueue; import core.game.node.entity.npc.NPC; +import core.game.node.entity.npc.NPCBehavior; import core.game.node.entity.player.Player; import core.game.system.task.Pulse; import core.game.world.map.Direction; @@ -175,6 +176,11 @@ public abstract class MovementPulse extends Pulse { if (pathfinder == null) { if (mover instanceof Player) { this.pathfinder = Pathfinder.SMART; + } else if (mover instanceof NPC) { + NPC npc = (NPC)mover; + NPCBehavior behavior = npc.behavior; + Pathfinder pf = behavior != null ? behavior.getPathfinderOverride(npc) : null; + this.pathfinder = pf != null ? pf : Pathfinder.DUMB; } else { this.pathfinder = Pathfinder.DUMB; } diff --git a/Server/src/main/core/game/node/entity/impl/Properties.java b/Server/src/main/core/game/node/entity/impl/Properties.java index 9c002f1ac..78e4f8807 100644 --- a/Server/src/main/core/game/node/entity/impl/Properties.java +++ b/Server/src/main/core/game/node/entity/impl/Properties.java @@ -33,7 +33,7 @@ public final class Properties { /** * The entity's combat pulse. */ - private final CombatPulse combatPulse; + private CombatPulse combatPulse; /** * If the entity is retaliating. @@ -332,6 +332,14 @@ public final class Properties { return combatPulse; } + /** + * Sets the combatPulse. + * @return The void. + */ + public void setCombatPulse(CombatPulse combatPulse) { + this.combatPulse = (CombatPulse)combatPulse; + } + /** * Gets the retaliating. * @return The retaliating. @@ -571,4 +579,4 @@ public final class Properties { this.protectStyle = protectStyle; } -} \ No newline at end of file +} 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 ba1fdf3e3..f3cafc5b6 100644 --- a/Server/src/main/core/game/node/entity/npc/NPC.java +++ b/Server/src/main/core/game/node/entity/npc/NPC.java @@ -8,6 +8,7 @@ import core.game.interaction.MovementPulse; import core.game.node.entity.Entity; import core.game.node.entity.combat.BattleState; import core.game.node.entity.combat.spell.CombatSpell; +import core.game.node.entity.combat.CombatPulse; import core.game.node.entity.combat.CombatStyle; import core.game.node.entity.combat.spell.DefaultCombatSpell; import core.game.node.entity.combat.equipment.WeaponInterface; @@ -187,6 +188,7 @@ public class NPC extends Entity { */ public static NPC create(int id, Location location, Direction direction, Object... objects) { NPC n = AbstractNPC.forId(id); + if (n != null) { n = ((AbstractNPC) n).construct(id, location, objects); } @@ -229,6 +231,8 @@ public class NPC extends Entity { } } behavior.onCreation(this); + // FIXME: hack around MovementPulse's constructor getting run while behavior is null when behavior is set between NPC constructor and init. + getProperties().setCombatPulse(new CombatPulse(this)); } @Override 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 6dbc967d6..55f9b99e6 100644 --- a/Server/src/main/core/game/node/entity/npc/NPCBehavior.kt +++ b/Server/src/main/core/game/node/entity/npc/NPCBehavior.kt @@ -9,6 +9,7 @@ import core.game.node.entity.combat.BattleState import core.game.node.entity.combat.CombatStyle import core.game.node.entity.combat.CombatSwingHandler import core.game.world.map.path.ClipMaskSupplier +import core.game.world.map.path.Pathfinder open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface { companion object { @@ -134,6 +135,13 @@ open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface { */ open fun getSwingHandlerOverride(self: NPC, original: CombatSwingHandler) : CombatSwingHandler {return original} + /** + * Called by MovementPulse to determine if a non-default Pathfinder should be used (e.g. whether this npc should intelligently walk around obstacles) + */ + open fun getPathfinderOverride(self: NPC): Pathfinder? { + return null + } + /** * Called by pathfinding code to determine the clipping mask supplier this NPC should use. You can use this to ignore water, etc. */ diff --git a/Server/src/main/core/game/node/entity/player/link/SkullManager.java b/Server/src/main/core/game/node/entity/player/link/SkullManager.java index b2ca53276..fd50bdeca 100644 --- a/Server/src/main/core/game/node/entity/player/link/SkullManager.java +++ b/Server/src/main/core/game/node/entity/player/link/SkullManager.java @@ -1,7 +1,11 @@ package core.game.node.entity.player.link; +import core.game.container.Container; +import core.game.container.ContainerEvent; +import core.game.container.ContainerListener; import core.game.node.entity.Entity; import core.game.node.entity.player.Player; +import core.game.node.item.Item; import core.ServerConstants; import static core.api.ContentAPIKt.*; @@ -17,6 +21,35 @@ import static core.game.world.map.zone.impl.WildernessZone.WILDERNESS_PROT_ATTR; * @author Emperor */ public final class SkullManager { + public enum SkullIcon { + NONE(-1), + WHITE(0), + RED(1), + BH_RED5(2), + BH_BLUE4(3), + BH_GREEN3(4), + BH_GREY2(5), + BH_BROWN1(6), + SCREAM(7); + public final int id; + SkullIcon(int id) { + this.id = id; + } + public static SkullIcon forId(int id) { + switch(id) { + case 0: return SkullIcon.WHITE; + case 1: return SkullIcon.RED; + case 2: return SkullIcon.BH_RED5; + case 3: return SkullIcon.BH_BLUE4; + case 4: return SkullIcon.BH_GREEN3; + case 5: return SkullIcon.BH_GREY2; + case 6: return SkullIcon.BH_BROWN1; + case 7: return SkullIcon.SCREAM; + default: return SkullIcon.NONE; + } + } + } + /** * The player instance. @@ -198,15 +231,74 @@ public final class SkullManager { return skulled || deepWilderness; } - public boolean isDeepWilderness() { - return deepWilderness; - } + public boolean isDeepWilderness() { + return deepWilderness; + } - public void setDeepWilderness (boolean deepWildy) { - setSkullIcon(deepWildy ? 1 : skulled ? 0 : -1); - setSkullCheckDisabled(deepWildy); - deepWilderness = deepWildy; + public void setDeepWilderness (boolean deepWildy) { + if(deepWildy) { + updateDWSkullIcon(); + } else { + removeDWSkullIcon(); + } + setSkullCheckDisabled(deepWildy); + deepWilderness = deepWildy; + } + + public static final long DEEP_WILD_DROP_RISK_THRESHOLD = 100000; + public void updateDWSkullIcon() { + if (player.getAttribute("deepwild-value-listener") == null) { + ContainerListener listener = new ContainerListener() { + @Override + public void update(Container c, ContainerEvent event) { + refresh(c); + } + + @Override + public void refresh(Container c) { + updateDWSkullIcon(); + } + }; + player.setAttribute("deepwild-value-listener", listener); + player.getInventory().getListeners().add(listener); + player.getEquipment().getListeners().add(listener); } + long value = 0; + long maxValue = 0; + for (Item item : player.getInventory().toArray()) { + if (item != null) { + long alchValue = item.getAlchemyValue(); + value += alchValue; + maxValue = Math.max(maxValue, alchValue); + } + } + for (Item item : player.getEquipment().toArray()) { + if (item != null) { + long alchValue = item.getAlchemyValue(); + value += alchValue; + maxValue = Math.max(maxValue, alchValue); + } + } + // Act as if protect item is always active when calculating risk + value -= maxValue; + player.setAttribute("deepwild-value-risk", value); + SkullIcon skull = SkullIcon.BH_BROWN1; + if (value >= DEEP_WILD_DROP_RISK_THRESHOLD) { + skull = SkullIcon.RED; + } + setSkullIcon(skull.id); + } + + public void removeDWSkullIcon() { + setSkullIcon(skulled ? 0 : -1); + ContainerListener listener = player.getAttribute("deepwild-value-listener"); + if (listener != null) { + player.getInventory().getListeners().remove(listener); + player.getEquipment().getListeners().remove(listener); + } + player.removeAttribute("deepwild-value-listener"); + player.removeAttribute("deepwild-value-risk"); + } /** * Sets the skulled. diff --git a/Server/src/main/core/game/node/item/Item.java b/Server/src/main/core/game/node/item/Item.java index 23e45d818..f84c7d648 100644 --- a/Server/src/main/core/game/node/item/Item.java +++ b/Server/src/main/core/game/node/item/Item.java @@ -106,6 +106,14 @@ public class Item extends Node{ return value * getAmount(); } + public long getAlchemyValue() { + long value = 1; + if (definition.getAlchemyValue(true) > value) { + value = definition.getAlchemyValue(true); + } + return value * getAmount(); + } + /** * Gets a copy of the item. * @return The item copy. @@ -245,4 +253,4 @@ public class Item extends Node{ return "Item id=" + getId() + ", name=" + getName() + ", amount=" + amount; } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/world/map/zone/impl/WildernessZone.java b/Server/src/main/core/game/world/map/zone/impl/WildernessZone.java index fd67844ea..5277f4409 100644 --- a/Server/src/main/core/game/world/map/zone/impl/WildernessZone.java +++ b/Server/src/main/core/game/world/map/zone/impl/WildernessZone.java @@ -1,9 +1,10 @@ package core.game.world.map.zone.impl; +import content.global.handlers.item.equipment.brawling_gloves.BrawlingGloves; +import content.global.skill.summoning.familiar.Familiar; import content.region.wilderness.handlers.DeepWildyThreat; import core.game.component.Component; import core.game.interaction.Option; -import content.global.handlers.item.equipment.brawling_gloves.BrawlingGloves; import core.game.node.Node; import core.game.node.entity.Entity; import core.game.node.entity.combat.CombatStyle; @@ -12,17 +13,17 @@ import core.game.node.entity.npc.agg.AggressiveBehavior; import core.game.node.entity.npc.agg.AggressiveHandler; import core.game.node.entity.player.Player; import core.game.node.entity.player.info.Rights; -import content.global.skill.summoning.familiar.Familiar; +import core.game.node.entity.player.link.SkullManager; import core.game.node.item.GroundItemManager; import core.game.node.item.Item; +import core.game.world.GameWorld; import core.game.world.map.Location; import core.game.world.map.zone.MapZone; import core.game.world.map.zone.ZoneBorders; import core.game.world.map.zone.ZoneRestriction; +import core.game.world.repository.Repository; import core.tools.RandomFunction; import org.rs09.consts.NPCs; -import core.game.world.GameWorld; -import core.game.world.repository.Repository; /** @@ -94,7 +95,9 @@ public final class WildernessZone extends MapZone { if (!(killer instanceof Player)) return; boolean isDeepWildy = ((Player) killer).getSkullManager().isDeepWilderness(); - boolean isValidTarget = e instanceof NPC && (isDeepWildy || e.asNpc().getName().contains("Revenant") || e.getId() == NPCs.CHAOS_ELEMENTAL_3200); + boolean isRevOrCele = e.asNpc().getName().contains("Revenant") || e.getId() == NPCs.CHAOS_ELEMENTAL_3200; + boolean isSufficientRisk = ((Player) killer).getAttribute("deepwild-value-risk", 0L) > SkullManager.DEEP_WILD_DROP_RISK_THRESHOLD; + boolean isValidTarget = e instanceof NPC && ((isDeepWildy && isSufficientRisk) || isRevOrCele); if (isDeepWildy) { DeepWildyThreat.adjustThreat((Player) killer, 50); @@ -102,13 +105,13 @@ public final class WildernessZone extends MapZone { if (!isValidTarget) return; - int cEleGloveRate = isDeepWildy ? 50 : 150; - int normalGloveRate = isDeepWildy ? 100 : 150; - int pvpGearRate = getNewDropRate(e.asNpc().getDefinition().getCombatLevel()); - if (isDeepWildy) + if (isDeepWildy && isRevOrCele) pvpGearRate /= 2; + int cEleGloveRate = isDeepWildy ? 50 : 150; + int normalGloveRate = isDeepWildy && isRevOrCele ? 100 : (int)((1.0/(1.0-Math.pow(1.0 - (1.0/(double)pvpGearRate), 16.0))) * 5.0 / 6.0); + if (RandomFunction.roll(e.getId() == NPCs.CHAOS_ELEMENTAL_3200 ? cEleGloveRate : normalGloveRate)) { byte glove = (byte) RandomFunction.random(1, 13); Item reward = new Item(BrawlingGloves.forIndicator(glove).getId());