From 0f908a1544b2ef90ed5bc6323b2c09e618213ec9 Mon Sep 17 00:00:00 2001 From: Ceikry Date: Wed, 9 Aug 2023 04:02:18 +0000 Subject: [PATCH] Deep wilderness balancing Added a threat system to the deep wilderness that increases as a player kills NPCs to add balance to lucrative drop rates of PvP items in the deep wilderness Threat decreases with time Threat only increases in the deep wilderness As threat increases, the risk of encountering hostile wilderness events increases Hostile wilderness events may spawn anywhere in the wilderness Improved the safety checks on items dropped in PvP Untradeable items dropped in PvP will now yield an amount of coins proportional to the item's high alchemy value, and delete the item. This amount is halved if the killer doesn't have the level for high alchemy. An additional 250 gp is deducted (after halving, if applicable) from all items to account for the hypothetical cost of a nature rune Added admin command to view threat level ::dwthreat --- .../wilderness/handlers/DeepWildyThreat.kt | 90 +++++++++++++++++++ .../handlers/revenants/RevenantType.java | 10 +++ .../core/game/node/entity/player/Player.java | 23 ++++- .../world/map/zone/impl/WildernessZone.java | 11 ++- 4 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt diff --git a/Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt b/Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt new file mode 100644 index 000000000..9bc2f593c --- /dev/null +++ b/Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt @@ -0,0 +1,90 @@ +package content.region.wilderness.handlers + +import content.region.wilderness.handlers.revenants.RevenantType +import core.api.* +import core.game.node.entity.Entity +import core.game.node.entity.combat.DeathTask +import core.game.node.entity.npc.NPC +import core.game.node.entity.player.Player +import core.game.system.command.Privilege +import core.game.system.timer.PersistTimer +import core.game.world.update.flag.context.Graphics +import core.tools.RandomFunction +import core.tools.colorize +import org.json.simple.JSONObject + +object DeepWildyThreat { + @JvmStatic fun getThreat (player: Player) : Int { + return getOrStartTimer(player).ticksLeft + } + + @JvmStatic fun adjustThreat (player: Player, amount: Int) { + val timer = getOrStartTimer(player) + timer.ticksLeft += amount + if (timer.ticksLeft >= 2500 && timer.lastMessage < 2000) { + sendMessage(player, colorize("%RYou sense a great wrath upon you.")) + timer.lastMessage = 2000 + } else if (timer.ticksLeft >= 1000 && timer.lastMessage < 1000) { + sendMessage(player, colorize("%RYou sense watchful eyes upon you.")) + timer.lastMessage = 1000 + } else if (timer.ticksLeft >= 500 && timer.lastMessage < 500) { + sendMessage(player, colorize("%RYou sense a dark presence.")) + timer.lastMessage = 500 + } + } +} + +class DWThreatTimer : PersistTimer(1, "dw-threat"), Commands { + var ticksLeft = 0 + var lastMessage = 0 + var currentRev: NPC? = null + var chats = arrayOf("Leave this place!", "Suffer!", "Death to you!", "Flee, coward!", "Leave my resting place!", "Let me rest in peace!", "You belong to me!") + + override fun run(entity: Entity): Boolean { + if (ticksLeft-- <= 0) return false + if (ticksLeft > 3000) ticksLeft = 3000 + if (ticksLeft % 5 != 0) return true + if (entity !is Player) return false + if (!entity.skullManager.isWilderness) return true + + val rollchance = + if (ticksLeft >= 2500) 10 + else if (ticksLeft >= 2000) 200 + else if (ticksLeft >= 1500) 400 + else if (ticksLeft >= 1000) 800 + else if (ticksLeft >= 500) 1500 + else 2_000_000 + + if ((currentRev == null || DeathTask.isDead(currentRev) || !currentRev!!.isActive) && RandomFunction.roll(rollchance)) { + val type = RevenantType.getClosestHigherOrEqual(entity.properties.currentCombatLevel) + val npc = NPC.create(type.ids.random(), entity.location) + npc.isRespawn = false + npc.init() + npc.attack(entity) + Graphics.send(Graphics(86), npc.location) + ticksLeft -= 500 + sendChat(npc, chats.random()) + currentRev = npc + } else if (currentRev != null && !currentRev!!.location.withinDistance(entity.location, 25)) { + poofClear(currentRev!!) + currentRev = null + } + + return true + } + + override fun save(root: JSONObject, entity: Entity) { + root["threat-time-remaining"] = ticksLeft + } + + override fun parse(root: JSONObject, entity: Entity) { + ticksLeft = root.getOrDefault("threat-time-remaining", 3000) as? Int ?: 3000 + } + + override fun defineCommands() { + define("dwthreat", Privilege.ADMIN, "", "") {player, _ -> + val timer = getOrStartTimer(player) + notify(player, "Current Threat: ${timer.ticksLeft}") + } + } +} \ No newline at end of file diff --git a/Server/src/main/content/region/wilderness/handlers/revenants/RevenantType.java b/Server/src/main/content/region/wilderness/handlers/revenants/RevenantType.java index fe582d1cf..b2f06aba3 100644 --- a/Server/src/main/content/region/wilderness/handlers/revenants/RevenantType.java +++ b/Server/src/main/content/region/wilderness/handlers/revenants/RevenantType.java @@ -1,5 +1,7 @@ package content.region.wilderness.handlers.revenants; +import core.cache.def.impl.NPCDefinition; + /** * A revenant type. * @author Vexia @@ -58,4 +60,12 @@ public enum RevenantType { public int getMaxHit() { return maxHit; } + + public static RevenantType getClosestHigherOrEqual (int combatLevel) { + for (RevenantType t : values()) { + NPCDefinition def = NPCDefinition.forId(t.ids[0]); + if (def.getCombatLevel() >= combatLevel) return t; + } + return null; + } } diff --git a/Server/src/main/core/game/node/entity/player/Player.java b/Server/src/main/core/game/node/entity/player/Player.java index 0904ec16d..909123d6d 100644 --- a/Server/src/main/core/game/node/entity/player/Player.java +++ b/Server/src/main/core/game/node/entity/player/Player.java @@ -2,6 +2,7 @@ package core.game.node.entity.player; import content.global.handlers.item.equipment.special.SalamanderSwingHandler; import content.global.skill.runecrafting.PouchManager; +import core.api.EquipmentSlot; import core.game.component.Component; import core.game.container.Container; import core.game.container.ContainerType; @@ -34,6 +35,7 @@ import core.game.node.entity.player.link.skillertasks.SkillerTasks; import core.game.node.entity.skill.Skills; import content.global.skill.construction.HouseManager; import content.global.skill.summoning.familiar.FamiliarManager; +import core.game.node.item.GroundItem; import core.game.node.item.GroundItemManager; import core.game.node.item.Item; import core.game.system.communication.CommunicationInfo; @@ -613,6 +615,11 @@ public class Player extends Entity { if (this.isArtificial() && killer instanceof NPC) { return; } + if (killer instanceof Player && getWorldTicks() - killer.getAttribute("/save:last-murder-news", 0) >= 500) { + Item wep = getItemFromEquipment((Player) killer, EquipmentSlot.WEAPON); + sendNews(killer.getName() + " has murdered " + getName() + " with " + (wep == null ? "their fists." : (StringUtils.isPlusN(wep.getName()) ? "an " : "a ") + wep.getName())); + killer.setAttribute("/save:last-murder-news", getWorldTicks()); + } getPacketDispatch().sendMessage("Oh dear, you are dead!"); incrementAttribute("/save:"+STATS_BASE+":"+STATS_DEATHS); @@ -653,7 +660,9 @@ public class Player extends Entity { g.initialize(this, location, Arrays.stream(c[1].toArray()).filter(Objects::nonNull).toArray(Item[]::new)); //note: the amount of code required to filter nulls from an array in Java is atrocious. } else { StringBuilder itemsLost = new StringBuilder(); + int coins = 0; for (Item item : c[1].toArray()) { + boolean stayPrivate = false; if (item == null) continue; if (killer instanceof Player) itemsLost.append(getItemName(item.getId())).append("(").append(item.getAmount()).append("), "); @@ -661,8 +670,20 @@ public class Player extends Entity { continue; if (GraveController.shouldRelease(item.getId())) continue; + if (!item.getDefinition().isTradeable()) { + if (killer instanceof Player) { + int value = item.getDefinition().getAlchemyValue(true); + if (getStatLevel(killer, Skills.MAGIC) < 55) value /= 2; + coins += Math.max(0, value - 250); + continue; + } else stayPrivate = true; + } item = GraveController.checkTransform(item); - GroundItemManager.create(item, location, killer instanceof Player ? (Player) killer : this); + GroundItem gi = GroundItemManager.create(item, location, killer instanceof Player ? (Player) killer : this); + gi.setRemainPrivate(stayPrivate); + } + if (coins > 0) { + GroundItemManager.create(new Item(Items.COINS_995, coins), location, (Player) killer); } if (killer instanceof Player) PlayerMonitor.log((Player) killer, LogType.PK, "Killed " + name + ", who dropped: " + itemsLost); 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 6a0c77a05..fd7211e08 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,5 +1,6 @@ package core.game.world.map.zone.impl; +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; @@ -91,10 +92,14 @@ public final class WildernessZone extends MapZone { private void rollWildernessExclusiveLoot(Entity e, Entity killer) { 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); - + + if (isDeepWildy) { + DeepWildyThreat.adjustThreat((Player) killer, 250); + } + if (!isValidTarget) return; int cEleGloveRate = isDeepWildy ? 50 : 150; @@ -109,6 +114,7 @@ public final class WildernessZone extends MapZone { Item reward = new Item(BrawlingGloves.forIndicator(glove).getId()); GroundItemManager.create(reward, e.asNpc().getDropLocation(), killer.asPlayer()); Repository.sendNews(killer.getUsername() + " has received " + reward.getName().toLowerCase() + " from a " + e.asNpc().getName() + "!"); + DeepWildyThreat.adjustThreat((Player) killer, 200); } for (int j : PVP_GEAR) { @@ -122,6 +128,7 @@ public final class WildernessZone extends MapZone { } Repository.sendNews(killer.asPlayer().getUsername() + " has received a " + reward.getName() + " from a " + e.asNpc().getName() + "!"); GroundItemManager.create(reward, ((NPC) e).getDropLocation(), killer.asPlayer()); + DeepWildyThreat.adjustThreat((Player) killer, 1000); } } }