From 87ce52530e96a785e8edc898e372ae85434fe38e Mon Sep 17 00:00:00 2001 From: Tyluur Date: Mon, 20 Apr 2026 10:38:44 -0400 Subject: [PATCH 1/5] fix(combat): clear stale spell state and abort swing when cast requirements fail Stop combat swing execution when Magic.castSpell fails due to missing requirements. Also clear transient spell flags ("spell", "one_time", and "autocast") in failure paths to prevent follow-up clicks from inheriting stale cast state. --- game/src/main/kotlin/content/entity/combat/Combat.kt | 7 +++++-- game/src/main/kotlin/content/skill/magic/Magic.kt | 2 ++ .../src/main/kotlin/content/skill/magic/book/SpellRunes.kt | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/game/src/main/kotlin/content/entity/combat/Combat.kt b/game/src/main/kotlin/content/entity/combat/Combat.kt index 92b9b25c44..660fc2338a 100644 --- a/game/src/main/kotlin/content/entity/combat/Combat.kt +++ b/game/src/main/kotlin/content/entity/combat/Combat.kt @@ -204,9 +204,12 @@ class Combat(val combatDefinitions: CombatDefinitions) : } else if (character is Player) { val style = character.fightStyle if (style == "magic" || style == "blaze") { - if (Magic.castSpell(character, target)) { - CombatApi.swing(character, target, character.weapon.id, style) + if (!Magic.castSpell(character, target)) { + character.mode = EmptyMode + character.target = null + return } + CombatApi.swing(character, target, character.weapon.id, style) } else { CombatApi.swing(character, target, character.weapon.id, style) } diff --git a/game/src/main/kotlin/content/skill/magic/Magic.kt b/game/src/main/kotlin/content/skill/magic/Magic.kt index afbca073be..8b16d59d92 100644 --- a/game/src/main/kotlin/content/skill/magic/Magic.kt +++ b/game/src/main/kotlin/content/skill/magic/Magic.kt @@ -20,6 +20,8 @@ object Magic { fun castSpell(source: Character, target: Character): Boolean { if (source.spell.isNotBlank() && source is Player && !source.removeSpellItems(source.spell)) { source.clear("autocast") + source.clear("spell") + source.clear("one_time") return false } val spell = source.spell diff --git a/game/src/main/kotlin/content/skill/magic/book/SpellRunes.kt b/game/src/main/kotlin/content/skill/magic/book/SpellRunes.kt index ec4c37f765..bd9f56df46 100644 --- a/game/src/main/kotlin/content/skill/magic/book/SpellRunes.kt +++ b/game/src/main/kotlin/content/skill/magic/book/SpellRunes.kt @@ -10,6 +10,8 @@ class SpellRunes : Script { combatPrepare(style = "magic") { _ -> if (spell.isNotBlank() && !hasSpellItems(spell)) { clear("autocast") + clear("spell") + clear("one_time") false } else { true From 81657336c52178290d0f3048940cebabbfb79c9b Mon Sep 17 00:00:00 2001 From: Tyluur Date: Mon, 20 Apr 2026 11:15:04 -0400 Subject: [PATCH 2/5] fix(magic): preserve autocast on manual cast failure and clear stale spell flags --- game/src/main/kotlin/content/skill/magic/Magic.kt | 4 +++- game/src/main/kotlin/content/skill/magic/book/SpellRunes.kt | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/game/src/main/kotlin/content/skill/magic/Magic.kt b/game/src/main/kotlin/content/skill/magic/Magic.kt index 8b16d59d92..8d2a27a209 100644 --- a/game/src/main/kotlin/content/skill/magic/Magic.kt +++ b/game/src/main/kotlin/content/skill/magic/Magic.kt @@ -19,7 +19,9 @@ import world.gregs.voidps.engine.get object Magic { fun castSpell(source: Character, target: Character): Boolean { if (source.spell.isNotBlank() && source is Player && !source.removeSpellItems(source.spell)) { - source.clear("autocast") + if (!source.contains("spell")) { + source.clear("autocast") + } source.clear("spell") source.clear("one_time") return false diff --git a/game/src/main/kotlin/content/skill/magic/book/SpellRunes.kt b/game/src/main/kotlin/content/skill/magic/book/SpellRunes.kt index bd9f56df46..52b8cb2cb8 100644 --- a/game/src/main/kotlin/content/skill/magic/book/SpellRunes.kt +++ b/game/src/main/kotlin/content/skill/magic/book/SpellRunes.kt @@ -9,7 +9,9 @@ class SpellRunes : Script { init { combatPrepare(style = "magic") { _ -> if (spell.isNotBlank() && !hasSpellItems(spell)) { - clear("autocast") + if (!contains("spell")) { + clear("autocast") + } clear("spell") clear("one_time") false From ae609570d1783f0cf2ad8a0f30776693050de77b Mon Sep 17 00:00:00 2001 From: Tyluur Date: Tue, 21 Apr 2026 12:41:24 -0400 Subject: [PATCH 3/5] feat(combat): enable magic spells on players - Add CombatSpellsOnPlayer to handle interface spell clicks via ItemOnPlayerInteract - Route spell interactions through approach system using wildcard handler - Dynamically resolve spell names from interface definitions using cast_id - Support modern (192), ancient (193), lunar (430) spellbooks - Remove redundant isMagicSpell check from Interact tick loop --- .../handle/InterfaceOnPlayerOptionHandler.kt | 15 ++++++- .../skill/magic/CombatSpellsOnPlayer.kt | 45 +++++++++++++++++++ .../content/skill/magic/spell/Spells.kt | 4 +- 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 game/src/main/kotlin/content/skill/magic/CombatSpellsOnPlayer.kt diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnPlayerOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnPlayerOptionHandler.kt index a48c61fc67..30b3bce033 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnPlayerOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnPlayerOptionHandler.kt @@ -1,11 +1,14 @@ package world.gregs.voidps.engine.client.instruction.handle +import com.github.michaelbull.logging.InlineLogger import world.gregs.voidps.engine.client.instruction.InstructionHandler import world.gregs.voidps.engine.client.instruction.InterfaceHandler import world.gregs.voidps.engine.client.ui.closeInterfaces import world.gregs.voidps.engine.entity.character.mode.interact.ItemOnPlayerInteract import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.Players +import world.gregs.voidps.engine.entity.character.player.name +import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.network.client.instruction.InteractInterfacePlayer class InterfaceOnPlayerOptionHandler( @@ -16,9 +19,19 @@ class InterfaceOnPlayerOptionHandler( val (playerIndex, interfaceId, componentId, itemId, itemSlot) = instruction val target = Players.indexed(playerIndex) ?: return false - val (id, component, item) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return false + if ((interfaceId == 192 || interfaceId == 193) && itemId == -1) { + player.closeInterfaces() + player["magic_spell"] = "$interfaceId:$componentId" + player["spellbook"] = if (interfaceId == 193) 1 else 0 + player.mode = ItemOnPlayerInteract(target, "$interfaceId:$componentId", Item.EMPTY, -1, player) + return true + } + + val (id, component, item) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) + ?: return false player.closeInterfaces() player.mode = ItemOnPlayerInteract(target, "$id:$component", item, itemSlot, player) + return true } } diff --git a/game/src/main/kotlin/content/skill/magic/CombatSpellsOnPlayer.kt b/game/src/main/kotlin/content/skill/magic/CombatSpellsOnPlayer.kt new file mode 100644 index 0000000000..0887d7318f --- /dev/null +++ b/game/src/main/kotlin/content/skill/magic/CombatSpellsOnPlayer.kt @@ -0,0 +1,45 @@ +package content.skill.magic.spell + +import com.github.michaelbull.logging.InlineLogger +import content.skill.magic.Magic +import world.gregs.voidps.cache.definition.Params +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.variable.hasClock +import world.gregs.voidps.engine.client.variable.start +import world.gregs.voidps.engine.data.definition.InterfaceDefinitions +import world.gregs.voidps.engine.entity.Approachable +import world.gregs.voidps.engine.entity.character.player.name + +class CombatSpellsOnPlayer : Script { + private val logger = InlineLogger() + + init { + Approachable.onPlayer.getOrPut("*") { mutableListOf() }.add { interact -> + val id = interact.id + if (!id.startsWith("192:") &&!id.startsWith("193:") &&!id.startsWith("194:") &&!id.startsWith("430:")) return@add + if (hasClock("action_delay")) return@add + + val parts = id.split(":") + val ifaceId = parts[0].toInt() + val compId = parts[1].toInt() + + val defs = InterfaceDefinitions.definitions + if (ifaceId >= defs.size) return@add + val def = defs[ifaceId] + val component = def.components?.get(compId)?: return@add + val spell = component.stringId + + if (component.params?.get(Params.id("cast_id")) == null) return@add + + logger.debug { "Spell $id ($spell) on ${interact.target.name}" } + approachRange(10) + this.spell = spell + set("one_time", true) + + interact.updateInteraction { + start("action_delay", 4) + Magic.castSpell(this@add, interact.target) + } + } + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/skill/magic/spell/Spells.kt b/game/src/main/kotlin/content/skill/magic/spell/Spells.kt index 76b944e802..5e3b436b55 100644 --- a/game/src/main/kotlin/content/skill/magic/spell/Spells.kt +++ b/game/src/main/kotlin/content/skill/magic/spell/Spells.kt @@ -23,7 +23,9 @@ class Spells : Script { if (spell.endsWith("_burst") || spell.endsWith("_barrage")) { val targets = multiTargets(target, 9) for (targ in targets) { - targ.directHit(this, random.nextInt(0..damage), type, weapon, spell) + // damage can be -1 on a miss — clamp to 0 + val splash = if (damage > 0) random.nextInt(0..damage) else 0 + targ.directHit(this, splash, type, weapon, spell) } } } From b406b134137937759da271c42839b29f155fe766 Mon Sep 17 00:00:00 2001 From: Tyluur Date: Tue, 21 Apr 2026 12:58:43 -0400 Subject: [PATCH 4/5] fix(combat): stop magic walking to target on manual casts - End ItemOnPlayerInteract after initial cast to transfer to Combat mode - Removes updateInteraction that reset launched flag and re-pathed - Manual casts now behave like autocast: approach once, stand at range - Prevents step-forward on each spell click --- .../entity/character/mode/interact/Interact.kt | 3 ++- .../content/skill/magic/CombatSpellsOnPlayer.kt | 14 +++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt index e2dc9308d3..71e044ea49 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt @@ -130,7 +130,8 @@ open class Interact( interacted = false clearInteracted = false } - if (!character.hasMenuOpen()) { + // Don't move if we just interacted at range + if (!character.hasMenuOpen() && !interacted) { super.tick() } if (!interacted || updateRange) { diff --git a/game/src/main/kotlin/content/skill/magic/CombatSpellsOnPlayer.kt b/game/src/main/kotlin/content/skill/magic/CombatSpellsOnPlayer.kt index 0887d7318f..bde7e84e86 100644 --- a/game/src/main/kotlin/content/skill/magic/CombatSpellsOnPlayer.kt +++ b/game/src/main/kotlin/content/skill/magic/CombatSpellsOnPlayer.kt @@ -1,13 +1,14 @@ -package content.skill.magic.spell +package content.skill.magic import com.github.michaelbull.logging.InlineLogger -import content.skill.magic.Magic +import content.skill.magic.spell.spell import world.gregs.voidps.cache.definition.Params import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.variable.hasClock import world.gregs.voidps.engine.client.variable.start import world.gregs.voidps.engine.data.definition.InterfaceDefinitions import world.gregs.voidps.engine.entity.Approachable +import world.gregs.voidps.engine.entity.character.mode.EmptyMode import world.gregs.voidps.engine.entity.character.player.name class CombatSpellsOnPlayer : Script { @@ -36,9 +37,12 @@ class CombatSpellsOnPlayer : Script { this.spell = spell set("one_time", true) - interact.updateInteraction { - start("action_delay", 4) - Magic.castSpell(this@add, interact.target) + start("action_delay", 4) + Magic.castSpell(this@add, interact.target) + + // End Interact, let Combat mode handle subsequent casts + if (mode == interact) { + mode = EmptyMode } } } From b9a8e44117cf1bf0104709926ce24fa347096a8f Mon Sep 17 00:00:00 2001 From: Tyluur Date: Tue, 21 Apr 2026 13:07:00 -0400 Subject: [PATCH 5/5] fix(combat): exclude caster from AoE magic splash damage - Add source parameter to multiTargets() to filter out attacker - Prevents ice barrage/burst from hitting self in multi when adjacent to target - Updates spell handler to pass caster reference --- game/src/main/kotlin/content/skill/magic/spell/Spells.kt | 2 +- .../kotlin/content/skill/melee/weapon/SpecialAttackMelee.kt | 4 ++-- .../src/main/kotlin/content/skill/ranged/weapon/Chinchompa.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/game/src/main/kotlin/content/skill/magic/spell/Spells.kt b/game/src/main/kotlin/content/skill/magic/spell/Spells.kt index 5e3b436b55..e5a13f80d4 100644 --- a/game/src/main/kotlin/content/skill/magic/spell/Spells.kt +++ b/game/src/main/kotlin/content/skill/magic/spell/Spells.kt @@ -21,7 +21,7 @@ class Spells : Script { return@combatAttack } if (spell.endsWith("_burst") || spell.endsWith("_barrage")) { - val targets = multiTargets(target, 9) + val targets = multiTargets(this, target, 9) for (targ in targets) { // damage can be -1 on a miss — clamp to 0 val splash = if (damage > 0) random.nextInt(0..damage) else 0 diff --git a/game/src/main/kotlin/content/skill/melee/weapon/SpecialAttackMelee.kt b/game/src/main/kotlin/content/skill/melee/weapon/SpecialAttackMelee.kt index 9622b23c2b..f1a4f74243 100644 --- a/game/src/main/kotlin/content/skill/melee/weapon/SpecialAttackMelee.kt +++ b/game/src/main/kotlin/content/skill/melee/weapon/SpecialAttackMelee.kt @@ -8,13 +8,13 @@ import world.gregs.voidps.engine.entity.character.player.Players import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.map.spiral -fun multiTargets(target: Character, hits: Int): List { +fun multiTargets(source: Character, target: Character, hits: Int): List { val group = if (target is Player) Players else NPCs val targets = mutableListOf() for (tile in target.tile.spiral(1)) { val characters = group.at(tile) for (character in characters) { - if (character == target || !character.inMultiCombat) { + if (character == target || character == source || !character.inMultiCombat) { continue } targets.add(character) diff --git a/game/src/main/kotlin/content/skill/ranged/weapon/Chinchompa.kt b/game/src/main/kotlin/content/skill/ranged/weapon/Chinchompa.kt index aa7a7658cb..37d1219856 100644 --- a/game/src/main/kotlin/content/skill/ranged/weapon/Chinchompa.kt +++ b/game/src/main/kotlin/content/skill/ranged/weapon/Chinchompa.kt @@ -19,7 +19,7 @@ class Chinchompa : Script { combatAttack("range") { (target, damage, type, weapon, spell) -> if (weapon.id.endsWith("chinchompa") && target.inMultiCombat) { - val targets = multiTargets(target, if (target is Player) 9 else 11) + val targets = multiTargets(this, target, if (target is Player) 9 else 11) for (targ in targets) { targ.directHit(this, random.nextInt(0..damage), type, weapon, spell) }