Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .data/gamevals/varn.rscm
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ gorilla_protect_damage=9
gorilla_protect_hits=10
gorilla_miss_streak=11
gorilla_protect_switching=12
td_shield_up=13
td_overhead_style=14
guaranteed_hit=15
44 changes: 44 additions & 0 deletions .data/raw-cache/server/npcs.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4544,3 +4544,47 @@ contentGroup = "content.dark_crab_fishing_spot"
moveRestrict = "NoMove"
wanderRange = 0

[[npc]]
id = "npc.tormented_demon_1"
inherit = "npc.tormented_demon_1"
huntMode = 6
huntRange = 30
maxRange = 30
defaultMode = "Wander"
[npc.params]
"param.tormented_demon"=1
"param.attackrate"=4
"param.attack_anim"="seq.luc2_undead_demon_melee"
"param.attack_anim_stance1"="seq.luc2_undead_demon_melee"
"param.attack_anim_stance2"="seq.luc2_undead_demon_firey_balls"
"param.attack_anim_stance3"="seq.luc2_undead_demon_spare_ribs"
"param.defend_anim"="seq.luc2_undead_demon_defend"
"param.death_anim"="seq.luc2_undead_demon_death"
"param.bas_readyanim"="seq.luc2_undead_demon_ready"
"param.attack_sound_stance1"=9219
"param.attack_sound_stance2"=9229
"param.attack_sound_stance3"=9253
"param.npc_attack_type"="category.attacktype_crush"

[[npc]]
id = "npc.tormented_demon_2"
inherit = "npc.tormented_demon_2"
huntMode = 6
huntRange = 30
maxRange = 30
defaultMode = "Wander"
[npc.params]
"param.tormented_demon"=1
"param.attackrate"=4
"param.attack_anim"="seq.luc2_undead_demon_melee"
"param.attack_anim_stance1"="seq.luc2_undead_demon_melee"
"param.attack_anim_stance2"="seq.luc2_undead_demon_firey_balls"
"param.attack_anim_stance3"="seq.luc2_undead_demon_spare_ribs"
"param.defend_anim"="seq.luc2_undead_demon_defend"
"param.death_anim"="seq.luc2_undead_demon_death"
"param.bas_readyanim"="seq.luc2_undead_demon_ready"
"param.attack_sound_stance1"=9219
"param.attack_sound_stance2"=9229
"param.attack_sound_stance3"=9253
"param.npc_attack_type"="category.attacktype_crush"

9 changes: 9 additions & 0 deletions .data/raw-cache/server/varn.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,12 @@ id = "varn.gorilla_miss_streak"

[[varn]]
id = "varn.gorilla_protect_switching"

[[varn]]
id = "varn.td_shield_up"

[[varn]]
id = "varn.td_overhead_style"

