Combo runes are now consumed before standard elemental runes if both are present in inventory

1 combo rune can count for 2 elemental runes if the spell requires both
This commit is contained in:
Sam Marder
2026-05-30 14:13:28 +00:00
committed by Ryan
parent 626a3d0f80
commit dd1cfbba51
4 changed files with 63 additions and 120 deletions
@@ -47,11 +47,10 @@ abstract class SpellListener(val bookName: String) : Listener {
player.sendMessage("You need a magic level of $magicLevel to cast this spell.")
throw IllegalStateException()
}
for(rune in runes){
if(!SpellUtils.hasRune(player,rune)){
player.sendMessage("You don't have enough ${rune.definition.name.lowercase()}s to cast this spell.")
throw IllegalStateException()
}
val missing = SpellUtils.hasRunes(player, runes)
if (missing != null) {
player.sendMessage("You don't have enough ${missing.definition.name.lowercase()}s to cast this spell.")
throw IllegalStateException()
}
for(item in specialEquipment){
if(!player.equipment.contains(item,1)){
@@ -3,9 +3,9 @@ package content.global.skill.magic
import core.game.node.entity.combat.spell.CombinationRune
import core.game.node.entity.combat.spell.MagicStaff
import core.game.node.entity.combat.spell.Runes
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.node.item.Item
import kotlin.math.min
object SpellUtils {
/**
@@ -32,70 +32,57 @@ object SpellUtils {
return false
}
fun hasRune(p:Player,rune:Item):Boolean{
val removeItems = p.getAttribute("spell:runes",ArrayList<Item>())
if(usingStaff(p,rune.id)) return true
if(p.inventory.containsItem(rune)){
removeItems.add(rune)
p.setAttribute("spell:runes",removeItems)
/**
* Validates if the player has the necessary runes to cast a spell.
*
* If the player is able to cast the spell, the "spell:runes" attribute will be set to the list of items that should
* be removed from the player's inventory after successfully casting the spell. This accounts for staves and
* combination runes.
*
* @param p The player casting the spell
* @param runes The runes and other items required to cast the spell
* @return null if the player can cast the spell or an Item representing at least one of the runes the player is missing
*/
@JvmStatic
fun hasRunes(p: Player, runes: Array<Item>): Item? {
val cost = HashMap<Int, Int>()
// `runes` are mostly actual runes but occasionally other items like staves or unpowered orbs
for (rune in runes) {
if (usingStaff(p, rune.id)) continue
cost[rune.id] = cost.getOrDefault(rune.id, 0) + rune.amount
}
val baseAmt = p.inventory.getAmount(rune.id)
var amtRemaining = rune.amount - baseAmt
val possibleComboRunes = CombinationRune.eligibleFor(Runes.forId(rune.id))
for (r in possibleComboRunes) {
if (p.inventory.containsItem(Item(r.id)) && amtRemaining > 0) {
val amt = p.inventory.getAmount(r.id)
if (amtRemaining <= amt) {
removeItems.add(Item(r.id,amtRemaining))
amtRemaining = 0
break
}
removeItems.add(Item(r.id,p.inventory.getAmount(r.id)))
amtRemaining -= p.inventory.getAmount(r.id)
val toRemove = ArrayList<Item>()
// Combination runes are used before elemental runes.
// https://runescape.wiki/w/Runecrafting?oldid=2618332#Function_and_Usage_of_Combination_Runes:
for (combo in CombinationRune.values()) {
val available = p.inventory.getAmount(combo.id)
val maxUsage = combo.types.mapNotNull { cost[it.id] }.maxOrNull() ?: 0
if (maxUsage > 0 && available >= 0) {
val usage = min(maxUsage, available)
toRemove.add(Item(combo.id, usage))
// Even if a spell uses both parts of a combo rune, it should only consume a single rune. For example,
// a spell that requires an air rune and an earth rune should only consume a single dust rune.
// https://youtu.be/9gAiqEmF-Hc?t=67
combo.types.forEach { cost[it.id] = cost.getOrDefault(it.id, 0) - usage }
}
}
p.setAttribute("spell:runes",removeItems)
return amtRemaining <= 0
}
fun hasRune(p: Player, item: Item, toRemove: MutableList<Item?>, message: Boolean): Boolean {
if (!usingStaff(p, item.id)) {
val hasBaseRune = p.inventory.contains(item.id, item.amount)
if (!hasBaseRune) {
val baseAmt = p.inventory.getAmount(item.id)
if (baseAmt > 0) {
toRemove.add(Item(item.id, p.inventory.getAmount(item.id)))
}
var amtRemaining = item.amount - baseAmt
val possibleComboRunes = CombinationRune.eligibleFor(Runes.forId(item.id))
for (r in possibleComboRunes) {
if (p.inventory.containsItem(Item(r.id)) && amtRemaining > 0) {
val amt = p.inventory.getAmount(r.id)
if (amtRemaining < amt) {
toRemove.add(Item(r.id, amtRemaining))
amtRemaining = 0
continue
}
amtRemaining -= p.inventory.getAmount(r.id)
toRemove.add(Item(r.id, p.inventory.getAmount(r.id)))
}
}
return if (amtRemaining <= 0) {
true
} else {
p.packetDispatch.sendMessage("You don't have enough " + item.name + "s to cast this spell.")
false
}
for ((runeId, amount) in cost) {
if (amount <= 0) continue
val available = p.inventory.getAmount(runeId)
if (available < amount) {
return Item(runeId, amount)
}
toRemove.add(item)
return true
}
return true
}
fun attackableNPC(npc: NPC): Boolean{
return npc.definition.hasAction("attack")
toRemove.add(Item(runeId, amount))
}
p.setAttribute("spell:runes", toRemove)
return null
}
@JvmStatic
@@ -2,7 +2,7 @@ package content.global.skill.magic.modern
import content.data.Quests
import content.global.skill.magic.SpellListener
import content.global.skill.magic.SpellUtils.hasRune
import content.global.skill.magic.SpellUtils.hasRunes
import content.global.skill.magic.TeleportMethod
import content.global.skill.magic.homeTeleport
import content.global.skill.magic.spellconsts.Modern
@@ -323,11 +323,10 @@ class ModernListeners : SpellListener("modern"){
sendMessage(player, "You need a magic level of ${spell.level} to cast this spell.")
return@queueScript stopExecuting(player)
}
for (rune in spell.requiredRunes) {
if(!hasRune(player,rune)){
sendMessage(player, "You don't have enough ${rune.name.lowercase()}s to cast this spell.")
return@queueScript stopExecuting(player)
}
val missing = hasRunes(player, spell.requiredRunes)
if (missing != null) {
sendMessage(player, "You don't have enough ${missing.name.lowercase()}s to cast this spell.")
return@queueScript stopExecuting(player)
}
visualizeSpell(player, CHARGE_ORB_ANIM, spell.graphics, spell.sound)
removeRunes(player)
@@ -1,5 +1,6 @@
package core.game.node.entity.combat.spell;
import content.global.skill.magic.SpellUtils;
import core.game.event.SpellCastEvent;
import core.game.node.Node;
import core.game.node.entity.Entity;
@@ -17,7 +18,6 @@ import core.plugin.Plugin;
import core.tools.RandomFunction;
import java.util.ArrayList;
import java.util.List;
import static core.api.ContentAPIKt.playGlobalAudio;
@@ -223,16 +223,17 @@ public abstract class MagicSpell implements Plugin<SpellType> {
if (runes == null) {
return true;
}
List<Item> toRemove = new ArrayList<>(20);
for (Item item : runes) {
if (!hasRune(p, item, toRemove, message)) {
return false;
Item missing = SpellUtils.hasRunes(p, runes);
if (missing != null) {
if (message) {
p.getPacketDispatch().sendMessage("You don't have enough " + missing.getName() + "s to cast this spell.");
}
return false;
}
if (remove) {
toRemove.forEach(i -> {
p.getInventory().remove(i);
});
ArrayList<Item> toRemove = p.getAttribute("spell:runes", new ArrayList<>());
toRemove.forEach(i -> p.getInventory().remove(i));
p.removeAttribute("spell:runes");
}
return true;
}
@@ -255,49 +256,6 @@ public abstract class MagicSpell implements Plugin<SpellType> {
return true;
}
/**
* Checks if the player has a rune to remove.
* @param p the player.
* @param item the item.
* @param toRemove the list of items to remove.
* @param message the message.
* @return {@code True} if so.
*/
public boolean hasRune(Player p, Item item, List<Item> toRemove, boolean message) {
if (!usingStaff(p, item.getId())) {
boolean hasBaseRune = p.getInventory().contains(item.getId(),item.getAmount());
if(!hasBaseRune){
int baseAmt = p.getInventory().getAmount(item.getId());
if(baseAmt > 0){
toRemove.add(new Item(item.getId(),p.getInventory().getAmount(item.getId())));
}
int amtRemaining = item.getAmount() - baseAmt;
List<CombinationRune> possibleComboRunes = CombinationRune.eligibleFor(Runes.forId(item.getId()));
for(CombinationRune r : possibleComboRunes){
if(p.getInventory().containsItem(new Item(r.id)) && amtRemaining > 0){
int amt = p.getInventory().getAmount(r.id);
if(amtRemaining < amt){
toRemove.add(new Item(r.id,amtRemaining));
amtRemaining = 0;
continue;
}
amtRemaining -= p.getInventory().getAmount(r.id);
toRemove.add(new Item(r.id,p.getInventory().getAmount(r.id)));
}
}
if(amtRemaining <= 0){
return true;
} else {
p.getPacketDispatch().sendMessage("You don't have enough " + item.getName() + "s to cast this spell.");
return false;
}
}
toRemove.add(item);
return true;
}
return true;
}
/**
* Adds the experience for casting this spell.
* @param entity The entity to reward with experience.