From a0435fb890c60f44530c18288af378999f8ac40f Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Tue, 29 Aug 2023 07:41:59 +0000 Subject: [PATCH] Deep wilderness threats now continue at next login if present when logging out Decreased brawler gloves and PvP gear drop rate from ordinary deep wilderness NPCs (does not apply to revenants or chaos elemental) Brawler gloves and PvP gear drops from ordinary deep wilderness NPCs now require at least 100k high alchemy risk (does not apply to revenants or chaos elemental) --- .../wilderness/handlers/DeepWildyThreat.kt | 13 ++- .../core/game/interaction/MovementPulse.java | 6 + .../game/node/entity/impl/Properties.java | 12 +- .../main/core/game/node/entity/npc/NPC.java | 4 + .../core/game/node/entity/npc/NPCBehavior.kt | 8 ++ .../node/entity/player/link/SkullManager.java | 106 ++++++++++++++++-- Server/src/main/core/game/node/item/Item.java | 10 +- .../world/map/zone/impl/WildernessZone.java | 21 ++-- 8 files changed, 159 insertions(+), 21 deletions(-) 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());