Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.rsmod.game.entity.Player

public object DragonfireProtection {

public enum class DragonfireType { Chromatic, Metal, WyvernIce }
public enum class DragonfireType { Chromatic, Metal, WyvernIce, Vorkath }

private val antifireShields = setOf(
"obj.antidragonbreathshield",
Expand All @@ -28,6 +28,9 @@ public object DragonfireProtection {
)

public fun resolveMaxHit(player: Player, type: DragonfireType, baseMax: Int): Int {
if (type == DragonfireType.Vorkath) {
return resolveVorkathMaxHit(player)
}
if (type == DragonfireType.WyvernIce) {
return when {
wyvernIceShields.any { it in player.worn } -> 10
Expand Down Expand Up @@ -56,6 +59,60 @@ public object DragonfireProtection {
private fun hasAntifireShield(player: Player): Boolean =
antifireShields.any { it in player.worn }

private fun resolveVorkathMaxHit(player: Player): Int =
resolveVorkathMaxHit(
shield = hasAntifireShield(player),
protect = isProtectingFromMagic(player),
antifire = hasAntifire(player),
superAntifire = hasSuperAntifire(player),
)

public fun resolveVorkathResistedMaxHit(player: Player): Int =
resolveVorkathResistedMaxHit(
shield = hasAntifireShield(player),
protect = isProtectingFromMagic(player),
antifire = hasAntifire(player),
superAntifire = hasSuperAntifire(player),
)

/** Pure post-quest Vorkath dragonfire protection table. */
public fun resolveVorkathMaxHit(
shield: Boolean,
protect: Boolean,
antifire: Boolean,
superAntifire: Boolean,
): Int =
when {
superAntifire && shield -> 0
superAntifire && protect -> 10
superAntifire -> 60
antifire && shield -> 10
antifire && protect -> 20
antifire -> 70
shield -> 20
protect -> 30
else -> 80
}

/** Vorkath still deals reduced dragonfire damage when its accuracy roll fails. */
public fun resolveVorkathResistedMaxHit(
shield: Boolean,
protect: Boolean,
antifire: Boolean,
superAntifire: Boolean,
): Int =
when {
superAntifire && shield -> 0
superAntifire && protect -> 10
superAntifire -> 30
antifire && shield -> 10
antifire && protect -> 20
antifire -> 40
shield -> 20
protect -> 30
else -> 50
}

private fun hasSuperAntifire(player: Player): Boolean =
player.vars["varbit.super_antifire_potion"] > 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.rsmod.api.death.NpcDeathDropHook
import org.rsmod.api.death.NpcDeathKillHook
import org.rsmod.api.death.PlayerDeathCleanupHook
import org.rsmod.api.death.PlayerDeathHook
import org.rsmod.api.death.PlayerDeathStorageHook
import org.rsmod.api.death.PvPAttackValidateHook
import org.rsmod.api.death.PvPPlayerHitHook
import org.rsmod.api.death.PvPSkullHook
Expand All @@ -16,6 +17,7 @@ public class DeathDropHooksModule : PluginModule() {
newSetBinding<NpcDeathKillHook>()
newSetBinding<PlayerDeathCleanupHook>()
newSetBinding<PlayerDeathHook>()
newSetBinding<PlayerDeathStorageHook>()
newSetBinding<PvPAttackValidateHook>()
newSetBinding<PvPSkullHook>()
newSetBinding<PvPPlayerHitHook>()
Expand Down
26 changes: 21 additions & 5 deletions api/death/src/main/kotlin/org/rsmod/api/death/PlayerDeath.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package org.rsmod.api.death

import dev.or2.central.account.Rights
import dev.openrune.ServerCacheManager
import dev.openrune.rscm.RSCM
import dev.openrune.rscm.RSCMType
import dev.or2.central.account.Rights
import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.rsmod.api.area.checker.AreaChecker
import org.rsmod.api.area.checker.isInWildernessBasic
import org.rsmod.api.mechanics.toxins.Toxin.cureAllToxins
import org.rsmod.api.player.death.DEATH_CAUSE_ATTR
import org.rsmod.api.player.death.DeathCause
import org.rsmod.api.player.hasProtectItemPrayer
import org.rsmod.api.player.hook.TeleportType
import org.rsmod.api.mechanics.toxins.Toxin.cureAllToxins
import org.rsmod.api.player.deathResetTimers
import org.rsmod.api.player.disablePrayers
import org.rsmod.api.player.hasProtectItemPrayer
import org.rsmod.api.player.hook.TeleportType
import org.rsmod.api.player.protect.ProtectedAccess
import org.rsmod.api.player.vars.boolVarBit
import org.rsmod.api.player.vars.intVarp
Expand All @@ -29,6 +29,7 @@ constructor(
private val mapClock: MapClock,
private val drops: PlayerDeathDrops,
private val handlingResolver: PlayerDeathHandlingResolver,
private val storageHooks: Set<PlayerDeathStorageHook>,
private val cleanupHooks: Set<PlayerDeathCleanupHook>,
private val areaChecker: AreaChecker,
) {
Expand Down Expand Up @@ -92,7 +93,22 @@ constructor(
val handling = handlingResolver.resolve(context)

val result = drops.selectDrops(player, context, handling)
drops.applyDrops(player, result, handling, deathCoords)
var stored = false
for (hook in storageHooks) {
if (hook.store(context, result)) stored = true
}
val appliedResult =
if (stored) {
result.copy(
supplyPile = emptyList(),
lostTradeable = emptyList(),
lostUntradeable = emptyList(),
coinsForKiller = 0L,
)
} else {
result
}
drops.applyDrops(player, appliedResult, handling, deathCoords)
drops.spawnRemains(deathCoords, handling)

player.attr.remove(DEATH_KILLER_ATTR)
Expand Down
13 changes: 13 additions & 0 deletions api/death/src/main/kotlin/org/rsmod/api/death/PlayerDeathHook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,16 @@ public const val RECENT_PVP_HIT_TICKS: Int = 600
public interface PlayerDeathHook {
public fun handleDeath(context: PlayerDeathContext): PlayerDeathHandling?
}

/**
* Stores the items a player would otherwise lose on death.
*
* Returning true claims the lost portion of [result], preventing it from being placed on the
* ground. Kept items are still returned to the player's inventory by the standard death flow.
*/
public interface PlayerDeathStorageHook {
public fun store(
context: PlayerDeathContext,
result: PlayerDeathDrops.DeathDropResult,
): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import org.rsmod.api.instances.events.InstancePlayerLeaveUnboundEvent
import org.rsmod.api.instances.events.InstanceStartedEvent
import org.rsmod.api.instances.events.InstanceTimeTickEvent
import org.rsmod.api.instances.region.InstanceAreaResolver
import org.rsmod.api.instances.region.InstancePlacement
import org.rsmod.api.instances.region.OsrsInstancing
import org.rsmod.api.instances.region.enterCoord
import org.rsmod.api.instances.region.localCoord
Expand All @@ -27,17 +26,14 @@ import org.rsmod.api.player.output.ChatType
import org.rsmod.api.player.output.mes
import org.rsmod.api.repo.npc.NpcRepository
import org.rsmod.api.repo.region.RegionRepository
import org.rsmod.api.table.InstanceSettingsRow
import org.rsmod.events.EventBus
import org.rsmod.events.KeyedEvent
import org.rsmod.game.MapClock
import org.rsmod.game.damage.DamageContributions
import org.rsmod.game.entity.Npc
import org.rsmod.game.entity.PathingEntity
import org.rsmod.game.entity.Player
import org.rsmod.game.entity.PlayerList
import org.rsmod.game.entity.npc.NpcUid
import org.rsmod.game.entity.util.PathingEntityCommon
import org.rsmod.game.region.Region
import org.rsmod.map.CoordGrid
import org.rsmod.routefinder.collision.CollisionFlagMap
Expand Down Expand Up @@ -218,6 +214,11 @@ constructor(
public fun sessionForRegion(regionId: Int): InstanceSession? =
regionToInstance[regionId]?.let(sessions::get)

public fun localCoord(session: InstanceSession, local: RegionLocal): CoordGrid? {
val region = regions[session.id] ?: return null
return session.localCoord(region, local)
}

public fun contributionsFor(id: InstanceId): DamageContributions? =
sessionForId(id)?.damageContributions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package org.rsmod.api.repo.world
import dev.openrune.rscm.RSCM.asRSCM
import dev.openrune.rscm.RSCMType
import dev.openrune.types.ProjAnimType
import dev.openrune.types.SequenceServerType
import dev.openrune.types.aconverted.SpotanimType
import dev.openrune.types.aconverted.SynthType
import jakarta.inject.Inject
Expand Down Expand Up @@ -36,6 +35,17 @@ public class WorldRepository @Inject constructor(private val zoneUpdates: ZoneUp
zoneUpdates.soundArea(source, synth.asRSCM(RSCMType.SYNTH), delay, loops, radius, size)
}

public fun soundArea(
source: CoordGrid,
synth: SynthType,
delay: Int = 0,
loops: Int = 1,
radius: Int = 5,
size: Int = 0,
) {
zoneUpdates.soundArea(source, synth.id, delay, loops, radius, size)
}

public fun soundArea(
source: PathingEntity,
synth: String,
Expand Down
37 changes: 37 additions & 0 deletions content/bosses/vorkath/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
plugins { id("base-conventions") }

dependencies {
implementation(libs.guice)
implementation(libs.fastutil)
implementation(libs.rsprot.api)
implementation(projects.api.attr)
implementation(projects.api.bossHpBarPlugin)
implementation(projects.api.registry)
implementation(projects.engine.events)
implementation(projects.api.combat.combatCommons)
implementation(projects.api.combat.combatScripts)
implementation(projects.api.combat.combatFormulas)
implementation(projects.api.death)
implementation(projects.api.instances)
implementation(projects.api.mechanics.toxins)
implementation(projects.api.npc)
implementation(projects.api.player)
implementation(projects.api.playerOutput)
implementation(projects.api.pluginCommons)
implementation(projects.api.random)
implementation(projects.api.repo)
implementation(projects.api.route)
implementation(projects.api.script)
implementation(projects.api.spells)
implementation(projects.api.utils.utilsLogging)
implementation(projects.api.utils.utilsVars)
implementation(projects.engine.game)
implementation(projects.engine.map)
implementation(projects.engine.plugin)
implementation(projects.engine.routefinder)
testImplementation(kotlin("test"))
testImplementation("org.mockito:mockito-core:5.18.0")
}


tasks.test { workingDir = rootProject.projectDir }
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.rsmod.content.bosses.vorkath

import jakarta.inject.Inject
import jakarta.inject.Singleton
import org.rsmod.game.entity.Player

/**
* Single replacement point for a future Dragon Slayer II quest check. OpenRune does not currently
* expose a complete Dragon Slayer II quest state, so the production default remains accessible.
*/
@Singleton
internal class VorkathAccessPolicy @Inject constructor() {
fun canAccess(@Suppress("UNUSED_PARAMETER") player: Player): Boolean = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.rsmod.content.bosses.vorkath

import org.rsmod.map.CoordGrid

/**
* Capture-derived layout: all 200 casts use 48 three-by-three cells, eight edge segments, then
* the player tile if it was not already selected (56 or 57 distinct pools).
*
* Grid/ranges/order and boss exclusion are observed. Uniform choice among each cell's walkable
* candidates is inferred; decoded packets cannot establish the server's random-number routine.
*/
internal object VorkathAcidLayout {
private const val BOSS_SIZE = 7

fun select(
boss: CoordGrid,
player: CoordGrid,
isWalkable: (CoordGrid) -> Boolean,
choose: (List<CoordGrid>) -> CoordGrid,
): Set<CoordGrid> {
val selected = linkedSetOf<CoordGrid>()
fun available(tile: CoordGrid): Boolean =
!occupiesBoss(boss, tile) && isWalkable(tile)

fun sample(x: IntRange, z: IntRange) {
val candidates = buildList {
for (offsetZ in z) {
for (offsetX in x) {
val tile = boss.translate(offsetX, offsetZ)
if (available(tile)) add(tile)
}
}
}
// A changed collision map must not create unreachable pools or an endless reroll.
if (candidates.isEmpty()) return
val tile = choose(candidates)
require(tile in candidates) { "Acid selection returned a tile outside its capture cell" }
selected += tile
}

// Canonical anchor (2269,4062): grid origin (2262,4055), outer bounds (2282,4075).
for (row in 0..6) {
for (column in 0..6) {
if (row == 3 && column == 3) continue
val x = -7 + column * 3
val z = -7 + row * 3
sample(x..x + 2, z..z + 2)
}
}

// South, north, west, east; exactly this packet order in every supplied cast.
sample(-3..-1, -8..-8)
sample(7..9, -8..-8)
sample(-3..-1, 14..14)
sample(7..9, 14..14)
sample(-8..-8, -3..-1)
sample(-8..-8, 7..9)
sample(14..14, -3..-1)
sample(14..14, 7..9)

if (available(player)) selected += player
return selected
}

/** Seven centre tiles of the southern edge are untouched except for the forced player pool. */
fun exitLane(boss: CoordGrid): Set<CoordGrid> =
(0 until BOSS_SIZE).mapTo(linkedSetOf()) { boss.translate(it, -8) }

private fun occupiesBoss(boss: CoordGrid, tile: CoordGrid): Boolean =
tile.x in boss.x until boss.x + BOSS_SIZE &&
tile.z in boss.z until boss.z + BOSS_SIZE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.rsmod.content.bosses.vorkath

import org.rsmod.map.CoordGrid

internal data class PendingAcidPool(val impactCycle: Int, val tile: CoordGrid)
Loading