[[varn]]
id = "varn.guaranteed_hit"
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class BossSpecBuilder(private val npcTypes: List<String>) {
exitAfter: Int? = null,
nextPhase: String? = null,
idleAnim: String? = null,
attackRate: Int? = null,
block: PhaseBuilder.() -> Unit,
): PhaseRef {
val builder = PhaseBuilder(name).apply(block)
Expand All @@ -59,6 +60,7 @@ class BossSpecBuilder(private val npcTypes: List<String>) {
exitAfter = exitAfter,
nextPhase = nextPhase,
idleAnim = idleAnim,
attackRate = attackRate ?: builder.attackRate,
entry = builder.entry,
exit = builder.exit,
selector = builder.selector,
Expand Down Expand Up @@ -358,6 +360,8 @@ class AbilityBuilder {
class PhaseBuilder(private val name: String) {
var entry: String? = null
var exit: String? = null

var attackRate: Int? = null
var selector: Selector = Selector.WeightedRandom()
internal val forceAbilities = mutableListOf<ForcedAbility>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import dev.openrune.rscm.RSCMType
import org.rsmod.api.bosses.spec.BossSpec
import org.rsmod.api.bosses.validation.SpecValidator
import org.rsmod.api.npc.access.StandardNpcAccess
import org.rsmod.api.npc.events.NpcHitEvents
import org.rsmod.api.script.onAiApPlayer2
import org.rsmod.api.script.onAiOpPlayer2
import org.rsmod.api.script.onEvent
import org.rsmod.api.script.onModifyNpcHit
import org.rsmod.api.npc.events.NpcHitEvents
import org.rsmod.game.entity.Npc
import org.rsmod.game.entity.Player
import org.rsmod.game.entity.npc.NpcStateEvents
Expand Down Expand Up @@ -97,12 +97,19 @@ object BossCombat {
val encounter = deps.encounterRegistry.of(npc)
if (encounter.currentPhase == null) return
val tick = deps.mapClock.cycle
if (encounter.phaseEnteredTick < 0) {
encounter.phaseEnteredTick = tick
}

checkAutoTransitions(this, target, encounter, tick, spec, deps)
checkTriggers(this, target, spec, deps, encounter)

val ticksSinceLastAttack = tick - encounter.lastAbilityTick
if (ticksSinceLastAttack < spec.stats.attackRate) return
val attackRate =
encounter.attackRateOverride
?: encounter.currentPhase?.attackRate
?: spec.stats.attackRate
if (ticksSinceLastAttack < attackRate) return

val phase = encounter.currentPhase ?: return
val abilityName = encounter.selectAbility(phase.selector, tick, target) ?: return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
package org.rsmod.api.bosses.runtime

import kotlin.random.Random
import org.rsmod.api.bosses.spec.*
import org.rsmod.game.entity.Npc
import org.rsmod.game.entity.Player
import kotlin.random.Random

class BossEncounter(
val npc: Npc,
val spec: BossSpec,
) {
var currentPhaseName: String = spec.phases.keys.firstOrNull() ?: ""
var phaseEnteredTick: Int = 0

var phaseEnteredTick: Int = -1
var lastAbilityTick: Int = 0
var lastAbilityName: String? = null
var invulnerable: Boolean = false
var damageScale: Double = 1.0
var lethalHandled: Boolean = false

/**
* Per-encounter attack-rate override (ticks between ability uses). Takes precedence over
* [PhaseSpec.attackRate] and [BossStats.attackRate]. Null by default and recreated with the
* encounter on respawn, so bosses that never set it are unaffected.
*/
var attackRateOverride: Int? = null

internal val firedTriggers = mutableSetOf<Int>()
internal val firedPhaseEntries = mutableSetOf<String>()
private val cooldowns = mutableMapOf<String, Int>()
private val forcedTickLastFired = mutableMapOf<String, Int>()
private var rotationCursor = 0
private var basicAttackCount = 0
private var forceAttackThreshold = -1
Expand All @@ -37,6 +46,7 @@ class BossEncounter(
phaseEnteredTick = tick
rotationCursor = 0
cooldowns.clear()
forcedTickLastFired.clear()
basicAttackCount = 0
forceAttackThreshold = -1

Expand All @@ -55,8 +65,9 @@ class BossEncounter(

for (forced in phase.forceAbilities) {
if (forced.attackMin != null) continue
val elapsed = tick - phaseEnteredTick
if (elapsed > 0 && elapsed % forced.period == 0) {
val lastFired = forcedTickLastFired[forced.ability] ?: phaseEnteredTick
if (tick - lastFired >= forced.period) {
forcedTickLastFired[forced.ability] = tick
return forced.ability
}
}
Expand Down
16 changes: 14 additions & 2 deletions api/bosses/src/main/kotlin/org/rsmod/api/bosses/runtime/BossFx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ fun BossDeps.bossProjectile(
delay: Int,
travel: Int,
curve: Int,
progress: Int = 0,
): ProjAnim {
val proj =
ProjAnim(
Expand All @@ -26,7 +27,7 @@ fun BossDeps.bossProjectile(
startTime = delay,
endTime = delay + travel,
angle = curve,
progress = 0,
progress = progress,
sourceIndex = 0,
targetIndex = 0,
startCoord = src,
Expand Down Expand Up @@ -72,9 +73,20 @@ fun BossDeps.lob(
landTicks: Int,
landGfx: Int,
landGfxHeight: Int = 0,
progress: Int = 0,
onLand: (Player) -> Unit,
) {
bossProjectile(spotanim, npc.coords.translate(2, 2), targetTile, startHeight, endHeight, delay, travel, curve)
bossProjectile(
spotanim,
npc.coords.translate(2, 2),
targetTile,
startHeight,
endHeight,
delay,
travel,
curve,
progress,
)
worldQueues.add(landTicks) {
worldRepo.spotanimMap(SpotanimType(landGfx), targetTile, landGfxHeight)
val player = targetUid.resolve(playerList) ?: return@add
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package org.rsmod.api.bosses.runtime
import dev.openrune.ServerCacheManager
import dev.openrune.rscm.RSCM.asRSCM
import dev.openrune.rscm.RSCMType
import dev.openrune.types.NpcMode
import dev.openrune.types.ProjAnimType
import dev.openrune.types.aconverted.SpotanimType
import org.rsmod.api.bosses.spec.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ data class PhaseSpec(
val entryHp: Double? = null,
val transmog: String? = null,
val lockMovement: Boolean = false,
/**
* Per-phase attack-rate default (ticks between ability uses). When set, it overrides
* [BossStats.attackRate] while this phase is active. A per-encounter
* [org.rsmod.api.bosses.runtime.BossEncounter.attackRateOverride] takes precedence over both.
*/
val attackRate: Int? = null,
val exitAfter: Int? = null,
val nextPhase: String? = null,
val idleAnim: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ sealed interface Selector {
val entries: List<WeightedRef> = emptyList(),
val noRepeatBias: Double = 0.5,
) : Selector {
constructor(vararg entries: WeightedRef, noRepeatBias: Double = 0.5)
: this(entries.toList(), noRepeatBias)
constructor(vararg entries: WeightedRef, noRepeatBias: Double = 0.5) :
this(entries.toList(), noRepeatBias)
}

data class Rotation(val sequence: List<String>) : Selector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,20 @@ public object AccuracyOperations {
* Calculates the hit chance based on the attacker's [attackRoll] and the target's
* [defenceRoll].
*
* @param npcAttributes When it contains [NpcAttr.GuaranteedHit] (e.g. a target that's currently
* "defenceless"), the roll is skipped entirely and this returns a guaranteed `100%`. Only
* meaningful for player-vs-npc accuracy; PvP/NvN/NvP callers can omit it.
* @return An integer between `0` and `10,000`, where `0` represents a `0%` hit chance, `1`
* represents a `0.01%` hit chance, and `10,000` represents a `100%` hit chance.
*/
public fun calculateHitChance(attackRoll: Int, defenceRoll: Int): Int {
public fun calculateHitChance(
attackRoll: Int,
defenceRoll: Int,
npcAttributes: EnumSet<CombatNpcAttributes> = EnumSet.noneOf(CombatNpcAttributes::class.java),
): Int {
if (NpcAttr.GuaranteedHit in npcAttributes) {
return HIT_CHANCE_SCALE
}
return calculateHitRoll(attackRoll, defenceRoll)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ constructor(
return computeSpellHitChance(
source = player,
target = targetType,
npc = target,
spell = spell,
targetDefence = target.defenceLvl,
targetCurrHp = target.hitpoints,
Expand All @@ -64,12 +65,13 @@ constructor(
targetWeaknessPercent: Int,
spellbook: Spellbook?,
usedSunfireRune: Boolean,
npc: Npc? = null,
): Int {
val spellAttributes =
magicAttributes.spellCollect(source, spell, spellbook, usedSunfireRune, random)

val slayerTask = target.isSlayerTask(source)
val npcAttributes = npcAttributes.collect(target, targetCurrHp, targetMaxHp, slayerTask)
val npcAttributes = npcAttributes.collect(target, npc, targetCurrHp, targetMaxHp, slayerTask)

val attackRoll =
computeSpellAttackRoll(
Expand All @@ -90,7 +92,7 @@ constructor(
)
val defenceRoll = modifySpellDefenceRoll(baseDefenceRoll, spellAttributes)

return AccuracyOperations.calculateHitChance(attackRoll, defenceRoll)
return AccuracyOperations.calculateHitChance(attackRoll, defenceRoll, npcAttributes)
}

public fun computeSpellAttackRoll(
Expand Down Expand Up @@ -119,6 +121,7 @@ constructor(
computeStaffHitChance(
source = player,
target = target.visType,
npc = target,
targetDefence = target.defenceLvl,
targetCurrHp = target.hitpoints,
targetMaxHp = target.baseHitpointsLvl,
Expand All @@ -136,11 +139,12 @@ constructor(
targetMagic: Int,
attackStyle: MagicAttackStyle?,
specialMultiplier: Double,
npc: Npc? = null,
): Int {
val staffAttributes = magicAttributes.staffCollect(source, random)

val slayerTask = target.isSlayerTask(source)
val npcAttributes = npcAttributes.collect(target, targetCurrHp, targetMaxHp, slayerTask)
val npcAttributes = npcAttributes.collect(target, npc, targetCurrHp, targetMaxHp, slayerTask)

val baseAttackRoll =
computeStaffAttackRoll(source, attackStyle, staffAttributes, npcAttributes)
Expand All @@ -157,7 +161,7 @@ constructor(
)
val defenceRoll = modifyStaffDefenceRoll(baseDefenceRoll, staffAttributes)

return AccuracyOperations.calculateHitChance(attackRoll, defenceRoll)
return AccuracyOperations.calculateHitChance(attackRoll, defenceRoll, npcAttributes)
}

public fun computeStaffAttackRoll(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ constructor(
computeHitChance(
source = player,
target = target.visType,
npc = target,
targetDefence = target.defenceLvl,
targetCurrHp = target.hitpoints,
targetMaxHp = target.baseHitpointsLvl,
Expand All @@ -55,11 +56,12 @@ constructor(
attackStyle: MeleeAttackStyle?,
blockType: MeleeAttackType?,
specialMultiplier: Double,
npc: Npc? = null,
): Int {
val meleeAttributes = meleeAttributes.collect(source, attackType)

val slayerTask = target.isSlayerTask(source)
val npcAttributes = npcAttributes.collect(target, targetCurrHp, targetMaxHp, slayerTask)
val npcAttributes = npcAttributes.collect(target, npc, targetCurrHp, targetMaxHp, slayerTask)

val baseAttackRoll =
computeAttackRoll(source, attackType, attackStyle, meleeAttributes, npcAttributes)
Expand All @@ -75,7 +77,7 @@ constructor(
npcAttributes = npcAttributes,
)

val hitChance = AccuracyOperations.calculateHitChance(attackRoll, defenceRoll)
val hitChance = AccuracyOperations.calculateHitChance(attackRoll, defenceRoll, npcAttributes)
return MeleeAccuracyOperations.modifyHitChance(
hitChance = hitChance,
attackRoll = attackRoll,
Expand Down
Loading