From ae609570d1783f0cf2ad8a0f30776693050de77b Mon Sep 17 00:00:00 2001 From: Tyluur Date: Tue, 21 Apr 2026 12:41:24 -0400 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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) }