From f7df0c5e85973ff3e3770e19f319502e87e54caa Mon Sep 17 00:00:00 2001 From: a-thoren Date: Tue, 18 Aug 2026 18:19:23 +0200 Subject: [PATCH 1/8] feat: add events for player hit queueing and impacts as well as the ability to roll accuracy in the boss DSL --- api/bosses/build.gradle.kts | 1 + .../rsmod/api/bosses/dsl/EffectBuilders.kt | 2 + .../org/rsmod/api/bosses/runtime/BossDeps.kt | 4 ++ .../api/bosses/runtime/EffectInterpreter.kt | 61 +++++++++++++------ .../org/rsmod/api/bosses/spec/DamageExpr.kt | 9 ++- .../combat/commons/player/PlayerExtensions.kt | 17 ++++++ .../api/combat/manager/PlayerAttackManager.kt | 7 ++- .../kotlin/org/rsmod/api/combat/NvPCombat.kt | 15 ++--- .../rsmod/api/core/module/EntityHitModule.kt | 3 + .../rsmod/api/player/hit/PlayerExtensions.kt | 13 ++-- .../hit/modifier/StandardPlayerHitModifier.kt | 13 +++- .../processor/DamageOnlyPlayerHitProcessor.kt | 3 + .../processor/StandardPlayerHitProcessor.kt | 3 + .../api/player/protect/ProtectedAccess.kt | 12 ++-- .../player/protect/ProtectedAccessContext.kt | 3 + .../protect/ProtectedAccessContextFactory.kt | 4 ++ .../api/script/PlayerScriptEventExtensions.kt | 4 ++ 17 files changed, 129 insertions(+), 45 deletions(-) diff --git a/api/bosses/build.gradle.kts b/api/bosses/build.gradle.kts index 41b7f29e7..7ae2dc361 100644 --- a/api/bosses/build.gradle.kts +++ b/api/bosses/build.gradle.kts @@ -6,6 +6,7 @@ dependencies { implementation(libs.guice) implementation(projects.api.pluginCommons) implementation(projects.api.combat.combatCommons) + implementation(projects.api.combat.combatFormulas) implementation(projects.api.npc) implementation(projects.api.player) implementation(projects.api.random) diff --git a/api/bosses/src/main/kotlin/org/rsmod/api/bosses/dsl/EffectBuilders.kt b/api/bosses/src/main/kotlin/org/rsmod/api/bosses/dsl/EffectBuilders.kt index f3df4be92..0efaadb80 100644 --- a/api/bosses/src/main/kotlin/org/rsmod/api/bosses/dsl/EffectBuilders.kt +++ b/api/bosses/src/main/kotlin/org/rsmod/api/bosses/dsl/EffectBuilders.kt @@ -2,6 +2,7 @@ package org.rsmod.api.bosses.dsl import dev.openrune.types.NpcMode import org.rsmod.api.bosses.spec.* +import org.rsmod.api.combat.commons.types.MeleeAttackType as EngineMeleeAttackType fun anim(seq: String, delay: Int = 0): Effect = Effect.Anim(seq, delay) @@ -185,6 +186,7 @@ typealias Rotation = Selector.Rotation typealias Run = Effect.Run typealias TransitionTo = Effect.TransitionTo typealias AllInRadius = TargetExpr.AllInRadius +typealias MeleeAttackType = EngineMeleeAttackType val OnDeath: Condition = Condition.OnDeath val OnSpawn: Condition = Condition.OnSpawn diff --git a/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/BossDeps.kt b/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/BossDeps.kt index 7f3efa14d..4838b334d 100644 --- a/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/BossDeps.kt +++ b/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/BossDeps.kt @@ -2,6 +2,8 @@ package org.rsmod.api.bosses.runtime import jakarta.inject.Inject import jakarta.inject.Singleton +import org.rsmod.api.combat.formulas.AccuracyFormulae +import org.rsmod.api.player.hit.modifier.PlayerHitModifier import org.rsmod.api.random.GameRandom import org.rsmod.api.repo.npc.NpcRepository import org.rsmod.api.repo.world.WorldRepository @@ -21,4 +23,6 @@ class BossDeps @Inject constructor( val collision: CollisionFlagMap, val encounterRegistry: EncounterRegistry, val extensionRegistry: BossExtensionRegistry, + val accuracy: AccuracyFormulae, + val playerHitModifier: PlayerHitModifier, ) diff --git a/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/EffectInterpreter.kt b/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/EffectInterpreter.kt index 82d2c1aa4..226b057ae 100644 --- a/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/EffectInterpreter.kt +++ b/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/EffectInterpreter.kt @@ -10,8 +10,9 @@ import org.rsmod.api.bosses.spec.* import org.rsmod.api.bosses.spec.HitType as BossHitType import org.rsmod.api.combat.commons.CombatEffects import org.rsmod.api.combat.commons.DragonfireProtection +import org.rsmod.api.combat.commons.player.finishNpcHit +import org.rsmod.api.combat.commons.types.MeleeAttackType import org.rsmod.api.npc.access.StandardNpcAccess -import org.rsmod.api.player.hit.queueHit import org.rsmod.api.player.output.mes import org.rsmod.api.player.stat.hitpoints import org.rsmod.game.entity.Npc @@ -118,13 +119,15 @@ class EffectInterpreter( } val delay = hit.delay.coerceAtLeast(1) for (t in targets) { - var damage = evaluateDamage(hit.damage) + var damage = evaluateDamage(hit.damage, hit.type, t) dragonfireType(hit.type)?.let { dfType -> - val cap = DragonfireProtection.resolveMaxHit(t, dfType, damageMax(hit.damage)) + val cap = DragonfireProtection.resolveMaxHit(t, dfType, damageMax(hit.damage, hit.type, t)) damage = if (cap <= 0) 0 else deps.random.of(cap + 1) } - hit.spotanim?.let { t.spotanim(it, height = hit.spotanimHeight) } - t.queueHit(npc, delay, hit.type.toEngine(), damage) + if (damage > 0) { + hit.spotanim?.let { t.spotanim(it, height = hit.spotanimHeight) } + } + t.finishNpcHit(npc, delay, hit.type.toEngine(), damage, deps.playerHitModifier) } } @@ -152,12 +155,15 @@ class EffectInterpreter( deps.worldRepo.projAnim(projAnim) proj.hit?.let { hit -> - var damage = evaluateDamage(hit.damage) + var damage = evaluateDamage(hit.damage, hit.type, t) dragonfireType(hit.type)?.let { dfType -> - val cap = DragonfireProtection.resolveMaxHit(t, dfType, damageMax(hit.damage)) + val cap = DragonfireProtection.resolveMaxHit(t, dfType, damageMax(hit.damage, hit.type, t)) damage = if (cap <= 0) 0 else deps.random.of(cap + 1) } - t.queueHit(npc, projAnim.serverCycles, hit.type.toEngine(), damage) + if (damage > 0) { + hit.spotanim?.let { t.spotanim(it, height = hit.spotanimHeight) } + } + t.finishNpcHit(npc, projAnim.serverCycles, hit.type.toEngine(), damage, deps.playerHitModifier) } } @@ -168,8 +174,8 @@ class EffectInterpreter( it.coords.chebyshevDistance(center) <= radius } for (t in targets) { - val damage = evaluateDamage(aoe.damage) - t.queueHit(npc, 0, aoe.type.toEngine(), damage) + val damage = evaluateDamage(aoe.damage, aoe.type, t) + t.finishNpcHit(npc, 0, aoe.type.toEngine(), damage, deps.playerHitModifier) } } @@ -203,7 +209,8 @@ class EffectInterpreter( deps.playerList.filter { it.coords.chebyshevDistance(center) <= effect.targetRadius } for (player in landing) { if (player.hitpoints > 0 && player.coords in tileSet) { - player.queueHit(npc, 1, effect.type.toEngine(), evaluateDamage(effect.damage)) + val damage = evaluateDamage(effect.damage, effect.type, player) + player.finishNpcHit(npc, 1, effect.type.toEngine(), damage, deps.playerHitModifier) } } } @@ -307,11 +314,11 @@ class EffectInterpreter( } } - private fun damageMax(expr: DamageExpr): Int = + private fun damageMax(expr: DamageExpr, hitType: BossHitType, t: Player): Int = when (expr) { is DamageExpr.Roll -> if (expr.range.isEmpty()) 0 else expr.range.last is DamageExpr.Fixed -> expr.value - else -> evaluateDamage(expr) + else -> evaluateDamage(expr, hitType, t) } private fun dragonfireType(t: BossHitType): DragonfireProtection.DragonfireType? = @@ -322,7 +329,7 @@ class EffectInterpreter( else -> null } - private fun evaluateDamage(expr: DamageExpr): Int { + private fun evaluateDamage(expr: DamageExpr, hitType: BossHitType, t: Player): Int { return when (expr) { is DamageExpr.Fixed -> expr.value is DamageExpr.Roll -> @@ -330,13 +337,31 @@ class EffectInterpreter( else { expr.range.first + deps.random.of(expr.range.last - expr.range.first + 1) } - is DamageExpr.Accuracy -> evaluateDamage(expr.on) - is DamageExpr.PercentOfTargetHp -> (target.hitpoints * expr.fraction).toInt() - is DamageExpr.Min -> minOf(evaluateDamage(expr.a), evaluateDamage(expr.b)) - is DamageExpr.Max -> maxOf(evaluateDamage(expr.a), evaluateDamage(expr.b)) + is DamageExpr.Accuracy -> { + val landed = rollAccuracy(hitType, t, expr.meleeAttackType) + evaluateDamage(if (landed) expr.on else expr.miss, hitType, t) + } + is DamageExpr.PercentOfTargetHp -> (t.hitpoints * expr.fraction).toInt() + is DamageExpr.Min -> minOf(evaluateDamage(expr.a, hitType, t), evaluateDamage(expr.b, hitType, t)) + is DamageExpr.Max -> maxOf(evaluateDamage(expr.a, hitType, t), evaluateDamage(expr.b, hitType, t)) } } + private fun rollAccuracy( + hitType: BossHitType, + t: Player, + meleeAttackType: MeleeAttackType? = null, + ): Boolean = + when (hitType) { + BossHitType.Melee -> deps.accuracy.rollMeleeAccuracy(npc, t, meleeAttackType, deps.random) + BossHitType.Ranged -> deps.accuracy.rollRangedAccuracy(npc, t, deps.random) + BossHitType.Magic, + BossHitType.Dragonfire, + BossHitType.DragonfireMetal, + BossHitType.WyvernIce -> deps.accuracy.rollMagicAccuracy(npc, t, deps.random) + BossHitType.Typeless -> true + } + private fun BossHitType.toEngine(): HitType = when (this) { BossHitType.Melee -> HitType.Melee BossHitType.Ranged -> HitType.Ranged diff --git a/api/bosses/src/main/kotlin/org/rsmod/api/bosses/spec/DamageExpr.kt b/api/bosses/src/main/kotlin/org/rsmod/api/bosses/spec/DamageExpr.kt index e81ce7b21..537e6b127 100644 --- a/api/bosses/src/main/kotlin/org/rsmod/api/bosses/spec/DamageExpr.kt +++ b/api/bosses/src/main/kotlin/org/rsmod/api/bosses/spec/DamageExpr.kt @@ -1,9 +1,16 @@ package org.rsmod.api.bosses.spec +import org.rsmod.api.combat.commons.types.MeleeAttackType + sealed interface DamageExpr { data class Fixed(val value: Int) : DamageExpr data class Roll(val range: IntRange) : DamageExpr - data class Accuracy(val on: DamageExpr, val miss: DamageExpr = Fixed(0)) : DamageExpr + + data class Accuracy( + val on: DamageExpr, + val miss: DamageExpr = Fixed(0), + val meleeAttackType: MeleeAttackType? = null, + ) : DamageExpr data class PercentOfTargetHp(val fraction: Double) : DamageExpr data class Min(val a: DamageExpr, val b: DamageExpr) : DamageExpr data class Max(val a: DamageExpr, val b: DamageExpr) : DamageExpr diff --git a/api/combat/combat-commons/src/main/kotlin/org/rsmod/api/combat/commons/player/PlayerExtensions.kt b/api/combat/combat-commons/src/main/kotlin/org/rsmod/api/combat/commons/player/PlayerExtensions.kt index ec1b86d35..a9fdcee63 100644 --- a/api/combat/combat-commons/src/main/kotlin/org/rsmod/api/combat/commons/player/PlayerExtensions.kt +++ b/api/combat/combat-commons/src/main/kotlin/org/rsmod/api/combat/commons/player/PlayerExtensions.kt @@ -7,6 +7,8 @@ import dev.openrune.types.ItemServerType import dev.openrune.types.SequenceServerType import kotlin.math.min import org.rsmod.api.config.refs.params +import org.rsmod.api.player.hit.modifier.PlayerHitModifier +import org.rsmod.api.player.hit.queueHit import org.rsmod.api.player.lefthand import org.rsmod.api.player.protect.ProtectedAccess import org.rsmod.api.player.righthand @@ -15,6 +17,8 @@ import org.rsmod.game.entity.Npc import org.rsmod.game.entity.Player import org.rsmod.game.entity.npc.NpcUid import org.rsmod.game.entity.player.PlayerUid +import org.rsmod.game.hit.Hit +import org.rsmod.game.hit.HitType import org.rsmod.game.type.getOrNull private val ProtectedAccess.autoRetaliateDisabled by boolVarp("varp.option_nodef") @@ -54,6 +58,19 @@ public fun ProtectedAccess.combatRetaliate(uid: PlayerUid, flinchDelay: Int) { opPlayer2(source) } +public fun Player.finishNpcHit( + source: Npc, + delay: Int, + type: HitType, + damage: Int, + modifier: PlayerHitModifier, +): Hit { + queueCombatRetaliate(source) + val hit = queueHit(source, delay, type, damage, modifier) + combatPlayDefendAnim() + return hit +} + public fun Player.combatPlayDefendAnim(clientDelay: Int = 0) { val righthandType = getOrNull(righthand) val lefthandType = getOrNull(lefthand) diff --git a/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt b/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt index 223c6e0a2..b20418fc3 100644 --- a/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt +++ b/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt @@ -32,6 +32,7 @@ import org.rsmod.api.combat.formulas.MaxHitFormulae import org.rsmod.api.config.refs.params import org.rsmod.api.death.PvPPlayerHitHook import org.rsmod.api.npc.hit.modifier.NpcHitModifier +import org.rsmod.api.player.hit.modifier.PlayerHitModifier import org.rsmod.api.npc.hit.queueHit import org.rsmod.api.player.hit.queueHit import org.rsmod.api.player.cheat.adminMaxHit @@ -67,6 +68,7 @@ constructor( private val accuracy: AccuracyFormulae, private val maxHits: MaxHitFormulae, private val npcHitModifier: NpcHitModifier, + private val playerHitModifier: PlayerHitModifier, private val npcInteractions: NpcInteractions, private val npcTInteractions: NpcTInteractions, private val playerInteractions: PlayerInteractions, @@ -720,7 +722,7 @@ constructor( // last entries in the queue list at the time of processing. target.queueCombatRetaliate(source) - val hit = target.queueHit(source, delay, HitType.Melee, damage) + val hit = target.queueHit(source, delay, HitType.Melee, damage, playerHitModifier) notifyPlayerHit(source, target) target.combatPlayDefendAnim() return hit @@ -1060,6 +1062,7 @@ constructor( delay = hitDelay, type = HitType.Ranged, damage = damage, + modifier = playerHitModifier, sourceSecondary = ammo, ) notifyPlayerHit(source, target) @@ -1112,6 +1115,7 @@ constructor( delay = hitDelay, type = HitType.Ranged, damage = damage, + modifier = playerHitModifier, sourceSecondary = ammo, ) return hit @@ -1571,6 +1575,7 @@ constructor( delay = hitDelay, type = HitType.Magic, damage = damage, + modifier = playerHitModifier, sourceSecondary = spell, ) notifyPlayerHit(source, target) diff --git a/api/combat/combat-scripts/src/main/kotlin/org/rsmod/api/combat/NvPCombat.kt b/api/combat/combat-scripts/src/main/kotlin/org/rsmod/api/combat/NvPCombat.kt index 54a44a22b..35d71d860 100644 --- a/api/combat/combat-scripts/src/main/kotlin/org/rsmod/api/combat/NvPCombat.kt +++ b/api/combat/combat-scripts/src/main/kotlin/org/rsmod/api/combat/NvPCombat.kt @@ -6,8 +6,7 @@ import dev.openrune.types.ProjAnimType import jakarta.inject.Inject import org.rsmod.api.combat.commons.CombatAttack import org.rsmod.api.combat.commons.npc.attackRate -import org.rsmod.api.combat.commons.player.combatPlayDefendAnim -import org.rsmod.api.combat.commons.player.queueCombatRetaliate +import org.rsmod.api.combat.commons.player.finishNpcHit import org.rsmod.api.combat.formulas.AccuracyFormulae import org.rsmod.api.combat.formulas.MaxHitFormulae import org.rsmod.api.combat.npc.attackingPlayer @@ -17,7 +16,7 @@ import org.rsmod.api.combat.player.lastCombat import org.rsmod.api.config.refs.params import org.rsmod.api.npc.access.StandardNpcAccess import org.rsmod.api.npc.isInCombat -import org.rsmod.api.player.hit.queueHit +import org.rsmod.api.player.hit.modifier.PlayerHitModifier import org.rsmod.api.player.isValidTarget import org.rsmod.api.player.output.soundSynth import org.rsmod.api.repo.world.WorldRepository @@ -31,6 +30,7 @@ constructor( private val accuracy: AccuracyFormulae, private val maxHits: MaxHitFormulae, private val worldRepo: WorldRepository, + private val hitModifier: PlayerHitModifier, ) { fun attack(access: StandardNpcAccess, target: Player, attack: CombatAttack.NpcAttack) { when (attack) { @@ -139,14 +139,7 @@ constructor( damage: Int, ) { setAttackVars(target) - - // Note: Retaliation must be queued _before_ the hit. If queued after, every hit would - // trigger the "speed-up" death mechanic, since the hit queues would no longer be the - // last entries in the queue list at the time of processing. - target.queueCombatRetaliate(npc) - - target.queueHit(npc, delay, type, damage) - target.combatPlayDefendAnim() + target.finishNpcHit(npc, delay, type, damage, hitModifier) } private fun canAttack(target: Player): Boolean { diff --git a/api/core/src/main/kotlin/org/rsmod/api/core/module/EntityHitModule.kt b/api/core/src/main/kotlin/org/rsmod/api/core/module/EntityHitModule.kt index 3562db52d..850381170 100644 --- a/api/core/src/main/kotlin/org/rsmod/api/core/module/EntityHitModule.kt +++ b/api/core/src/main/kotlin/org/rsmod/api/core/module/EntityHitModule.kt @@ -5,6 +5,8 @@ import org.rsmod.api.npc.hit.modifier.NpcHitModifier import org.rsmod.api.npc.hit.modifier.StandardNpcHitModifier import org.rsmod.api.npc.hit.processor.NpcHitProcessor import org.rsmod.api.npc.hit.processor.StandardNpcHitProcessor +import org.rsmod.api.player.hit.modifier.PlayerHitModifier +import org.rsmod.api.player.hit.modifier.StandardPlayerHitModifier import org.rsmod.api.player.hit.processor.DamageOnlyPlayerHitProcessor import org.rsmod.api.player.hit.processor.InstantPlayerHitProcessor import org.rsmod.module.ExtendedModule @@ -14,6 +16,7 @@ public object EntityHitModule : ExtendedModule() { newSetBinding() bindBaseInstance(StandardNpcHitModifier::class.java) bindBaseInstance(StandardNpcHitProcessor::class.java) + bindBaseInstance(StandardPlayerHitModifier::class.java) bindBaseInstance(DamageOnlyPlayerHitProcessor::class.java) } } diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/hit/PlayerExtensions.kt b/api/player/src/main/kotlin/org/rsmod/api/player/hit/PlayerExtensions.kt index 902062b48..4e0bf1cf0 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/hit/PlayerExtensions.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/hit/PlayerExtensions.kt @@ -7,7 +7,6 @@ import org.rsmod.api.config.refs.done.BaseHitmarkGroups import org.rsmod.api.config.refs.done.hitmark_groups import org.rsmod.api.player.hit.modifier.NoopPlayerHitModifier import org.rsmod.api.player.hit.modifier.PlayerHitModifier -import org.rsmod.api.player.hit.modifier.StandardPlayerHitModifier import org.rsmod.api.player.hit.processor.DamageOnlyPlayerHitProcessor import org.rsmod.api.player.hit.processor.InstantPlayerHitProcessor import org.rsmod.api.player.hit.processor.QueuedPlayerHitProcessor @@ -63,11 +62,11 @@ public fun Player.queueHit( delay: Int, type: HitType, damage: Int, + modifier: PlayerHitModifier, hitmark: HitmarkTypeGroup = hitmark_groups.regular_damage, specific: Boolean = false, sourceWeapon: ItemServerType? = null, sourceSecondary: ItemServerType? = null, - modifier: PlayerHitModifier = StandardPlayerHitModifier, ): Hit { val cappedDamage = min(hitpoints, damage) val builder = @@ -123,9 +122,9 @@ public fun Player.queueHit( delay: Int, type: HitType, damage: Int, + modifier: PlayerHitModifier, hitmark: HitmarkTypeGroup = hitmark_groups.regular_damage, sourceSecondary: ItemServerType? = null, - modifier: PlayerHitModifier = StandardPlayerHitModifier, ): Hit { val builder = InternalPlayerHits.createBuilder( @@ -178,9 +177,9 @@ public fun Player.queueHit( delay: Int, type: HitType, damage: Int, + modifier: PlayerHitModifier, hitmark: HitmarkTypeGroup = hitmark_groups.regular_damage, specific: Boolean = false, - modifier: PlayerHitModifier = StandardPlayerHitModifier, strongQueue: Boolean = true, ): Hit { val builder = @@ -311,11 +310,11 @@ public fun Player.queueImpactHit( delay: Int, type: HitType, damage: Int, + modifier: PlayerHitModifier, hitmark: HitmarkTypeGroup = hitmark_groups.regular_damage, specific: Boolean = false, sourceWeapon: ItemServerType? = null, sourceSecondary: ItemServerType? = null, - modifier: PlayerHitModifier = StandardPlayerHitModifier, ) { val builder = InternalPlayerHits.createBuilder( @@ -371,9 +370,9 @@ public fun Player.queueImpactHit( delay: Int, type: HitType, damage: Int, + modifier: PlayerHitModifier, hitmark: HitmarkTypeGroup = hitmark_groups.regular_damage, sourceSecondary: ItemServerType? = null, - modifier: PlayerHitModifier = StandardPlayerHitModifier, ) { val builder = InternalPlayerHits.createBuilder( @@ -424,9 +423,9 @@ public fun Player.queueImpactHit( delay: Int, type: HitType, damage: Int, + modifier: PlayerHitModifier, hitmark: HitmarkTypeGroup = hitmark_groups.regular_damage, specific: Boolean = false, - modifier: PlayerHitModifier = StandardPlayerHitModifier, strongQueue: Boolean = true, ) { val builder = diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/hit/modifier/StandardPlayerHitModifier.kt b/api/player/src/main/kotlin/org/rsmod/api/player/hit/modifier/StandardPlayerHitModifier.kt index b4577a82d..04ed2b36b 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/hit/modifier/StandardPlayerHitModifier.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/hit/modifier/StandardPlayerHitModifier.kt @@ -1,13 +1,19 @@ package org.rsmod.api.player.hit.modifier +import jakarta.inject.Inject import org.rsmod.api.player.cheat.adminGodMode +import org.rsmod.api.player.events.PlayerHitEvents import org.rsmod.api.player.hit.PlayerAbsorption +import org.rsmod.events.EventBus import org.rsmod.game.entity.Player import org.rsmod.game.hit.HitBuilder import org.rsmod.game.hit.HitType -public object StandardPlayerHitModifier : PlayerHitModifier { +public class StandardPlayerHitModifier @Inject constructor(private val eventBus: EventBus) : + PlayerHitModifier { override fun HitBuilder.modify(target: Player) { + target.publishEvent(this) + if (target.adminGodMode) { damage = 0 return @@ -34,4 +40,9 @@ public object StandardPlayerHitModifier : PlayerHitModifier { ) } } + + private fun Player.publishEvent(hit: HitBuilder) { + val event = PlayerHitEvents.Modify(this, hit) + eventBus.publish(event) + } } diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/DamageOnlyPlayerHitProcessor.kt b/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/DamageOnlyPlayerHitProcessor.kt index 69be38262..417105f76 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/DamageOnlyPlayerHitProcessor.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/DamageOnlyPlayerHitProcessor.kt @@ -6,6 +6,7 @@ import org.rsmod.game.damage.recordDamageOn import org.rsmod.api.player.cheat.adminGodMode import org.rsmod.api.player.death.recordDeathCause import org.rsmod.api.player.death.resolveDeathCause +import org.rsmod.api.player.events.PlayerHitEvents import org.rsmod.api.player.events.PlayerHitpointsChangedEvent import org.rsmod.api.player.headbar.InternalPlayerHeadbars import org.rsmod.api.player.queueDeath @@ -59,6 +60,8 @@ constructor( val headbar = hit.createHeadbar(hitpoints, baseHitpointsLvl) showHeadbar(headbar) + + eventBus.publish(PlayerHitEvents.Impact(this, hit)) } private fun Hit.createHeadbar(currHp: Int, maxHp: Int): Headbar = diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/StandardPlayerHitProcessor.kt b/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/StandardPlayerHitProcessor.kt index 171146cc4..760ffa864 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/StandardPlayerHitProcessor.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/StandardPlayerHitProcessor.kt @@ -12,6 +12,7 @@ import org.rsmod.api.player.death.DEATH_CAUSE_ATTR import org.rsmod.api.player.death.DeathCause import org.rsmod.api.player.death.recordDeathCause import org.rsmod.api.player.death.resolveDeathCause +import org.rsmod.api.player.events.PlayerHitEvents import org.rsmod.api.player.events.PlayerHitpointsChangedEvent import org.rsmod.api.player.headbar.InternalPlayerHeadbars import org.rsmod.api.player.lefthand @@ -65,6 +66,8 @@ public object StandardPlayerHitProcessor : QueuedPlayerHitProcessor { val headbar = hit.createHeadbar(player.hitpoints, player.baseHitpointsLvl) player.showHeadbar(headbar) + + publish(PlayerHitEvents.Impact(player, hit)) } private fun ProtectedAccess.publishPlayerHitpointsChangedEvent(oldHitpoints: Int, hit: Hit) { diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt index 89af046f7..44e9ebeb5 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt @@ -1432,7 +1432,7 @@ public class ProtectedAccess( specific: Boolean = false, sourceWeapon: ItemServerType? = null, sourceSecondary: ItemServerType? = null, - modifier: PlayerHitModifier = StandardPlayerHitModifier, + modifier: PlayerHitModifier = context.hitModifier, ): Hit { return player.queueHit( source = source, @@ -1490,7 +1490,7 @@ public class ProtectedAccess( damage: Int, hitmark: HitmarkTypeGroup = hitmark_groups.regular_damage, sourceSecondary: ItemServerType? = null, - modifier: PlayerHitModifier = StandardPlayerHitModifier, + modifier: PlayerHitModifier = context.hitModifier, ): Hit { return player.queueHit( source = source, @@ -1544,7 +1544,7 @@ public class ProtectedAccess( damage: Int, hitmark: HitmarkTypeGroup = hitmark_groups.regular_damage, specific: Boolean = false, - modifier: PlayerHitModifier = StandardPlayerHitModifier, + modifier: PlayerHitModifier = context.hitModifier, strongQueue: Boolean = true, ): Hit { return player.queueHit( @@ -1639,7 +1639,7 @@ public class ProtectedAccess( specific: Boolean = false, sourceWeapon: ItemServerType? = null, sourceSecondary: ItemServerType? = null, - modifier: PlayerHitModifier = StandardPlayerHitModifier, + modifier: PlayerHitModifier = context.hitModifier, ) { player.queueImpactHit( source = source, @@ -1698,7 +1698,7 @@ public class ProtectedAccess( damage: Int, hitmark: HitmarkTypeGroup = hitmark_groups.regular_damage, sourceSecondary: ItemServerType? = null, - modifier: PlayerHitModifier = StandardPlayerHitModifier, + modifier: PlayerHitModifier = context.hitModifier, ) { player.queueImpactHit( source = source, @@ -1750,7 +1750,7 @@ public class ProtectedAccess( damage: Int, hitmark: HitmarkTypeGroup = hitmark_groups.regular_damage, specific: Boolean = false, - modifier: PlayerHitModifier = StandardPlayerHitModifier, + modifier: PlayerHitModifier = context.hitModifier, ) { player.queueImpactHit( delay = delay, diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContext.kt b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContext.kt index edd77a0b9..560232039 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContext.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContext.kt @@ -3,6 +3,7 @@ package org.rsmod.api.player.protect import org.rsmod.api.area.checker.AreaChecker import org.rsmod.api.market.MarketPrices import org.rsmod.api.player.dialogue.align.TextAlignment +import org.rsmod.api.player.hit.modifier.PlayerHitModifier import org.rsmod.api.player.hit.processor.InstantPlayerHitProcessor import org.rsmod.api.player.interact.HeldInteractions import org.rsmod.api.player.interact.LocInteractions @@ -84,9 +85,11 @@ public data class ProtectedAccessContext( private val getMarketPrices: () -> MarketPrices, private val getInstantHitProcessor: () -> InstantPlayerHitProcessor, private val getTeleportValidator: () -> PlayerTeleportValidator, + private val getHitModifier: () -> PlayerHitModifier, ) { public val random: GameRandom by lazyLoad { getRandom() } public val eventBus: EventBus by lazyLoad { getEventBus() } + public val hitModifier: PlayerHitModifier by lazyLoad { getHitModifier() } public val npcList: NpcList by lazyLoad { getNpcList() } public val playerList: PlayerList by lazyLoad { getPlayerList() } public val collision: CollisionFlagMap by lazyLoad { getCollision() } diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContextFactory.kt b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContextFactory.kt index e6a28b4ab..ede9cf337 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContextFactory.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContextFactory.kt @@ -4,6 +4,7 @@ import jakarta.inject.Inject import org.rsmod.api.area.checker.AreaChecker import org.rsmod.api.market.MarketPrices import org.rsmod.api.player.dialogue.align.TextAlignment +import org.rsmod.api.player.hit.modifier.PlayerHitModifier import org.rsmod.api.player.hit.processor.InstantPlayerHitProcessor import org.rsmod.api.player.interact.HeldInteractions import org.rsmod.api.player.interact.LocInteractions @@ -37,11 +38,13 @@ constructor( private val marketPrices: MarketPrices, private val instantHitProcessor: InstantPlayerHitProcessor, private val teleportValidator: PlayerTeleportValidator, + private val hitModifier: PlayerHitModifier, ) { public fun create(): ProtectedAccessContext = ProtectedAccessContext( getRandom = { random }, getEventBus = { eventBus }, + getHitModifier = { hitModifier }, getNpcList = { npcList }, getPlayerList = { playerList }, getCollision = { collision }, @@ -77,6 +80,7 @@ constructor( getMarketPrices = { error("No market prices provided.") }, getInstantHitProcessor = { error("No instant hit processor provided.") }, getTeleportValidator = { error("No teleport validator provided.") }, + getHitModifier = { error("No hit modifier provided.") }, ) /** diff --git a/api/script/src/main/kotlin/org/rsmod/api/script/PlayerScriptEventExtensions.kt b/api/script/src/main/kotlin/org/rsmod/api/script/PlayerScriptEventExtensions.kt index 29d4e28ce..83a0cd83a 100644 --- a/api/script/src/main/kotlin/org/rsmod/api/script/PlayerScriptEventExtensions.kt +++ b/api/script/src/main/kotlin/org/rsmod/api/script/PlayerScriptEventExtensions.kt @@ -4,6 +4,7 @@ import dev.openrune.definition.type.widget.ComponentType import dev.openrune.rscm.RSCM.asRSCM import dev.openrune.rscm.RSCMType import dev.openrune.types.ItemServerType +import org.rsmod.api.player.events.PlayerHitEvents import org.rsmod.api.player.events.PlayerHitpointsChangedEvent import org.rsmod.api.player.events.PlayerMovementEvent import org.rsmod.api.player.events.PlayerQueueEvents @@ -106,6 +107,9 @@ public fun ScriptContext.onPlayerCoordsChanged(action: PlayerMovementEvent.Coord public fun ScriptContext.onPlayerHitpointsChanged(action: PlayerHitpointsChangedEvent.() -> Unit): Unit = onEvent(action) +public fun ScriptContext.onPlayerHit(action: PlayerHitEvents.Impact.() -> Unit): Unit = + onEvent(action) + public fun ScriptContext.onWorldMapClick( requiredRights: Rights, action: suspend ProtectedAccess.(WorldMapClick) -> Unit, From 073a643fabec4627a8be39493d686da76f21940b Mon Sep 17 00:00:00 2001 From: a-thoren Date: Tue, 18 Aug 2026 18:20:10 +0200 Subject: [PATCH 2/8] feat: add events for player hit queueing and impacts as well as the ability to roll accuracy in the boss DSL --- .../org/rsmod/api/player/events/PlayerHitEvents.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 api/player/src/main/kotlin/org/rsmod/api/player/events/PlayerHitEvents.kt diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/events/PlayerHitEvents.kt b/api/player/src/main/kotlin/org/rsmod/api/player/events/PlayerHitEvents.kt new file mode 100644 index 000000000..d8e1e249e --- /dev/null +++ b/api/player/src/main/kotlin/org/rsmod/api/player/events/PlayerHitEvents.kt @@ -0,0 +1,12 @@ +package org.rsmod.api.player.events + +import org.rsmod.events.UnboundEvent +import org.rsmod.game.entity.Player +import org.rsmod.game.hit.Hit +import org.rsmod.game.hit.HitBuilder + +public class PlayerHitEvents { + public data class Modify(public val player: Player, public val hit: HitBuilder) : UnboundEvent + + public data class Impact(public val player: Player, public val hit: Hit) : UnboundEvent +} From b932142e4df3788d1b4a476f26cb828eead266fd Mon Sep 17 00:00:00 2001 From: a-thoren Date: Tue, 18 Aug 2026 18:20:40 +0200 Subject: [PATCH 3/8] feat: start on demonic gorillas --- .data/gamevals/varn.rscm | 6 + .data/raw-cache/server/varn.toml | 18 ++ .../hit/modifier/StandardNpcHitModifier.kt | 15 ++ .../bosses/demonicgorilla/DemonicGorilla.kt | 221 ++++++++++++++++++ 4 files changed, 260 insertions(+) create mode 100644 content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt diff --git a/.data/gamevals/varn.rscm b/.data/gamevals/varn.rscm index c3c644ba3..adf2fb29d 100644 --- a/.data/gamevals/varn.rscm +++ b/.data/gamevals/varn.rscm @@ -4,3 +4,9 @@ generic_state_2=2 attacking_player=3 lastattack=4 flat_armour=5 +immune_melee=6 +immune_ranged=7 +immune_magic=8 +gorilla_protect_damage=9 +gorilla_protect_hits=10 +gorilla_miss_streak=11 diff --git a/.data/raw-cache/server/varn.toml b/.data/raw-cache/server/varn.toml index c91e27ecf..8f682fa92 100644 --- a/.data/raw-cache/server/varn.toml +++ b/.data/raw-cache/server/varn.toml @@ -15,3 +15,21 @@ id = "varn.lastattack" [[varn]] id = "varn.flat_armour" + +[[varn]] +id = "varn.immune_melee" + +[[varn]] +id = "varn.immune_ranged" + +[[varn]] +id = "varn.immune_magic" + +[[varn]] +id = "varn.gorilla_protect_damage" + +[[varn]] +id = "varn.gorilla_protect_hits" + +[[varn]] +id = "varn.gorilla_miss_streak" diff --git a/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/modifier/StandardNpcHitModifier.kt b/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/modifier/StandardNpcHitModifier.kt index 10f7d9332..67e7e9498 100644 --- a/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/modifier/StandardNpcHitModifier.kt +++ b/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/modifier/StandardNpcHitModifier.kt @@ -7,11 +7,13 @@ import org.rsmod.api.npc.events.NpcHitEvents import org.rsmod.events.EventBus import org.rsmod.game.entity.Npc import org.rsmod.game.hit.HitBuilder +import org.rsmod.game.hit.HitType public class StandardNpcHitModifier @Inject constructor(private val eventBus: EventBus) : NpcHitModifier { override fun HitBuilder.modify(target: Npc) { target.publishEvent(this) + target.applyStyleImmunity(this) target.applyFlatArmour(this) } @@ -20,6 +22,19 @@ public class StandardNpcHitModifier @Inject constructor(private val eventBus: Ev eventBus.publish(event) } + private fun Npc.applyStyleImmunity(hit: HitBuilder) { + val immune = + when (hit.type) { + HitType.Typeless -> false + HitType.Melee -> vars["varn.immune_melee"] == 1 + HitType.Ranged -> vars["varn.immune_ranged"] == 1 + HitType.Magic -> vars["varn.immune_magic"] == 1 + } + if (immune) { + hit.damage = 0 + } + } + private fun Npc.applyFlatArmour(hit: HitBuilder) { val armour = vars["varn.flat_armour"] diff --git a/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt b/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt new file mode 100644 index 000000000..39d39f881 --- /dev/null +++ b/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt @@ -0,0 +1,221 @@ +package org.rsmod.content.bosses.demonicgorilla + +import dev.openrune.ServerCacheManager +import dev.openrune.rscm.RSCM.asRSCM +import dev.openrune.rscm.RSCMType +import jakarta.inject.Inject +import org.rsmod.api.bosses.dsl.* +import org.rsmod.api.bosses.runtime.BossCombat +import org.rsmod.api.bosses.runtime.BossDeps +import org.rsmod.api.bosses.runtime.encounter +import org.rsmod.api.bosses.spec.BossSpec +import org.rsmod.api.bosses.spec.DamageExpr +import org.rsmod.api.npc.events.NpcHitEvents +import org.rsmod.api.player.events.PlayerHitEvents +import org.rsmod.api.script.onEvent +import org.rsmod.game.entity.Npc +import org.rsmod.game.entity.NpcList +import org.rsmod.game.entity.npc.NpcStateEvents +import org.rsmod.game.hit.Hit +import org.rsmod.game.hit.HitType +import org.rsmod.plugin.scripts.PluginScript +import org.rsmod.plugin.scripts.ScriptContext + +private const val PHASE_MELEE = "melee" +private const val PHASE_RANGED = "ranged" +private const val PHASE_MAGIC = "magic" +private val PHASES = listOf(PHASE_MELEE, PHASE_RANGED, PHASE_MAGIC) + +class DemonicGorilla +@Inject +constructor(private val deps: BossDeps, private val npcList: NpcList) : PluginScript() { + + val spec: BossSpec = + boss( + "npc.mm2_demon_gorilla_1_melee", + "npc.mm2_demon_gorilla_1_ranged", + "npc.mm2_demon_gorilla_1_magic", + "npc.mm2_demon_gorilla_2_melee", + "npc.mm2_demon_gorilla_2_ranged", + "npc.mm2_demon_gorilla_2_magic", + ) { + stats(attackRate = ATTACK_RATE, aggressionRadius = 8) + + val meleeAttack = + ability("melee_attack") { + anim(MELEE_ATTACK_SEQ) + hit { + damage(Accuracy(Roll(0..MAX_HIT), meleeAttackType = MeleeAttackType.Crush)) + type(Melee) + delay = MELEE_HIT_DELAY + } + } + val rangedAttack = + ability("ranged_attack") { + anim(RANGED_ATTACK_SEQ) + projectile { + spotanim = RANGED_PROJECTILE_SPOT + hit { + damage(Accuracy(Roll(0..MAX_HIT))) + type(Ranged) + spotanim(RANGED_IMPACT_SPOT) + } + } + } + val magicAttack = + ability("magic_attack") { + anim(MAGIC_ATTACK_SEQ) + projectile { + spotanim = MAGIC_PROJECTILE_SPOT + hit { + damage(Accuracy(Roll(0..MAX_HIT))) + type(Magic) + spotanim(MAGIC_IMPACT_SPOT) + } + } + } + val boulder = + ability("boulder") { + anim(BOULDER_SEQ) + debris( + telegraph = BOULDER_TELEGRAPH_SPOT, + impact = BOULDER_IMPACT_SPOT, + damage = DamageExpr.PercentOfTargetHp(BOULDER_DAMAGE_FRACTION), + type = Typeless, + windup = BOULDER_WINDUP_TICKS, + targetRadius = 0, + scatterRadius = 0, + count = 1..1, + center = CurrentTargetTile, + ) + } + + phase(PHASE_MELEE) { weightedSelectorRandom { +random(meleeAttack, weight = 1) } } + phase(PHASE_RANGED) { + weightedSelectorRandom { + +random(rangedAttack, weight = 3) + +random(boulder, weight = 1) + } + } + phase(PHASE_MAGIC) { + weightedSelectorRandom { + +random(magicAttack, weight = 3) + +random(boulder, weight = 1) + } + } + } + + override fun ScriptContext.startup() { + BossCombat.register(this, spec, deps, onModifyHit = { GorillaProtection.onModifyHit(this) }) + + val bossIds = spec.npcTypes.mapNotNullTo(mutableSetOf()) { it.npcTypeId() } + onEvent { if (npc.type.id in bossIds) GorillaProtection.reset(npc) } + onEvent { + if (npc.type.id in bossIds) GorillaProtection.reset(npc) + } + onEvent { onAttackImpact(bossIds, hit) } + } + + /** Tracks the outgoing miss streak that drives the gorilla's style-switch trigger. */ + private fun onAttackImpact(bossIds: Set, hit: Hit) { + if (!hit.isFromNpc || hit.type == HitType.Typeless) return + val npc = hit.resolveNpcSource(npcList) ?: return + if (npc.type.id !in bossIds) return + + if (hit.damage > 0) { + npc.vars["varn.gorilla_miss_streak"] = 0 + return + } + + val encounter = deps.encounter(npc) + val style = encounter.currentPhaseName + val missStreak = npc.vars["varn.gorilla_miss_streak"] + 1 + if (missStreak >= MISS_STREAK_THRESHOLD) { + npc.say(SWITCH_SCREAM) + val others = PHASES.filter { it != style } + val next = others[deps.random.of(others.size)] + encounter.transitionTo(next, deps.mapClock.cycle) + npc.vars["varn.gorilla_miss_streak"] = 0 + } else { + npc.vars["varn.gorilla_miss_streak"] = missStreak + } + } + + private fun String.npcTypeId(): Int? = ServerCacheManager.getNpc(this.asRSCM(RSCMType.NPC))?.id + + private companion object { + private const val ATTACK_RATE = 5 + private const val MAX_HIT = 31 + private const val MELEE_HIT_DELAY = 1 + + private const val MISS_STREAK_THRESHOLD = 3 + + private const val BOULDER_WINDUP_TICKS = 2 + private const val BOULDER_DAMAGE_FRACTION = 0.33 + private const val SWITCH_SCREAM = "Rhaaaaaaa!" + + private const val BOULDER_SEQ = "seq.demonic_gorilla_smash_chest" + private const val BOULDER_TELEGRAPH_SPOT = "spotanim.mm2_gorilla_stone" + private const val BOULDER_IMPACT_SPOT = "spotanim.mm2_gorilla_stonesplat" + + private const val MELEE_ATTACK_SEQ = "seq.demonic_gorilla_punch" + private const val RANGED_ATTACK_SEQ = "seq.demonic_gorilla_range" + private const val MAGIC_ATTACK_SEQ = "seq.demonic_gorilla_magic" + + private const val RANGED_IMPACT_SPOT = "spotanim.mm2_gorilla_stonesplat" + private const val MAGIC_IMPACT_SPOT = "spotanim.mm2_gorilla_spellsplat" + + private const val RANGED_PROJECTILE_SPOT = "spotanim.mm2_gorilla_stone" + private const val MAGIC_PROJECTILE_SPOT = "spotanim.mm2_gorilla_spell" + } +} + +private object GorillaProtection { + private const val PROTECT_HIT_THRESHOLD = 4 + private const val PROTECT_DAMAGE_THRESHOLD = 70 + + fun reset(npc: Npc) { + npc.vars["varn.gorilla_protect_damage"] = 0 + npc.vars["varn.gorilla_protect_hits"] = 0 + npc.vars["varn.gorilla_miss_streak"] = 0 + applyImmunity(npc, PHASES.random()) + } + + fun onModifyHit(event: NpcHitEvents.Modify) { + if (!event.hit.isFromPlayer) return + val style = + when (event.hit.type) { + HitType.Melee -> PHASE_MELEE + HitType.Ranged -> PHASE_RANGED + HitType.Magic -> PHASE_MAGIC + else -> return + } + val npc = event.npc + + if (style == currentProtectStyle(npc)) return + + val damage = npc.vars["varn.gorilla_protect_damage"] + event.hit.damage + val hits = npc.vars["varn.gorilla_protect_hits"] + 1 + if (hits >= PROTECT_HIT_THRESHOLD && damage >= PROTECT_DAMAGE_THRESHOLD) { + npc.vars["varn.gorilla_protect_damage"] = 0 + npc.vars["varn.gorilla_protect_hits"] = 0 + applyImmunity(npc, style) + } else { + npc.vars["varn.gorilla_protect_damage"] = damage + npc.vars["varn.gorilla_protect_hits"] = hits + } + } + + private fun currentProtectStyle(npc: Npc): String = + when { + npc.vars["varn.immune_melee"] == 1 -> PHASE_MELEE + npc.vars["varn.immune_ranged"] == 1 -> PHASE_RANGED + else -> PHASE_MAGIC + } + + private fun applyImmunity(npc: Npc, style: String) { + npc.vars["varn.immune_melee"] = if (style == PHASE_MELEE) 1 else 0 + npc.vars["varn.immune_ranged"] = if (style == PHASE_RANGED) 1 else 0 + npc.vars["varn.immune_magic"] = if (style == PHASE_MAGIC) 1 else 0 + } +} From 109b41030710a34bf58e7826c7469081a05e0e70 Mon Sep 17 00:00:00 2001 From: a-thoren Date: Sun, 30 Aug 2026 20:30:17 +0200 Subject: [PATCH 4/8] More demonic gorillas --- .../bosses/demonic-gorilla/build.gradle.kts | 19 ++ .../bosses/demonicgorilla/DemonicGorilla.kt | 204 +++++++++++------- 2 files changed, 150 insertions(+), 73 deletions(-) create mode 100644 content/bosses/demonic-gorilla/build.gradle.kts diff --git a/content/bosses/demonic-gorilla/build.gradle.kts b/content/bosses/demonic-gorilla/build.gradle.kts new file mode 100644 index 000000000..59394d9c6 --- /dev/null +++ b/content/bosses/demonic-gorilla/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("base-conventions") + +} + +dependencies { + implementation(projects.api.bosses) + implementation(projects.api.pluginCommons) + implementation(projects.api.combat.combatCommons) + implementation(projects.api.combat.combatFormulas) + implementation(projects.api.npc) + implementation(projects.api.player) + implementation(projects.api.random) + implementation(projects.api.script) + implementation(projects.engine.game) + implementation(projects.engine.map) + implementation(projects.engine.plugin) + implementation(projects.engine.coroutine) +} diff --git a/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt b/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt index 39d39f881..c0951c7a9 100644 --- a/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt +++ b/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt @@ -4,6 +4,7 @@ import dev.openrune.ServerCacheManager import dev.openrune.rscm.RSCM.asRSCM import dev.openrune.rscm.RSCMType import jakarta.inject.Inject +import org.rsmod.annotations.InternalApi import org.rsmod.api.bosses.dsl.* import org.rsmod.api.bosses.runtime.BossCombat import org.rsmod.api.bosses.runtime.BossDeps @@ -26,19 +27,27 @@ private const val PHASE_RANGED = "ranged" private const val PHASE_MAGIC = "magic" private val PHASES = listOf(PHASE_MELEE, PHASE_RANGED, PHASE_MAGIC) -class DemonicGorilla -@Inject -constructor(private val deps: BossDeps, private val npcList: NpcList) : PluginScript() { +private val GORILLA_TYPE_NAMES: List = + listOf( + "npc.mm2_demon_gorilla_1_melee", + "npc.mm2_demon_gorilla_1_ranged", + "npc.mm2_demon_gorilla_1_magic", + "npc.mm2_demon_gorilla_2_melee", + "npc.mm2_demon_gorilla_2_ranged", + "npc.mm2_demon_gorilla_2_magic", + ) + +class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val npcList: NpcList) : + PluginScript() { + + /** Npc type id → its own RSCM name, for every gorilla variant. */ + private val nameByTypeId: Map = + GORILLA_TYPE_NAMES.associateBy { + requireNotNull(it.npcTypeId()) { "Missing npc type: $it" } + } val spec: BossSpec = - boss( - "npc.mm2_demon_gorilla_1_melee", - "npc.mm2_demon_gorilla_1_ranged", - "npc.mm2_demon_gorilla_1_magic", - "npc.mm2_demon_gorilla_2_melee", - "npc.mm2_demon_gorilla_2_ranged", - "npc.mm2_demon_gorilla_2_magic", - ) { + boss(*GORILLA_TYPE_NAMES.toTypedArray()) { stats(attackRate = ATTACK_RATE, aggressionRadius = 8) val meleeAttack = @@ -90,6 +99,8 @@ constructor(private val deps: BossDeps, private val npcList: NpcList) : PluginSc ) } + // dump: no gorilla observed attacking with melee (demonic_gorilla_punch) ever threw a + // boulder; the one boulder came from a gorilla in its ranged attack phase (see below). phase(PHASE_MELEE) { weightedSelectorRandom { +random(meleeAttack, weight = 1) } } phase(PHASE_RANGED) { weightedSelectorRandom { @@ -106,16 +117,89 @@ constructor(private val deps: BossDeps, private val npcList: NpcList) : PluginSc } override fun ScriptContext.startup() { - BossCombat.register(this, spec, deps, onModifyHit = { GorillaProtection.onModifyHit(this) }) + BossCombat.register(this, spec, deps, onModifyHit = { onModifyProtectionHit(this) }) val bossIds = spec.npcTypes.mapNotNullTo(mutableSetOf()) { it.npcTypeId() } - onEvent { if (npc.type.id in bossIds) GorillaProtection.reset(npc) } - onEvent { - if (npc.type.id in bossIds) GorillaProtection.reset(npc) - } + onEvent { if (npc.type.id in bossIds) resetGorilla(npc) } + onEvent { if (npc.type.id in bossIds) resetGorilla(npc) } onEvent { onAttackImpact(bossIds, hit) } } + /** + * Initialises a freshly (re)spawned gorilla: + * - Protection prayer is taken from its spawn identity (`npc.type`, which keeps its original + * gameval suffix through later [transmogToStyle] calls), not randomized - wiki: a gorilla's + * overhead is fixed by which variant spawned. + * - Attack style (encounter phase) IS randomized - wiki: "start with a random attack style". + * Attack style and protection are independent, so this can differ from the protection style. + * + * Runs from the `Create`/`Respawn` handlers, which are registered after [BossCombat]'s own + * `Respawn` reset, so the encounter created here survives that reset. + */ + private fun resetGorilla(npc: Npc) { + val protectStyle = nameByTypeId[npc.type.id]?.substringAfterLast('_') ?: return + npc.vars["varn.gorilla_protect_damage"] = 0 + npc.vars["varn.gorilla_protect_hits"] = 0 + npc.vars["varn.gorilla_miss_streak"] = 0 + applyImmunity(npc, protectStyle) + + val startStyle = PHASES[deps.random.of(PHASES.size)] + deps.encounter(npc).transitionTo(startStyle, deps.mapClock.cycle) + } + + /** Tracks the outgoing damage/hit counters that drive the gorilla's protection prayer switch. */ + private fun onModifyProtectionHit(event: NpcHitEvents.Modify) { + if (!event.hit.isFromPlayer) return + val style = + when (event.hit.type) { + HitType.Melee -> PHASE_MELEE + HitType.Ranged -> PHASE_RANGED + HitType.Magic -> PHASE_MAGIC + else -> return + } + val npc = event.npc + + if (style == currentProtectStyle(npc)) return + + val damage = npc.vars["varn.gorilla_protect_damage"] + event.hit.damage + val hits = npc.vars["varn.gorilla_protect_hits"] + 1 + if (hits >= PROTECT_HIT_THRESHOLD && damage >= PROTECT_DAMAGE_THRESHOLD) { + npc.vars["varn.gorilla_protect_damage"] = 0 + npc.vars["varn.gorilla_protect_hits"] = 0 + applyImmunity(npc, style) + transmogToStyle(npc, style) + } else { + npc.vars["varn.gorilla_protect_damage"] = damage + npc.vars["varn.gorilla_protect_hits"] = hits + } + } + + private fun currentProtectStyle(npc: Npc): String = + when { + npc.vars["varn.immune_melee"] == 1 -> PHASE_MELEE + npc.vars["varn.immune_ranged"] == 1 -> PHASE_RANGED + else -> PHASE_MAGIC + } + + private fun applyImmunity(npc: Npc, style: String) { + npc.vars["varn.immune_melee"] = if (style == PHASE_MELEE) 1 else 0 + npc.vars["varn.immune_ranged"] = if (style == PHASE_RANGED) 1 else 0 + npc.vars["varn.immune_magic"] = if (style == PHASE_MAGIC) 1 else 0 + } + + /** + * Transmogs [npc] into the sibling gorilla type sharing its spawn family (the RSCM name minus + * its style suffix) with [style]'s suffix. + */ + @OptIn(InternalApi::class) + private fun transmogToStyle(npc: Npc, style: String) { + val spawnName = nameByTypeId[npc.type.id] ?: return + val newName = spawnName.substringBeforeLast('_') + "_" + style + val npcType = newName.npcTypeId()?.let(ServerCacheManager::getNpc) ?: return + npc.transmog(npcType, Int.MAX_VALUE) + npc.assignUid() + } + /** Tracks the outgoing miss streak that drives the gorilla's style-switch trigger. */ private fun onAttackImpact(bossIds: Set, hit: Hit) { if (!hit.isFromNpc || hit.type == HitType.Typeless) return @@ -131,7 +215,10 @@ constructor(private val deps: BossDeps, private val npcList: NpcList) : PluginSc val style = encounter.currentPhaseName val missStreak = npc.vars["varn.gorilla_miss_streak"] + 1 if (missStreak >= MISS_STREAK_THRESHOLD) { - npc.say(SWITCH_SCREAM) + // TODO: the wiki documents a "bone-chilling scream" / `Rhaaaaaaa!` cue on the style + // switch. It is NOT a `say` packet (the rev235 dump rsprox-3342-rev235 has zero gorilla + // `say` lines across many switches) nor an attributable area sound in that capture, so + // it is most likely a client-side synth sound - id still unknown. Re-add once identified. val others = PHASES.filter { it != style } val next = others[deps.random.of(others.size)] encounter.transitionTo(next, deps.mapClock.cycle) @@ -144,19 +231,40 @@ constructor(private val deps: BossDeps, private val npcList: NpcList) : PluginSc private fun String.npcTypeId(): Int? = ServerCacheManager.getNpc(this.asRSCM(RSCMType.NPC))?.id private companion object { - private const val ATTACK_RATE = 5 - private const val MAX_HIT = 31 + // "dump:" = cross-checked against proxy capture rsprox-3342-rev235 (rev235, ~25 kills, one + // boulder at tick 2876). "wiki:" = https://oldschool.runescape.wiki/w/Demonic_gorilla/Strategies + private const val ATTACK_RATE = 5 // dump: confirmed - same gorilla re-attacks exactly 5t apart + // wiki: "each one hitting up to 30 damage". Dump only saw 21 land (through a prayer lapse), + // which is consistent. Old value of 31 was slightly over. + private const val MAX_HIT = 30 private const val MELEE_HIT_DELAY = 1 + // wiki: switches attack style after 3 missed hits (prayer-blocked counts; boulder does not). private const val MISS_STREAK_THRESHOLD = 3 + // wiki: switches protection prayer after >=4 hits AND >=70 total damage in the unprotected + // style(s) - e.g. 49 melee then 26 ranged -> immediately Protect from Missiles. + private const val PROTECT_HIT_THRESHOLD = 4 + private const val PROTECT_DAMAGE_THRESHOLD = 70 - private const val BOULDER_WINDUP_TICKS = 2 + // dump: rock projanim launches on the cast tick and the splash map-anim lands on the target + // tile 5 ticks later (cast t2876 -> impact t2881); projanim heights 600 -> 10. + private const val BOULDER_WINDUP_TICKS = 5 + // wiki: "Players will take 33% of their health as damage if they don't move away" - i.e. a + // fraction of *current* hp, so PercentOfTargetHp(0.33) is the right model. private const val BOULDER_DAMAGE_FRACTION = 0.33 - private const val SWITCH_SCREAM = "Rhaaaaaaa!" + // dump: the windup anim on the casting gorilla is demonic_gorilla_smash_chest (also its idle + // taunt), so this is right - there is no dedicated boulder sequence. private const val BOULDER_SEQ = "seq.demonic_gorilla_smash_chest" - private const val BOULDER_TELEGRAPH_SPOT = "spotanim.mm2_gorilla_stone" - private const val BOULDER_IMPACT_SPOT = "spotanim.mm2_gorilla_stonesplat" + // dump: the boulder uses the falling-rock projanim `myarm_rock_roc_travel` (id 856), NOT the + // ranged attack's `mm2_gorilla_stone`. Impact is the map-anim `castlewars_catapult_splash` + // (id 305) plus area sound 1444. `mm2_gorilla_stone*` are the ordinary ranged attack only. + // The lone boulder (tick 2876) came from npc 15186, which was in its ranged attack phase + // (its prior attacks at t2782/t2787 were demonic_gorilla_range) though its npc id stayed + // mm2_demon_gorilla_2_melee throughout - i.e. attack phase drove it, protection id was + // independent and unchanged. + private const val BOULDER_TELEGRAPH_SPOT = "spotanim.myarm_rock_roc_travel" + private const val BOULDER_IMPACT_SPOT = "spotanim.castlewars_catapult_splash" private const val MELEE_ATTACK_SEQ = "seq.demonic_gorilla_punch" private const val RANGED_ATTACK_SEQ = "seq.demonic_gorilla_range" @@ -169,53 +277,3 @@ constructor(private val deps: BossDeps, private val npcList: NpcList) : PluginSc private const val MAGIC_PROJECTILE_SPOT = "spotanim.mm2_gorilla_spell" } } - -private object GorillaProtection { - private const val PROTECT_HIT_THRESHOLD = 4 - private const val PROTECT_DAMAGE_THRESHOLD = 70 - - fun reset(npc: Npc) { - npc.vars["varn.gorilla_protect_damage"] = 0 - npc.vars["varn.gorilla_protect_hits"] = 0 - npc.vars["varn.gorilla_miss_streak"] = 0 - applyImmunity(npc, PHASES.random()) - } - - fun onModifyHit(event: NpcHitEvents.Modify) { - if (!event.hit.isFromPlayer) return - val style = - when (event.hit.type) { - HitType.Melee -> PHASE_MELEE - HitType.Ranged -> PHASE_RANGED - HitType.Magic -> PHASE_MAGIC - else -> return - } - val npc = event.npc - - if (style == currentProtectStyle(npc)) return - - val damage = npc.vars["varn.gorilla_protect_damage"] + event.hit.damage - val hits = npc.vars["varn.gorilla_protect_hits"] + 1 - if (hits >= PROTECT_HIT_THRESHOLD && damage >= PROTECT_DAMAGE_THRESHOLD) { - npc.vars["varn.gorilla_protect_damage"] = 0 - npc.vars["varn.gorilla_protect_hits"] = 0 - applyImmunity(npc, style) - } else { - npc.vars["varn.gorilla_protect_damage"] = damage - npc.vars["varn.gorilla_protect_hits"] = hits - } - } - - private fun currentProtectStyle(npc: Npc): String = - when { - npc.vars["varn.immune_melee"] == 1 -> PHASE_MELEE - npc.vars["varn.immune_ranged"] == 1 -> PHASE_RANGED - else -> PHASE_MAGIC - } - - private fun applyImmunity(npc: Npc, style: String) { - npc.vars["varn.immune_melee"] = if (style == PHASE_MELEE) 1 else 0 - npc.vars["varn.immune_ranged"] = if (style == PHASE_RANGED) 1 else 0 - npc.vars["varn.immune_magic"] = if (style == PHASE_MAGIC) 1 else 0 - } -} From 8c9bcfabfb112a708d68d905829789eeb6a78c8e Mon Sep 17 00:00:00 2001 From: a-thoren Date: Mon, 31 Aug 2026 18:22:34 +0200 Subject: [PATCH 5/8] feat: more gorillas --- .data/gamevals/synth.rscm | 1 + .../api/bosses/runtime/EffectInterpreter.kt | 4 +- .../api/combat/manager/PlayerAttackManager.kt | 9 +- .../org/rsmod/api/npc/hit/NpcExtensions.kt | 8 + .../hit/modifier/StandardNpcHitModifier.kt | 11 +- .../bosses/demonicgorilla/DemonicGorilla.kt | 138 +++++++++++------- 6 files changed, 102 insertions(+), 69 deletions(-) diff --git a/.data/gamevals/synth.rscm b/.data/gamevals/synth.rscm index ecf66801b..b1ab2a387 100644 --- a/.data/gamevals/synth.rscm +++ b/.data/gamevals/synth.rscm @@ -192,3 +192,4 @@ crystal_sing=6234 toa_shadow_weapon_cast_fire_01=6410 shooting_star_whisle=4924 shooting_star_crash=4925 +mm2_gorilla_boulder_impact=1444 diff --git a/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/EffectInterpreter.kt b/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/EffectInterpreter.kt index 226b057ae..f0d855aa3 100644 --- a/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/EffectInterpreter.kt +++ b/api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/EffectInterpreter.kt @@ -161,7 +161,9 @@ class EffectInterpreter( damage = if (cap <= 0) 0 else deps.random.of(cap + 1) } if (damage > 0) { - hit.spotanim?.let { t.spotanim(it, height = hit.spotanimHeight) } + hit.spotanim?.let { + t.spotanim(it, delay = projAnim.clientCycles, height = hit.spotanimHeight) + } } t.finishNpcHit(npc, projAnim.serverCycles, hit.type.toEngine(), damage, deps.playerHitModifier) } diff --git a/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt b/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt index b20418fc3..673f347be 100644 --- a/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt +++ b/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt @@ -31,6 +31,7 @@ import org.rsmod.api.combat.formulas.AccuracyFormulae import org.rsmod.api.combat.formulas.MaxHitFormulae import org.rsmod.api.config.refs.params import org.rsmod.api.death.PvPPlayerHitHook +import org.rsmod.api.npc.hit.isStyleImmuneTo import org.rsmod.api.npc.hit.modifier.NpcHitModifier import org.rsmod.api.player.hit.modifier.PlayerHitModifier import org.rsmod.api.npc.hit.queueHit @@ -245,7 +246,7 @@ constructor( } private fun giveCombatXp(player: Player, target: Npc, attack: CombatAttack.Melee, damage: Int) { - if (player.shouldBlockNpcCombatXp(target)) { + if (player.shouldBlockNpcCombatXp(target) || target.isStyleImmuneTo(HitType.Melee)) { return } val cappedDamage = min(damage, target.hitpoints) @@ -316,7 +317,7 @@ constructor( attack: CombatAttack.Ranged, damage: Int, ) { - if (player.shouldBlockNpcCombatXp(target)) { + if (player.shouldBlockNpcCombatXp(target) || target.isStyleImmuneTo(HitType.Ranged)) { return } val cappedDamage = min(damage, target.hitpoints) @@ -378,7 +379,7 @@ constructor( } private fun giveCombatXp(player: Player, target: Npc, attack: CombatAttack.Spell, damage: Int) { - if (player.shouldBlockNpcCombatXp(target)) { + if (player.shouldBlockNpcCombatXp(target) || target.isStyleImmuneTo(HitType.Magic)) { return } val cappedDamage = min(damage, target.hitpoints) @@ -432,7 +433,7 @@ constructor( @Suppress("unused") private fun giveCombatXp(player: Player, target: Npc, attack: CombatAttack.Staff, damage: Int) { - if (player.shouldBlockNpcCombatXp(target)) { + if (player.shouldBlockNpcCombatXp(target) || target.isStyleImmuneTo(HitType.Magic)) { return } val cappedDamage = min(damage, target.hitpoints) diff --git a/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/NpcExtensions.kt b/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/NpcExtensions.kt index 70867ae38..f7b7c8f98 100644 --- a/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/NpcExtensions.kt +++ b/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/NpcExtensions.kt @@ -223,6 +223,14 @@ public fun Npc.visHitmark(): HitmarkTypeGroup { return hitmark } +public fun Npc.isStyleImmuneTo(type: HitType): Boolean = + when (type) { + HitType.Melee -> vars["varn.immune_melee"] == 1 + HitType.Ranged -> vars["varn.immune_ranged"] == 1 + HitType.Magic -> vars["varn.immune_magic"] == 1 + HitType.Typeless -> false + } + private fun Npc.modifyAndQueueHit(delay: Int, builder: HitBuilder, modifier: NpcHitModifier): Hit { modifier.modify(builder, this) val hit = builder.build() diff --git a/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/modifier/StandardNpcHitModifier.kt b/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/modifier/StandardNpcHitModifier.kt index 67e7e9498..3be66801e 100644 --- a/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/modifier/StandardNpcHitModifier.kt +++ b/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/modifier/StandardNpcHitModifier.kt @@ -4,10 +4,10 @@ import jakarta.inject.Inject import kotlin.math.absoluteValue import kotlin.math.min import org.rsmod.api.npc.events.NpcHitEvents +import org.rsmod.api.npc.hit.isStyleImmuneTo import org.rsmod.events.EventBus import org.rsmod.game.entity.Npc import org.rsmod.game.hit.HitBuilder -import org.rsmod.game.hit.HitType public class StandardNpcHitModifier @Inject constructor(private val eventBus: EventBus) : NpcHitModifier { @@ -23,14 +23,7 @@ public class StandardNpcHitModifier @Inject constructor(private val eventBus: Ev } private fun Npc.applyStyleImmunity(hit: HitBuilder) { - val immune = - when (hit.type) { - HitType.Typeless -> false - HitType.Melee -> vars["varn.immune_melee"] == 1 - HitType.Ranged -> vars["varn.immune_ranged"] == 1 - HitType.Magic -> vars["varn.immune_magic"] == 1 - } - if (immune) { + if (isStyleImmuneTo(hit.type)) { hit.damage = 0 } } diff --git a/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt b/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt index c0951c7a9..ce60c4cc2 100644 --- a/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt +++ b/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt @@ -3,19 +3,24 @@ package org.rsmod.content.bosses.demonicgorilla import dev.openrune.ServerCacheManager import dev.openrune.rscm.RSCM.asRSCM import dev.openrune.rscm.RSCMType +import dev.openrune.types.aconverted.SpotanimType import jakarta.inject.Inject import org.rsmod.annotations.InternalApi import org.rsmod.api.bosses.dsl.* import org.rsmod.api.bosses.runtime.BossCombat import org.rsmod.api.bosses.runtime.BossDeps +import org.rsmod.api.bosses.runtime.bossProjectile import org.rsmod.api.bosses.runtime.encounter import org.rsmod.api.bosses.spec.BossSpec -import org.rsmod.api.bosses.spec.DamageExpr +import org.rsmod.api.bosses.spec.ProjectileConfig +import org.rsmod.api.combat.commons.player.finishNpcHit import org.rsmod.api.npc.events.NpcHitEvents import org.rsmod.api.player.events.PlayerHitEvents +import org.rsmod.api.player.stat.hitpoints import org.rsmod.api.script.onEvent import org.rsmod.game.entity.Npc import org.rsmod.game.entity.NpcList +import org.rsmod.game.entity.Player import org.rsmod.game.entity.npc.NpcStateEvents import org.rsmod.game.hit.Hit import org.rsmod.game.hit.HitType @@ -40,7 +45,6 @@ private val GORILLA_TYPE_NAMES: List = class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val npcList: NpcList) : PluginScript() { - /** Npc type id → its own RSCM name, for every gorilla variant. */ private val nameByTypeId: Map = GORILLA_TYPE_NAMES.associateBy { requireNotNull(it.npcTypeId()) { "Missing npc type: $it" } @@ -64,6 +68,7 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val anim(RANGED_ATTACK_SEQ) projectile { spotanim = RANGED_PROJECTILE_SPOT + config = RANGED_PROJECTILE_CONFIG hit { damage(Accuracy(Roll(0..MAX_HIT))) type(Ranged) @@ -76,31 +81,21 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val anim(MAGIC_ATTACK_SEQ) projectile { spotanim = MAGIC_PROJECTILE_SPOT + config = MAGIC_PROJECTILE_CONFIG hit { damage(Accuracy(Roll(0..MAX_HIT))) type(Magic) - spotanim(MAGIC_IMPACT_SPOT) + spotanim(MAGIC_IMPACT_SPOT, height = MAGIC_IMPACT_HEIGHT) } } } + val boulder = ability("boulder") { anim(BOULDER_SEQ) - debris( - telegraph = BOULDER_TELEGRAPH_SPOT, - impact = BOULDER_IMPACT_SPOT, - damage = DamageExpr.PercentOfTargetHp(BOULDER_DAMAGE_FRACTION), - type = Typeless, - windup = BOULDER_WINDUP_TICKS, - targetRadius = 0, - scatterRadius = 0, - count = 1..1, - center = CurrentTargetTile, - ) + include(external(BOULDER_HANDLER)) } - // dump: no gorilla observed attacking with melee (demonic_gorilla_punch) ever threw a - // boulder; the one boulder came from a gorilla in its ranged attack phase (see below). phase(PHASE_MELEE) { weightedSelectorRandom { +random(meleeAttack, weight = 1) } } phase(PHASE_RANGED) { weightedSelectorRandom { @@ -118,6 +113,7 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val override fun ScriptContext.startup() { BossCombat.register(this, spec, deps, onModifyHit = { onModifyProtectionHit(this) }) + deps.extensionRegistry.register(BOULDER_HANDLER) { _, npc, target, _ -> throwBoulder(npc, target) } val bossIds = spec.npcTypes.mapNotNullTo(mutableSetOf()) { it.npcTypeId() } onEvent { if (npc.type.id in bossIds) resetGorilla(npc) } @@ -125,17 +121,32 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val onEvent { onAttackImpact(bossIds, hit) } } - /** - * Initialises a freshly (re)spawned gorilla: - * - Protection prayer is taken from its spawn identity (`npc.type`, which keeps its original - * gameval suffix through later [transmogToStyle] calls), not randomized - wiki: a gorilla's - * overhead is fixed by which variant spawned. - * - Attack style (encounter phase) IS randomized - wiki: "start with a random attack style". - * Attack style and protection are independent, so this can differ from the protection style. - * - * Runs from the `Create`/`Respawn` handlers, which are registered after [BossCombat]'s own - * `Respawn` reset, so the encounter created here survives that reset. - */ + private fun throwBoulder(npc: Npc, target: Player) { + val tile = target.coords + deps.bossProjectile( + spotanim = BOULDER_TELEGRAPH_SPOT.asRSCM(RSCMType.SPOTANIM), + src = tile, + target = tile, + startHeight = BOULDER_PROJ_START_HEIGHT, + endHeight = BOULDER_PROJ_END_HEIGHT, + delay = BOULDER_PROJ_START_DELAY, + travel = BOULDER_PROJ_TRAVEL, + curve = BOULDER_PROJ_ANGLE, + ) + deps.worldQueues.add(BOULDER_WINDUP_TICKS) { + deps.worldRepo.spotanimMap( + SpotanimType(BOULDER_IMPACT_SPOT.asRSCM(RSCMType.SPOTANIM)), + tile, + BOULDER_IMPACT_HEIGHT, + ) + deps.worldRepo.soundArea(tile, BOULDER_IMPACT_SOUND, radius = BOULDER_SOUND_RADIUS) + if (target.hitpoints > 0 && target.coords == tile) { + val damage = (target.hitpoints * BOULDER_DAMAGE_FRACTION).toInt() + target.finishNpcHit(npc, 1, HitType.Typeless, damage, deps.playerHitModifier) + } + } + } + private fun resetGorilla(npc: Npc) { val protectStyle = nameByTypeId[npc.type.id]?.substringAfterLast('_') ?: return npc.vars["varn.gorilla_protect_damage"] = 0 @@ -145,9 +156,13 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val val startStyle = PHASES[deps.random.of(PHASES.size)] deps.encounter(npc).transitionTo(startStyle, deps.mapClock.cycle) + applyStyleRange(npc, startStyle) + } + + private fun applyStyleRange(npc: Npc, style: String) { + npc.apRangeOverride = if (style == PHASE_MELEE) null else RANGED_MAGIC_AP_RANGE } - /** Tracks the outgoing damage/hit counters that drive the gorilla's protection prayer switch. */ private fun onModifyProtectionHit(event: NpcHitEvents.Modify) { if (!event.hit.isFromPlayer) return val style = @@ -166,8 +181,11 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val if (hits >= PROTECT_HIT_THRESHOLD && damage >= PROTECT_DAMAGE_THRESHOLD) { npc.vars["varn.gorilla_protect_damage"] = 0 npc.vars["varn.gorilla_protect_hits"] = 0 - applyImmunity(npc, style) - transmogToStyle(npc, style) + // Queue with 1 tick delay to ensure triggering hit lands + deps.worldQueues.add(1) { + applyImmunity(npc, style) + transmogToStyle(npc, style) + } } else { npc.vars["varn.gorilla_protect_damage"] = damage npc.vars["varn.gorilla_protect_hits"] = hits @@ -215,13 +233,10 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val val style = encounter.currentPhaseName val missStreak = npc.vars["varn.gorilla_miss_streak"] + 1 if (missStreak >= MISS_STREAK_THRESHOLD) { - // TODO: the wiki documents a "bone-chilling scream" / `Rhaaaaaaa!` cue on the style - // switch. It is NOT a `say` packet (the rev235 dump rsprox-3342-rev235 has zero gorilla - // `say` lines across many switches) nor an attributable area sound in that capture, so - // it is most likely a client-side synth sound - id still unknown. Re-add once identified. val others = PHASES.filter { it != style } val next = others[deps.random.of(others.size)] encounter.transitionTo(next, deps.mapClock.cycle) + applyStyleRange(npc, next) npc.vars["varn.gorilla_miss_streak"] = 0 } else { npc.vars["varn.gorilla_miss_streak"] = missStreak @@ -231,40 +246,30 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val private fun String.npcTypeId(): Int? = ServerCacheManager.getNpc(this.asRSCM(RSCMType.NPC))?.id private companion object { - // "dump:" = cross-checked against proxy capture rsprox-3342-rev235 (rev235, ~25 kills, one - // boulder at tick 2876). "wiki:" = https://oldschool.runescape.wiki/w/Demonic_gorilla/Strategies - private const val ATTACK_RATE = 5 // dump: confirmed - same gorilla re-attacks exactly 5t apart - // wiki: "each one hitting up to 30 damage". Dump only saw 21 land (through a prayer lapse), - // which is consistent. Old value of 31 was slightly over. + private const val ATTACK_RATE = 5 + private const val RANGED_MAGIC_AP_RANGE = 7 private const val MAX_HIT = 30 private const val MELEE_HIT_DELAY = 1 - // wiki: switches attack style after 3 missed hits (prayer-blocked counts; boulder does not). private const val MISS_STREAK_THRESHOLD = 3 - // wiki: switches protection prayer after >=4 hits AND >=70 total damage in the unprotected - // style(s) - e.g. 49 melee then 26 ranged -> immediately Protect from Missiles. private const val PROTECT_HIT_THRESHOLD = 4 private const val PROTECT_DAMAGE_THRESHOLD = 70 - // dump: rock projanim launches on the cast tick and the splash map-anim lands on the target - // tile 5 ticks later (cast t2876 -> impact t2881); projanim heights 600 -> 10. private const val BOULDER_WINDUP_TICKS = 5 - // wiki: "Players will take 33% of their health as damage if they don't move away" - i.e. a - // fraction of *current* hp, so PercentOfTargetHp(0.33) is the right model. private const val BOULDER_DAMAGE_FRACTION = 0.33 - // dump: the windup anim on the casting gorilla is demonic_gorilla_smash_chest (also its idle - // taunt), so this is right - there is no dedicated boulder sequence. private const val BOULDER_SEQ = "seq.demonic_gorilla_smash_chest" - // dump: the boulder uses the falling-rock projanim `myarm_rock_roc_travel` (id 856), NOT the - // ranged attack's `mm2_gorilla_stone`. Impact is the map-anim `castlewars_catapult_splash` - // (id 305) plus area sound 1444. `mm2_gorilla_stone*` are the ordinary ranged attack only. - // The lone boulder (tick 2876) came from npc 15186, which was in its ranged attack phase - // (its prior attacks at t2782/t2787 were demonic_gorilla_range) though its npc id stayed - // mm2_demon_gorilla_2_melee throughout - i.e. attack phase drove it, protection id was - // independent and unchanged. + private const val BOULDER_HANDLER = "demonicgorilla.boulder" private const val BOULDER_TELEGRAPH_SPOT = "spotanim.myarm_rock_roc_travel" private const val BOULDER_IMPACT_SPOT = "spotanim.castlewars_catapult_splash" + private const val BOULDER_IMPACT_SOUND = "synth.mm2_gorilla_boulder_impact" + private const val BOULDER_SOUND_RADIUS = 10 + private const val BOULDER_IMPACT_HEIGHT = 10 + private const val BOULDER_PROJ_START_HEIGHT = 600 + private const val BOULDER_PROJ_END_HEIGHT = 10 + private const val BOULDER_PROJ_START_DELAY = 15 + private const val BOULDER_PROJ_TRAVEL = 135 + private const val BOULDER_PROJ_ANGLE = 10 private const val MELEE_ATTACK_SEQ = "seq.demonic_gorilla_punch" private const val RANGED_ATTACK_SEQ = "seq.demonic_gorilla_range" @@ -272,8 +277,31 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val private const val RANGED_IMPACT_SPOT = "spotanim.mm2_gorilla_stonesplat" private const val MAGIC_IMPACT_SPOT = "spotanim.mm2_gorilla_spellsplat" + private const val MAGIC_IMPACT_HEIGHT = 70 private const val RANGED_PROJECTILE_SPOT = "spotanim.mm2_gorilla_stone" private const val MAGIC_PROJECTILE_SPOT = "spotanim.mm2_gorilla_spell" + + private val RANGED_PROJECTILE_CONFIG = + ProjectileConfig( + startHeight = 80, + endHeight = 0, + startDelay = 46, + travelTime = 0, + angle = 5, + progress = 11, + stepMultiplier = 5, + ) + + private val MAGIC_PROJECTILE_CONFIG = + ProjectileConfig( + startHeight = 80, + endHeight = 70, + startDelay = 46, + travelTime = 0, + angle = 5, + progress = 11, + stepMultiplier = 4, + ) } } From 550206f4143421a581d0d774908c9e089a8b4c2c Mon Sep 17 00:00:00 2001 From: a-thoren Date: Mon, 31 Aug 2026 19:25:18 +0200 Subject: [PATCH 6/8] feat: add demonic gorilla kc varp --- .data/gamevals/varp.rscm | 1 + .data/raw-cache/server/npcs.toml | 48 +++++++++++++++++++ .data/raw-cache/server/varp.toml | 6 +++ .../tables/CollectionLogCategoriesTable.kt | 1 + 4 files changed, 56 insertions(+) diff --git a/.data/gamevals/varp.rscm b/.data/gamevals/varp.rscm index 0247f28cc..cb9eee8c7 100644 --- a/.data/gamevals/varp.rscm +++ b/.data/gamevals/varp.rscm @@ -110,3 +110,4 @@ kc_smoke_devil=65615 kc_hydra=65616 kc_thermonuclear_smoke_devil=65654 kc_superior_monster=65655 +kc_demonic_gorilla=65656 diff --git a/.data/raw-cache/server/npcs.toml b/.data/raw-cache/server/npcs.toml index 8b6abbb17..9d8d09001 100644 --- a/.data/raw-cache/server/npcs.toml +++ b/.data/raw-cache/server/npcs.toml @@ -3725,3 +3725,51 @@ id = "npc.grinder_drew" inherit = "npc.grinder_drew" moveRestrict = "NoMove" wanderRange = 0 + +[[npc]] +id = "npc.mm2_demon_gorilla_1_melee" +inherit = "npc.mm2_demon_gorilla_1_melee" +[npc.params] +"param.death_anim"="seq.demonic_gorilla_death" +"param.killcount_varp"="varp.kc_demonic_gorilla" +"param.killcount_notify"=false + +[[npc]] +id = "npc.mm2_demon_gorilla_1_ranged" +inherit = "npc.mm2_demon_gorilla_1_ranged" +[npc.params] +"param.death_anim"="seq.demonic_gorilla_death" +"param.killcount_varp"="varp.kc_demonic_gorilla" +"param.killcount_notify"=false + +[[npc]] +id = "npc.mm2_demon_gorilla_1_magic" +inherit = "npc.mm2_demon_gorilla_1_magic" +[npc.params] +"param.death_anim"="seq.demonic_gorilla_death" +"param.killcount_varp"="varp.kc_demonic_gorilla" +"param.killcount_notify"=false + +[[npc]] +id = "npc.mm2_demon_gorilla_2_melee" +inherit = "npc.mm2_demon_gorilla_2_melee" +[npc.params] +"param.death_anim"="seq.demonic_gorilla_death" +"param.killcount_varp"="varp.kc_demonic_gorilla" +"param.killcount_notify"=false + +[[npc]] +id = "npc.mm2_demon_gorilla_2_ranged" +inherit = "npc.mm2_demon_gorilla_2_ranged" +[npc.params] +"param.death_anim"="seq.demonic_gorilla_death" +"param.killcount_varp"="varp.kc_demonic_gorilla" +"param.killcount_notify"=false + +[[npc]] +id = "npc.mm2_demon_gorilla_2_magic" +inherit = "npc.mm2_demon_gorilla_2_magic" +[npc.params] +"param.death_anim"="seq.demonic_gorilla_death" +"param.killcount_varp"="varp.kc_demonic_gorilla" +"param.killcount_notify"=false diff --git a/.data/raw-cache/server/varp.toml b/.data/raw-cache/server/varp.toml index 694c4b16c..33905a9cd 100644 --- a/.data/raw-cache/server/varp.toml +++ b/.data/raw-cache/server/varp.toml @@ -816,6 +816,12 @@ id = "varp.kc_superior_monster" scope = "Perm" transmit = "Never" +[[varp]] +isServerOnly = true +id = "varp.kc_demonic_gorilla" +scope = "Perm" +transmit = "Never" + [[varp]] isServerOnly = true id = "varp.drew_sandstone" diff --git a/or-cache/src/main/kotlin/dev/openrune/tables/CollectionLogCategoriesTable.kt b/or-cache/src/main/kotlin/dev/openrune/tables/CollectionLogCategoriesTable.kt index 6b3960b7b..29afb5317 100644 --- a/or-cache/src/main/kotlin/dev/openrune/tables/CollectionLogCategoriesTable.kt +++ b/or-cache/src/main/kotlin/dev/openrune/tables/CollectionLogCategoriesTable.kt @@ -620,6 +620,7 @@ object CollectionLogCategoriesTable { row("dbrow.collection_log_category_glough_s_experiments") { column(STRUCT_ID, 526) columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_gorillas_completed") + columnRSCM(COUNT_VARP_1, "varp.kc_demonic_gorilla") } row("dbrow.collection_log_category_hunter_guild") { From 8fae9f00d2becf18bbc65242b04873da29ee5854 Mon Sep 17 00:00:00 2001 From: a-thoren Date: Mon, 31 Aug 2026 19:26:32 +0200 Subject: [PATCH 7/8] fix: ensure protection switching is consistent with multi hitting weapons --- .data/gamevals/varn.rscm | 1 + .data/raw-cache/server/varn.toml | 3 +++ .../bosses/demonicgorilla/DemonicGorilla.kt | 22 +++++++++---------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.data/gamevals/varn.rscm b/.data/gamevals/varn.rscm index adf2fb29d..937b52b27 100644 --- a/.data/gamevals/varn.rscm +++ b/.data/gamevals/varn.rscm @@ -10,3 +10,4 @@ immune_magic=8 gorilla_protect_damage=9 gorilla_protect_hits=10 gorilla_miss_streak=11 +gorilla_protect_switching=12 diff --git a/.data/raw-cache/server/varn.toml b/.data/raw-cache/server/varn.toml index 8f682fa92..25148aed7 100644 --- a/.data/raw-cache/server/varn.toml +++ b/.data/raw-cache/server/varn.toml @@ -33,3 +33,6 @@ id = "varn.gorilla_protect_hits" [[varn]] id = "varn.gorilla_miss_streak" + +[[varn]] +id = "varn.gorilla_protect_switching" diff --git a/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt b/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt index ce60c4cc2..7c727df6a 100644 --- a/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt +++ b/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt @@ -151,6 +151,7 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val val protectStyle = nameByTypeId[npc.type.id]?.substringAfterLast('_') ?: return npc.vars["varn.gorilla_protect_damage"] = 0 npc.vars["varn.gorilla_protect_hits"] = 0 + npc.vars["varn.gorilla_protect_switching"] = 0 npc.vars["varn.gorilla_miss_streak"] = 0 applyImmunity(npc, protectStyle) @@ -178,17 +179,19 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val val damage = npc.vars["varn.gorilla_protect_damage"] + event.hit.damage val hits = npc.vars["varn.gorilla_protect_hits"] + 1 - if (hits >= PROTECT_HIT_THRESHOLD && damage >= PROTECT_DAMAGE_THRESHOLD) { - npc.vars["varn.gorilla_protect_damage"] = 0 - npc.vars["varn.gorilla_protect_hits"] = 0 - // Queue with 1 tick delay to ensure triggering hit lands + npc.vars["varn.gorilla_protect_damage"] = damage + npc.vars["varn.gorilla_protect_hits"] = hits + + val switchDue = hits >= PROTECT_HIT_THRESHOLD && damage >= PROTECT_DAMAGE_THRESHOLD + if (switchDue && npc.vars["varn.gorilla_protect_switching"] == 0) { + npc.vars["varn.gorilla_protect_switching"] = 1 deps.worldQueues.add(1) { applyImmunity(npc, style) transmogToStyle(npc, style) + npc.vars["varn.gorilla_protect_damage"] = 0 + npc.vars["varn.gorilla_protect_hits"] = 0 + npc.vars["varn.gorilla_protect_switching"] = 0 } - } else { - npc.vars["varn.gorilla_protect_damage"] = damage - npc.vars["varn.gorilla_protect_hits"] = hits } } @@ -205,10 +208,6 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val npc.vars["varn.immune_magic"] = if (style == PHASE_MAGIC) 1 else 0 } - /** - * Transmogs [npc] into the sibling gorilla type sharing its spawn family (the RSCM name minus - * its style suffix) with [style]'s suffix. - */ @OptIn(InternalApi::class) private fun transmogToStyle(npc: Npc, style: String) { val spawnName = nameByTypeId[npc.type.id] ?: return @@ -218,7 +217,6 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val npc.assignUid() } - /** Tracks the outgoing miss streak that drives the gorilla's style-switch trigger. */ private fun onAttackImpact(bossIds: Set, hit: Hit) { if (!hit.isFromNpc || hit.type == HitType.Typeless) return val npc = hit.resolveNpcSource(npcList) ?: return From 9ac5a127bf19bc2b2bc9811558d51fc57d72e74c Mon Sep 17 00:00:00 2001 From: a-thoren Date: Fri, 4 Sep 2026 16:17:32 +0200 Subject: [PATCH 8/8] fix: spotless --- .../combat/commons/player/PlayerExtensions.kt | 11 +-- .../api/combat/manager/PlayerAttackManager.kt | 15 ++-- .../kotlin/org/rsmod/api/combat/NvPCombat.kt | 11 ++- .../org/rsmod/api/npc/hit/NpcExtensions.kt | 6 +- .../processor/DamageOnlyPlayerHitProcessor.kt | 2 +- .../processor/StandardPlayerHitProcessor.kt | 13 ++- .../api/player/protect/ProtectedAccess.kt | 85 ++++++++++++------- .../player/protect/ProtectedAccessContext.kt | 2 +- .../protect/ProtectedAccessContextFactory.kt | 2 +- .../api/script/PlayerScriptEventExtensions.kt | 26 +++--- .../bosses/demonicgorilla/DemonicGorilla.kt | 4 +- 11 files changed, 99 insertions(+), 78 deletions(-) diff --git a/api/combat/combat-commons/src/main/kotlin/org/rsmod/api/combat/commons/player/PlayerExtensions.kt b/api/combat/combat-commons/src/main/kotlin/org/rsmod/api/combat/commons/player/PlayerExtensions.kt index a9fdcee63..8373c997e 100644 --- a/api/combat/combat-commons/src/main/kotlin/org/rsmod/api/combat/commons/player/PlayerExtensions.kt +++ b/api/combat/combat-commons/src/main/kotlin/org/rsmod/api/combat/commons/player/PlayerExtensions.kt @@ -4,7 +4,6 @@ import dev.openrune.ServerCacheManager import dev.openrune.rscm.RSCM import dev.openrune.rscm.RSCMType import dev.openrune.types.ItemServerType -import dev.openrune.types.SequenceServerType import kotlin.math.min import org.rsmod.api.config.refs.params import org.rsmod.api.player.hit.modifier.PlayerHitModifier @@ -78,15 +77,13 @@ public fun Player.combatPlayDefendAnim(clientDelay: Int = 0) { anim(defendAnim, delay = clientDelay) } -private fun resolveDefendAnim( - righthand: ItemServerType?, - lefthand: ItemServerType?, -): String { +private fun resolveDefendAnim(righthand: ItemServerType?, lefthand: ItemServerType?): String { val righthandAnim = righthand?.param(params.defend_anim) val lefthandAnim = lefthand?.param(params.defend_anim) return when { - lefthandAnim != null && !lefthandAnim.isType("seq.human_unarmedblock") -> RSCM.getReverseMapping(RSCMType.SEQ,lefthandAnim.id) - righthandAnim != null -> RSCM.getReverseMapping(RSCMType.SEQ,righthandAnim.id) + lefthandAnim != null && !lefthandAnim.isType("seq.human_unarmedblock") -> + RSCM.getReverseMapping(RSCMType.SEQ, lefthandAnim.id) + righthandAnim != null -> RSCM.getReverseMapping(RSCMType.SEQ, righthandAnim.id) else -> "seq.human_unarmedblock" } } diff --git a/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt b/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt index 673f347be..04c761eaf 100644 --- a/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt +++ b/api/combat/combat-manager/src/main/kotlin/org/rsmod/api/combat/manager/PlayerAttackManager.kt @@ -5,7 +5,6 @@ import dev.openrune.rscm.RSCM.asRSCM import dev.openrune.rscm.RSCMType import dev.openrune.types.ItemServerType import dev.openrune.types.SequenceServerType -import dev.openrune.types.StatType import dev.openrune.types.aconverted.SpotanimType import dev.openrune.types.aconverted.SynthType import jakarta.inject.Inject @@ -33,10 +32,10 @@ import org.rsmod.api.config.refs.params import org.rsmod.api.death.PvPPlayerHitHook import org.rsmod.api.npc.hit.isStyleImmuneTo import org.rsmod.api.npc.hit.modifier.NpcHitModifier -import org.rsmod.api.player.hit.modifier.PlayerHitModifier import org.rsmod.api.npc.hit.queueHit -import org.rsmod.api.player.hit.queueHit import org.rsmod.api.player.cheat.adminMaxHit +import org.rsmod.api.player.hit.modifier.PlayerHitModifier +import org.rsmod.api.player.hit.queueHit import org.rsmod.api.player.interact.NpcInteractions import org.rsmod.api.player.interact.NpcTInteractions import org.rsmod.api.player.interact.PlayerInteractions @@ -201,10 +200,12 @@ constructor( val fx = MeleeAnimationAndSound.from(attack.stance) val (animParam, soundParam, defaultAnim, defaultSound) = fx - val attackAnim = weapon?.paramOrNull(animParam) ?: SequenceServerType(defaultAnim.asRSCM(RSCMType.SEQ)) - val attackSound = weapon?.paramOrNull(soundParam) ?: SynthType(defaultSound.asRSCM(RSCMType.SYNTH)) + val attackAnim = + weapon?.paramOrNull(animParam) ?: SequenceServerType(defaultAnim.asRSCM(RSCMType.SEQ)) + val attackSound = + weapon?.paramOrNull(soundParam) ?: SynthType(defaultSound.asRSCM(RSCMType.SYNTH)) - player.anim(RSCM.getReverseMapping(RSCMType.SEQ,attackAnim.id), priority = 6) + player.anim(RSCM.getReverseMapping(RSCMType.SEQ, attackAnim.id), priority = 6) player.soundSynth(attackSound) } @@ -221,7 +222,7 @@ constructor( public fun playWeaponFx(player: Player, attack: CombatAttack.Ranged): Boolean { val weapon = getInvObj(attack.weapon) val attackAnim = weapon.paramOrNull(params.attack_anim_stance1) ?: return false - player.anim(RSCM.getReverseMapping(RSCMType.SEQ,attackAnim.id), priority = 6) + player.anim(RSCM.getReverseMapping(RSCMType.SEQ, attackAnim.id), priority = 6) val attackSound = weapon.paramOrNull(params.attack_sound_stance1) attackSound?.let(player::soundSynth) return true diff --git a/api/combat/combat-scripts/src/main/kotlin/org/rsmod/api/combat/NvPCombat.kt b/api/combat/combat-scripts/src/main/kotlin/org/rsmod/api/combat/NvPCombat.kt index 35d71d860..41184ce7a 100644 --- a/api/combat/combat-scripts/src/main/kotlin/org/rsmod/api/combat/NvPCombat.kt +++ b/api/combat/combat-scripts/src/main/kotlin/org/rsmod/api/combat/NvPCombat.kt @@ -119,9 +119,9 @@ constructor( * Spawns a projectile from the npc to [target], returning the number of cycles until impact. * * The projectile graphic comes from the npc's `proj_travel` (spotanim) param; its flight is - * described by the `proj_type` (projanim) param when present, otherwise [DEFAULT_PROJECTILE_TYPE]. - * When the npc defines no `proj_travel`, no projectile is spawned and the hit lands after - * [DEFAULT_PROJECTILE_HIT_DELAY] cycles. + * described by the `proj_type` (projanim) param when present, otherwise + * [DEFAULT_PROJECTILE_TYPE]. When the npc defines no `proj_travel`, no projectile is spawned + * and the hit lands after [DEFAULT_PROJECTILE_HIT_DELAY] cycles. */ private fun StandardNpcAccess.spawnProjectile(target: Player): Int { val travelSpot = @@ -157,7 +157,10 @@ constructor( private const val MELEE_HIT_DELAY = 1 private const val DEFAULT_PROJECTILE_HIT_DELAY = 2 - /** Standard projectile arc used when an npc provides a `proj_travel` graphic but no `proj_type`. */ + /** + * Standard projectile arc used when an npc provides a `proj_travel` graphic but no + * `proj_type`. + */ private val DEFAULT_PROJECTILE_TYPE = ProjAnimType( startHeight = 43, diff --git a/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/NpcExtensions.kt b/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/NpcExtensions.kt index f7b7c8f98..5ede304c1 100644 --- a/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/NpcExtensions.kt +++ b/api/npc/src/main/kotlin/org/rsmod/api/npc/hit/NpcExtensions.kt @@ -215,9 +215,9 @@ public fun Npc.visHitmark(): HitmarkTypeGroup { if (current != null) { return current } - val lit = RSCM.getReverseMapping(RSCMType.HITMARK,visType.param(params.hitmark_lit).id) - val tint = RSCM.getReverseMapping(RSCMType.HITMARK,visType.param(params.hitmark_tint).id) - val max = RSCM.getReverseMapping(RSCMType.HITMARK,visType.param(params.hitmark_max).id) + val lit = RSCM.getReverseMapping(RSCMType.HITMARK, visType.param(params.hitmark_lit).id) + val tint = RSCM.getReverseMapping(RSCMType.HITMARK, visType.param(params.hitmark_tint).id) + val max = RSCM.getReverseMapping(RSCMType.HITMARK, visType.param(params.hitmark_max).id) val hitmark = HitmarkTypeGroup(lit, tint, max) this.cachedHitmark = hitmark return hitmark diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/DamageOnlyPlayerHitProcessor.kt b/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/DamageOnlyPlayerHitProcessor.kt index 417105f76..d4db2bd16 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/DamageOnlyPlayerHitProcessor.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/DamageOnlyPlayerHitProcessor.kt @@ -2,7 +2,6 @@ package org.rsmod.api.player.hit.processor import jakarta.inject.Inject import kotlin.math.min -import org.rsmod.game.damage.recordDamageOn import org.rsmod.api.player.cheat.adminGodMode import org.rsmod.api.player.death.recordDeathCause import org.rsmod.api.player.death.resolveDeathCause @@ -14,6 +13,7 @@ import org.rsmod.api.player.stat.baseHitpointsLvl import org.rsmod.api.player.stat.hitpoints import org.rsmod.api.player.stat.statSub import org.rsmod.events.EventBus +import org.rsmod.game.damage.recordDamageOn import org.rsmod.game.entity.NpcList import org.rsmod.game.entity.Player import org.rsmod.game.entity.PlayerList diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/StandardPlayerHitProcessor.kt b/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/StandardPlayerHitProcessor.kt index 760ffa864..9ae69829a 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/StandardPlayerHitProcessor.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/hit/processor/StandardPlayerHitProcessor.kt @@ -1,6 +1,5 @@ package org.rsmod.api.player.hit.processor -import dev.openrune.rscm.RSCM import dev.openrune.rscm.RSCM.asRSCM import dev.openrune.rscm.RSCMType import dev.openrune.types.ItemServerType @@ -8,8 +7,6 @@ import dev.openrune.types.aconverted.SynthType import kotlin.math.min import org.rsmod.api.config.constants import org.rsmod.api.config.refs.BaseParams -import org.rsmod.api.player.death.DEATH_CAUSE_ATTR -import org.rsmod.api.player.death.DeathCause import org.rsmod.api.player.death.recordDeathCause import org.rsmod.api.player.death.resolveDeathCause import org.rsmod.api.player.events.PlayerHitEvents @@ -28,14 +25,14 @@ import org.rsmod.game.hit.HitType public object StandardPlayerHitProcessor : QueuedPlayerHitProcessor { private val hitSoundsBodyA = - listOf("synth.human_hit_1", "synth.human_hit_2", "synth.human_hit_3", "synth.human_hit_4").map { + listOf("synth.human_hit_1", "synth.human_hit_2", "synth.human_hit_3", "synth.human_hit_4") + .map { SynthType(it.asRSCM(RSCMType.SYNTH)) } + + private val hitSoundsBodyB = + listOf("synth.female_hit_1", "synth.female_hit_2").map { SynthType(it.asRSCM(RSCMType.SYNTH)) } - private val hitSoundsBodyB = listOf("synth.female_hit_1", "synth.female_hit_2").map { - SynthType(it.asRSCM(RSCMType.SYNTH)) - } - override fun ProtectedAccess.process(hit: Hit) { if (!hit.isValid(this)) { return diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt index 44e9ebeb5..2e2c9013f 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt @@ -9,13 +9,13 @@ import dev.openrune.rscm.RSCM import dev.openrune.rscm.RSCM.asRSCM import dev.openrune.rscm.RSCMType import dev.openrune.types.HitmarkTypeGroup +import dev.openrune.types.InvScope import dev.openrune.types.ItemServerType import dev.openrune.types.MesAnimType import dev.openrune.types.NpcMode import dev.openrune.types.NpcServerType import dev.openrune.types.ObjectServerType import dev.openrune.types.SequenceServerType -import dev.openrune.types.InvScope import dev.openrune.types.WalkTriggerType import dev.openrune.types.aconverted.CategoryType import dev.openrune.types.aconverted.SpotanimType @@ -146,8 +146,8 @@ import org.rsmod.events.EventBus import org.rsmod.events.KeyedEvent import org.rsmod.events.SuspendEvent import org.rsmod.events.UnboundEvent -import org.rsmod.game.entity.Npc import org.rsmod.game.damage.recordDamageOn +import org.rsmod.game.entity.Npc import org.rsmod.game.entity.PathingEntity import org.rsmod.game.entity.Player import org.rsmod.game.entity.npc.NpcUid @@ -382,8 +382,7 @@ public class ProtectedAccess( if (teleportType == TeleportType.Exempt) { return true } - val denial = - context.teleportValidator.validate(player, teleportType, context.areaChecker) + val denial = context.teleportValidator.validate(player, teleportType, context.areaChecker) if (denial == null) { return true } @@ -1008,7 +1007,7 @@ public class ProtectedAccess( coords: CoordGrid = this.coords, inv: Inventory = this.inv, ): Boolean { - return invAddOrDrop(repo, RSCM.getReverseMapping(RSCMType.OBJ,obj.id), count, coords, inv) + return invAddOrDrop(repo, RSCM.getReverseMapping(RSCMType.OBJ, obj.id), count, coords, inv) } /** @@ -1991,7 +1990,7 @@ public class ProtectedAccess( * the coroutine suspension. * @see [resumeWithMainModalProtectedAccess] */ - public suspend fun delayBySeq(anim: String, extra : Int = 0) { + public suspend fun delayBySeq(anim: String, extra: Int = 0) { val cycles = ServerCacheManager.getAnim(anim.asRSCM(RSCMType.SEQ))?.totalDelay ?: (2 + extra) require(cycles > 0) { "`cycles` must be greater than 0. (cycles=$cycles)" } @@ -2323,7 +2322,15 @@ public class ProtectedAccess( pauseText: String, eventBus: EventBus, ) { - player.ifDoubleobjbox(text, obj1.asRSCM(RSCMType.OBJ), zoom1, obj2.asRSCM(RSCMType.OBJ), zoom2, pauseText, eventBus) + player.ifDoubleobjbox( + text, + obj1.asRSCM(RSCMType.OBJ), + zoom1, + obj2.asRSCM(RSCMType.OBJ), + zoom2, + pauseText, + eventBus, + ) val modal = player.ui.getModalOrNull("component.chatbox:chatmodal") val input = coroutine.pause(ResumePauseButtonInput::class) resumePauseButtonWithProtectedAccess(input, modal, "component.objectbox_double:pausebutton") @@ -2347,20 +2354,22 @@ public class ProtectedAccess( * This dialogue is meant to be used in a script sequence where the script itself will replace * or close the dialogue after a certain number of cycles. */ - public fun doubleobjboxNp( - obj1: String, - zoom1: Int, - obj2: String, - zoom2: Int, - text: String, - ) { + public fun doubleobjboxNp(obj1: String, zoom1: Int, obj2: String, zoom2: Int, text: String) { val alignment = context.alignment val pages = alignment.generateChatPageList(text) if (pages.size > 1) { throw IllegalStateException("Text too long: $text") } val page = pages.first() - player.ifDoubleobjbox(page.text, obj1.asRSCM(RSCMType.OBJ), zoom1, obj2.asRSCM(RSCMType.OBJ), zoom2, "", context.eventBus) + player.ifDoubleobjbox( + page.text, + obj1.asRSCM(RSCMType.OBJ), + zoom1, + obj2.asRSCM(RSCMType.OBJ), + zoom2, + "", + context.eventBus, + ) } /** @@ -2677,7 +2686,15 @@ public class ProtectedAccess( npc.playerFace(player, faceFar = faceFar) } player.facePathingEntitySquare(npc) - player.ifChatNpcSpecific(title, RSCM.getReverseMapping(RSCMType.NPC,npc.type.id), text, chatanim, pauseText, lineHeight, eventBus) + player.ifChatNpcSpecific( + title, + RSCM.getReverseMapping(RSCMType.NPC, npc.type.id), + text, + chatanim, + pauseText, + lineHeight, + eventBus, + ) val modal = player.ui.getModalOrNull("component.chatbox:chatmodal") val input = coroutine.pause(ResumePauseButtonInput::class) resumePauseButtonWithProtectedAccess(input, modal, "component.chat_left:continue") @@ -2722,7 +2739,15 @@ public class ProtectedAccess( eventBus: EventBus, ) { player.facePathingEntitySquare(npc) - player.ifChatNpcSpecific(title, RSCM.getReverseMapping(RSCMType.NPC,npc.type.id), text, chatanim, pauseText, lineHeight, eventBus) + player.ifChatNpcSpecific( + title, + RSCM.getReverseMapping(RSCMType.NPC, npc.type.id), + text, + chatanim, + pauseText, + lineHeight, + eventBus, + ) val modal = player.ui.getModalOrNull("component.chatbox:chatmodal") val input = coroutine.pause(ResumePauseButtonInput::class) resumePauseButtonWithProtectedAccess(input, modal, "component.chat_left:continue") @@ -2780,12 +2805,7 @@ public class ProtectedAccess( * This dialogue is meant to be used in a script sequence where the script itself will replace * or close the dialogue after a certain number of cycles. */ - public fun chatNpcSpecificNp( - title: String, - type: String, - mesanim: MesAnimType, - text: String, - ) { + public fun chatNpcSpecificNp(title: String, type: String, mesanim: MesAnimType, text: String) { val alignment = context.alignment val pages = alignment.generateChatPageList(text) if (pages.size > 1) { @@ -3340,13 +3360,15 @@ public class ProtectedAccess( } public fun invContains(inv: Inventory, content: String): Boolean { - return inv.any { it != null && getInvObj(it).contentGroup == content.asRSCM(RSCMType.CONTENT) } + return inv.any { + it != null && getInvObj(it).contentGroup == content.asRSCM(RSCMType.CONTENT) + } } /** * Returns whether [content] (content group) exists in any of the player's inventories — e.g. - * inventory, worn equipment, bank, and any other loaded containers. Shared inventories such - * as shops are excluded. + * inventory, worn equipment, bank, and any other loaded containers. Shared inventories such as + * shops are excluded. */ public fun playerContainsContent(content: String): Boolean { val contentId = content.asRSCM(RSCMType.CONTENT) @@ -3365,7 +3387,9 @@ public class ProtectedAccess( /** @see [playerContainsObj] */ public fun playerContainsAnyObj(vararg objs: String): Boolean = objs.any(::playerContainsObj) - private inline fun playerContainsInInventories(crossinline predicate: (InvObj) -> Boolean): Boolean { + private inline fun playerContainsInInventories( + crossinline predicate: (InvObj) -> Boolean + ): Boolean { for (inventory in player.invMap.values) { if (inventory.type.scope == InvScope.Shared) continue if (inventory.any { slot -> slot != null && predicate(slot) }) { @@ -3448,7 +3472,6 @@ public class ProtectedAccess( repo.spotanimMap(spotanim, coord, height, delay) } - public fun spotanimMap( repo: WorldRepository, spotanim: SpotanimType, @@ -3586,11 +3609,7 @@ public class ProtectedAccess( return obj.isType(type) } - public fun ocIsType( - obj: InvObj?, - type: String, - vararg others: String, - ): Boolean { + public fun ocIsType(obj: InvObj?, type: String, vararg others: String): Boolean { return obj.isType(type) || others.any(obj::isType) } diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContext.kt b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContext.kt index 560232039..0f34a1f94 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContext.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContext.kt @@ -5,13 +5,13 @@ import org.rsmod.api.market.MarketPrices import org.rsmod.api.player.dialogue.align.TextAlignment import org.rsmod.api.player.hit.modifier.PlayerHitModifier import org.rsmod.api.player.hit.processor.InstantPlayerHitProcessor +import org.rsmod.api.player.hook.PlayerTeleportValidator import org.rsmod.api.player.interact.HeldInteractions import org.rsmod.api.player.interact.LocInteractions import org.rsmod.api.player.interact.NpcInteractions import org.rsmod.api.player.interact.PlayerInteractions import org.rsmod.api.player.interact.WornInteractions import org.rsmod.api.player.music.MusicPlayer -import org.rsmod.api.player.hook.PlayerTeleportValidator import org.rsmod.api.random.GameRandom import org.rsmod.events.EventBus import org.rsmod.game.entity.NpcList diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContextFactory.kt b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContextFactory.kt index ede9cf337..fcfcbb815 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContextFactory.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccessContextFactory.kt @@ -6,13 +6,13 @@ import org.rsmod.api.market.MarketPrices import org.rsmod.api.player.dialogue.align.TextAlignment import org.rsmod.api.player.hit.modifier.PlayerHitModifier import org.rsmod.api.player.hit.processor.InstantPlayerHitProcessor +import org.rsmod.api.player.hook.PlayerTeleportValidator import org.rsmod.api.player.interact.HeldInteractions import org.rsmod.api.player.interact.LocInteractions import org.rsmod.api.player.interact.NpcInteractions import org.rsmod.api.player.interact.PlayerInteractions import org.rsmod.api.player.interact.WornInteractions import org.rsmod.api.player.music.MusicPlayer -import org.rsmod.api.player.hook.PlayerTeleportValidator import org.rsmod.api.random.GameRandom import org.rsmod.events.EventBus import org.rsmod.game.entity.NpcList diff --git a/api/script/src/main/kotlin/org/rsmod/api/script/PlayerScriptEventExtensions.kt b/api/script/src/main/kotlin/org/rsmod/api/script/PlayerScriptEventExtensions.kt index 83a0cd83a..5207f7413 100644 --- a/api/script/src/main/kotlin/org/rsmod/api/script/PlayerScriptEventExtensions.kt +++ b/api/script/src/main/kotlin/org/rsmod/api/script/PlayerScriptEventExtensions.kt @@ -4,6 +4,7 @@ import dev.openrune.definition.type.widget.ComponentType import dev.openrune.rscm.RSCM.asRSCM import dev.openrune.rscm.RSCMType import dev.openrune.types.ItemServerType +import dev.or2.central.account.Rights import org.rsmod.api.player.events.PlayerHitEvents import org.rsmod.api.player.events.PlayerHitpointsChangedEvent import org.rsmod.api.player.events.PlayerMovementEvent @@ -13,7 +14,6 @@ import org.rsmod.api.player.events.interact.PlayerTEvents import org.rsmod.api.player.events.interact.PlayerUContentEvents import org.rsmod.api.player.events.interact.PlayerUEvents import org.rsmod.api.player.input.DialogInput -import dev.or2.central.account.Rights import org.rsmod.api.player.protect.ProtectedAccess import org.rsmod.api.player.ui.WorldMapClick import org.rsmod.game.entity.player.SessionStateEvent @@ -25,8 +25,7 @@ public fun ScriptContext.onPlayerInit(action: SessionStateEvent.Initialize.() -> public fun ScriptContext.onPlayerLogin(action: SessionStateEvent.Login.() -> Unit): Unit = onEvent(action) -public fun ScriptContext.onDialogInput(action: DialogInput.() -> Unit): Unit = - onEvent(action) +public fun ScriptContext.onDialogInput(action: DialogInput.() -> Unit): Unit = onEvent(action) public fun ScriptContext.onPlayerLogout(action: SessionStateEvent.Logout.() -> Unit): Unit = onEvent(action) @@ -101,11 +100,13 @@ public fun ScriptContext.onPlayerWalkTrigger( action: PlayerMovementEvent.WalkTrigger.() -> Unit, ): Unit = onEvent(trigger.asRSCM(RSCMType.WALKTRIGGER), action) -public fun ScriptContext.onPlayerCoordsChanged(action: PlayerMovementEvent.CoordsMovedEvent.() -> Unit): Unit = - onEvent(action) +public fun ScriptContext.onPlayerCoordsChanged( + action: PlayerMovementEvent.CoordsMovedEvent.() -> Unit +): Unit = onEvent(action) -public fun ScriptContext.onPlayerHitpointsChanged(action: PlayerHitpointsChangedEvent.() -> Unit): Unit = - onEvent(action) +public fun ScriptContext.onPlayerHitpointsChanged( + action: PlayerHitpointsChangedEvent.() -> Unit +): Unit = onEvent(action) public fun ScriptContext.onPlayerHit(action: PlayerHitEvents.Impact.() -> Unit): Unit = onEvent(action) @@ -113,9 +114,10 @@ public fun ScriptContext.onPlayerHit(action: PlayerHitEvents.Impact.() -> Unit): public fun ScriptContext.onWorldMapClick( requiredRights: Rights, action: suspend ProtectedAccess.(WorldMapClick) -> Unit, -): Unit = onProtectedEvent(WorldMapClick.BUS_ID) { event: WorldMapClick -> - if (!player.modLevel.isAtLeast(requiredRights)) { - return@onProtectedEvent +): Unit = + onProtectedEvent(WorldMapClick.BUS_ID) { event: WorldMapClick -> + if (!player.modLevel.isAtLeast(requiredRights)) { + return@onProtectedEvent + } + action(event) } - action(event) -} diff --git a/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt b/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt index 7c727df6a..c84efbb35 100644 --- a/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt +++ b/content/bosses/demonic-gorilla/src/main/kotlin/org/rsmod/content/bosses/demonicgorilla/DemonicGorilla.kt @@ -113,7 +113,9 @@ class DemonicGorilla @Inject constructor(private val deps: BossDeps, private val override fun ScriptContext.startup() { BossCombat.register(this, spec, deps, onModifyHit = { onModifyProtectionHit(this) }) - deps.extensionRegistry.register(BOULDER_HANDLER) { _, npc, target, _ -> throwBoulder(npc, target) } + deps.extensionRegistry.register(BOULDER_HANDLER) { _, npc, target, _ -> + throwBoulder(npc, target) + } val bossIds = spec.npcTypes.mapNotNullTo(mutableSetOf()) { it.npcTypeId() } onEvent { if (npc.type.id in bossIds) resetGorilla(npc) }