Add tests for api plugin

This commit is contained in:
Major-
2018-08-25 01:14:56 +01:00
parent fd52ee6026
commit f4aa3aae4e
13 changed files with 422 additions and 81 deletions
@@ -5,27 +5,73 @@ import org.apollo.cache.def.NpcDefinition
import org.apollo.cache.def.ObjectDefinition
import java.lang.IllegalArgumentException
/**
* Provides plugins with access to item, npc, and object definitions
*/
object Definitions {
fun item(id: Int): ItemDefinition? {
/**
* Returns the [ItemDefinition] with the specified [id]. Callers of this function must perform bounds checking on
* the [id] prior to invoking this method (i.e. verify that `id >= 0 && id < ItemDefinition.count()`).
*
* @throws IndexOutOfBoundsException If the id is out of bounds.
*/
fun item(id: Int): ItemDefinition {
return ItemDefinition.lookup(id)
}
/**
* Returns the [ItemDefinition] with the specified name, performing case-insensitive matching. If multiple items
* share the same name, the item with the lowest id is returned.
*
* The name may be suffixed with an explicit item id (as a way to disambiguate in the above case), by ending the
* name with `_id`, e.g. `monks_robe_42`. If an explicit id is attached, it must be bounds checked (in the same
* manner as [item(id: Int)][item]).
*/
fun item(name: String): ItemDefinition? {
return findEntity(ItemDefinition::getDefinitions, ItemDefinition::getName, name)
}
fun obj(id: Int): ObjectDefinition? {
/**
* Returns the [ObjectDefinition] with the specified [id]. Callers of this function must perform bounds checking on
* the [id] prior to invoking this method (i.e. verify that `id >= 0 && id < ObjectDefinition.count()`).
*
* @throws IndexOutOfBoundsException If the id is out of bounds.
*/
fun obj(id: Int): ObjectDefinition {
return ObjectDefinition.lookup(id)
}
/**
* Returns the [ObjectDefinition] with the specified name, performing case-insensitive matching. If multiple objects
* share the same name, the object with the lowest id is returned.
*
* The name may be suffixed with an explicit object id (as a way to disambiguate in the above case), by ending the
* name with `_id`, e.g. `man_2`. If an explicit id is attached, it must be bounds checked (in the same
* manner as [object(id: Int)][object]).
*/
fun obj(name: String): ObjectDefinition? {
return findEntity(ObjectDefinition::getDefinitions, ObjectDefinition::getName, name)
}
fun npc(id: Int): NpcDefinition? {
/**
* Returns the [NpcDefinition] with the specified [id]. Callers of this function must perform bounds checking on
* the [id] prior to invoking this method (i.e. verify that `id >= 0 && id < NpcDefinition.count()`).
*
* @throws IndexOutOfBoundsException If the id is out of bounds.
*/
fun npc(id: Int): NpcDefinition {
return NpcDefinition.lookup(id)
}
/**
* Returns the [NpcDefinition] with the specified name, performing case-insensitive matching. If multiple npcs
* share the same name, the npc with the lowest id is returned.
*
* The name may be suffixed with an explicit npc id (as a way to disambiguate in the above case), by ending the
* name with `_id`, e.g. `man_2`. If an explicit id is attached, it must be bounds checked (in the same
* manner as [npc(id: Int)][npc]).
*/
fun npc(name: String): NpcDefinition? {
return findEntity(NpcDefinition::getDefinitions, NpcDefinition::getName, name)
}
@@ -53,26 +53,37 @@ class SkillProxy(private val skills: SkillSet, private val skill: Int) {
}
/**
* Boosts the current level of this skill by [amount], if possible (i.e. if `current + amount <= maximum + amount`).
* Boosts the current level of this skill by [amount], if possible.
*/
fun boost(amount: Int) {
val new = Math.min(current + amount, maximum + amount)
require(amount >= 1) { "Can only boost skills by positive values." }
val new = if (current - maximum > amount) {
current
} else {
Math.min(current + amount, maximum + amount)
}
skills.setCurrentLevel(skill, new)
}
/**
* Drains the current level of this skill by [amount], if possible (i.e. if `current - amount >= 0`).
* Drains the current level of this skill by [amount], if possible.
*/
fun drain(amount: Int) {
require(amount >= 1) { "Can only drain skills by positive values." }
val new = Math.max(current - amount, 0)
skills.setCurrentLevel(skill, new)
}
/**
* Restores the current level of this skill by [amount], if possible (i.e. if `current + amount < maximum`).
* Restores the current level of this skill by [amount], if possible.
*/
fun restore(amount: Int) {
val new = Math.max(current + amount, maximum)
require(amount >= 1) { "Can only restore skills by positive values." }
val new = Math.min(current + amount, maximum)
skills.setCurrentLevel(skill, new)
}
@@ -0,0 +1,104 @@
package org.apollo.game.plugin.api
import org.apollo.game.model.Position
import org.apollo.game.model.World
import org.apollo.game.model.area.Region
import org.apollo.game.model.entity.Entity
import org.apollo.game.model.entity.EntityType
import org.apollo.game.model.entity.EntityType.DYNAMIC_OBJECT
import org.apollo.game.model.entity.EntityType.STATIC_OBJECT
import org.apollo.game.model.entity.obj.DynamicGameObject
import org.apollo.game.model.entity.obj.GameObject
import org.apollo.game.scheduling.ScheduledTask
/**
* Finds all of the [Entities][Entity] with the specified [EntityTypes][EntityType] at the specified [position], that
* match the provided [predicate].
*
* ```
* const val GOLD_COINS = 995
* ...
*
* val allCoins: Sequence<GroundItem> = region.find(position, EntityType.GROUND_ITEM) { item -> item.id == GOLD_COINS }
* ```
*/
fun <T : Entity> Region.find(position: Position, vararg types: EntityType, predicate: (T) -> Boolean): Sequence<T> {
return getEntities<T>(position, *types).asSequence().filter(predicate)
}
/**
* Finds the first [GameObject]s with the specified [id] at the specified [position].
*
* Note that the iteration order of entities in a [Region] is not defined - this function should not be used if there
* may be more than [GameObject] with the specified [id] (see [Region.findObjects]).
*/
fun Region.findObject(position: Position, id: Int): GameObject? {
return find<GameObject>(position, DYNAMIC_OBJECT, STATIC_OBJECT) { it.id == id }
.firstOrNull()
}
/**
* Finds **all** [GameObject]s with the specified [id] at the specified [position].
*/
fun Region.findObjects(position: Position, id: Int): Sequence<GameObject> {
return find(position, DYNAMIC_OBJECT, STATIC_OBJECT) { it.id == id }
}
/**
* Finds the first [GameObject]s with the specified [id] at the specified [position].
*
* Note that the iteration order of entities in a [Region] is not defined - this function should not be used if there
* may be more than [GameObject] with the specified [id] (see [World.findObjects]).
*/
fun World.findObject(position: Position, id: Int): GameObject? {
return regionRepository.fromPosition(position).findObject(position, id)
}
/**
* Finds **all** [GameObject]s with the specified [id] at the specified [position].
*/
fun World.findObjects(position: Position, id: Int): Sequence<GameObject> {
return regionRepository.fromPosition(position).findObjects(position, id)
}
/**
* Removes the specified [GameObject] from the world, replacing it with [replacement] object for [delay] **pulses**.
*/
fun World.replaceObject(obj: GameObject, replacement: Int, delay: Int) {
val replacementObj = DynamicGameObject.createPublic(this, replacement, obj.position, obj.type, obj.orientation)
schedule(ExpireObjectTask(this, obj, replacementObj, delay))
}
/**
* A [ScheduledTask] that temporarily replaces the [existing] [GameObject] with the [replacement] [GameObject] for the
* specified [duration].
*
* @param existing The [GameObject] that already exists and should be replaced.
* @param replacement The [GameObject] to replace the [existing] object with.
* @param duration The time, in **pulses**, for the [replacement] object to exist in the game world.
*/
private class ExpireObjectTask(
private val world: World,
private val existing: GameObject,
private val replacement: GameObject,
private val duration: Int
) : ScheduledTask(0, true) {
private var respawning: Boolean = false
override fun execute() {
val region = world.regionRepository.fromPosition(existing.position)
if (!respawning) {
world.spawn(replacement)
respawning = true
setDelay(duration)
} else {
region.removeEntity(replacement)
world.spawn(existing)
stop()
}
}
}
@@ -1,63 +0,0 @@
package org.apollo.game.plugin.api
import org.apollo.game.model.Position
import org.apollo.game.model.World
import org.apollo.game.model.area.Region
import org.apollo.game.model.entity.Entity
import org.apollo.game.model.entity.EntityType
import org.apollo.game.model.entity.EntityType.DYNAMIC_OBJECT
import org.apollo.game.model.entity.EntityType.STATIC_OBJECT
import org.apollo.game.model.entity.obj.DynamicGameObject
import org.apollo.game.model.entity.obj.GameObject
import org.apollo.game.scheduling.ScheduledTask
fun <T : Entity> Region.find(position: Position, predicate: (T) -> Boolean, vararg types: EntityType): Sequence<T> {
return getEntities<T>(position, *types).asSequence().filter(predicate)
}
fun Region.findObjects(position: Position, id: Int): Sequence<GameObject> {
return find(position, { it.id == id }, DYNAMIC_OBJECT, STATIC_OBJECT)
}
fun Region.findObject(position: Position, id: Int): GameObject? {
return find<GameObject>(position, { it.id == id }, DYNAMIC_OBJECT, STATIC_OBJECT).firstOrNull()
}
fun World.findObject(position: Position, objectId: Int): GameObject? {
return regionRepository.fromPosition(position).findObject(position, objectId)
}
fun World.findObjects(position: Position, id: Int): Sequence<GameObject> {
return regionRepository.fromPosition(position).findObjects(position, id)
}
fun World.expireObject(obj: GameObject, replacement: Int, respawnDelay: Int) {
val replacementObj = DynamicGameObject.createPublic(this, replacement, obj.position, obj.type, obj.orientation)
schedule(ExpireObjectTask(this, obj, replacementObj, respawnDelay))
}
class ExpireObjectTask(
private val world: World,
private val existing: GameObject,
private val replacement: GameObject,
private val respawnDelay: Int
) : ScheduledTask(0, true) {
private var respawning: Boolean = false
override fun execute() {
val region = world.regionRepository.fromPosition(existing.position)
if (!respawning) {
world.spawn(replacement)
respawning = true
setDelay(respawnDelay)
} else {
region.removeEntity(replacement)
world.spawn(existing)
stop()
}
}
}
@@ -0,0 +1,94 @@
package org.apollo.game.plugin.api
import org.apollo.cache.def.ItemDefinition
import org.apollo.cache.def.NpcDefinition
import org.apollo.cache.def.ObjectDefinition
import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
import org.apollo.game.plugin.testing.junit.api.annotations.ItemDefinitions
import org.apollo.game.plugin.testing.junit.api.annotations.NpcDefinitions
import org.apollo.game.plugin.testing.junit.api.annotations.ObjectDefinitions
import org.apollo.game.plugin.testing.junit.params.ItemDefinitionSource
import org.apollo.game.plugin.testing.junit.params.NpcDefinitionSource
import org.apollo.game.plugin.testing.junit.params.ObjectDefinitionSource
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
@ExtendWith(ApolloTestingExtension::class)
class DefinitionsTests {
@Test
fun `can find an ItemDefinition directly using its id`() {
val searched = Definitions.item(0)
assertEquals(items.first().id, searched.id)
}
@Test
fun `can find an ItemDefinition using its name`() {
val searched = Definitions.item("item_two")
assertEquals(items[2].id, searched?.id)
}
@ParameterizedTest
@ItemDefinitionSource
fun `can find ItemDefinitions directly using id suffixing`(item: ItemDefinition) {
val searched = Definitions.item("${item.name}_${item.id}")
assertEquals(item.id, searched?.id)
}
@Test
fun `can find an NpcDefinition directly using its id`() {
val searched = Definitions.npc(0)
assertEquals(npcs.first().id, searched.id)
}
@Test
fun `can find an NpcDefinition using its name`() {
val searched = Definitions.npc("npc_two")
assertEquals(items[2].id, searched?.id)
}
@ParameterizedTest
@NpcDefinitionSource
fun `can find NpcDefinitions directly using id suffixing`(npc: NpcDefinition) {
val searched = Definitions.npc("${npc.name}_${npc.id}")
assertEquals(npc.id, searched?.id)
}
@Test
fun `can find an ObjectDefinition directly using its id`() {
val searched = Definitions.obj(0)
assertEquals(objs.first().id, searched.id)
}
@Test
fun `can find an ObjectDefinition using its name`() {
val searched = Definitions.obj("obj_two")
assertEquals(items[2].id, searched?.id)
}
@ParameterizedTest
@ObjectDefinitionSource
fun `can find ObjectDefinitions directly using id suffixing`(obj: ObjectDefinition) {
val searched = Definitions.obj("${obj.name}_${obj.id}")
assertEquals(obj.id, searched?.id)
}
private companion object {
@ItemDefinitions
val items = listOf("item zero", "item one", "item two", "item duplicate name", "item duplicate name")
.mapIndexed { id, name -> ItemDefinition(id).also { it.name = name } }
@NpcDefinitions
val npcs = listOf("npc zero", "npc one", "npc two", "npc duplicate name", "npc duplicate name")
.mapIndexed { id, name -> NpcDefinition(id).also { it.name = name } }
@ObjectDefinitions
val objs = listOf("obj zero", "obj one", "obj two", "obj duplicate name", "obj duplicate name")
.mapIndexed { id, name -> ObjectDefinition(id).also { it.name = name } }
}
}
@@ -0,0 +1,123 @@
package org.apollo.game.plugin.api
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.Skill
import org.apollo.game.plugin.testing.junit.ApolloTestingExtension
import org.apollo.game.plugin.testing.junit.api.annotations.TestMock
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(ApolloTestingExtension::class)
class PlayerTests {
@TestMock
lateinit var player: Player
@BeforeEach
fun setHitpointsLevel() {
player.skillSet.setSkill(Skill.HITPOINTS, Skill(1_154.0, 10, 10))
}
@Test
fun `can boost skill above maximum level`() {
player.apply {
hitpoints.boost(5)
assertEquals(15, hitpoints.current)
assertEquals(10, hitpoints.maximum)
}
}
@Test
fun `boosts to the same skill do not accumulate`() {
player.apply {
hitpoints.boost(5)
hitpoints.boost(4)
assertEquals(15, hitpoints.current)
assertEquals(10, hitpoints.maximum)
}
}
@Test
fun `greater boosts can override earlier boosts`() {
player.apply {
hitpoints.boost(5)
hitpoints.boost(7)
assertEquals(17, hitpoints.current)
assertEquals(10, hitpoints.maximum)
}
}
@Test
fun `can drain skills`() {
player.apply {
hitpoints.drain(5)
assertEquals(5, hitpoints.current)
assertEquals(10, hitpoints.maximum)
}
}
@Test
fun `repeated drains on the same skill accumulate`() {
player.apply {
hitpoints.drain(4)
hitpoints.drain(5)
assertEquals(1, hitpoints.current)
assertEquals(10, hitpoints.maximum)
}
}
@Test
fun `cannot drain skills below zero`() {
player.apply {
hitpoints.drain(99)
assertEquals(0, hitpoints.current)
assertEquals(10, hitpoints.maximum)
}
}
@Test
fun `can restore previously-drained skills`() {
player.skillSet.setCurrentLevel(Skill.HITPOINTS, 1)
player.apply {
hitpoints.restore(5)
assertEquals(6, hitpoints.current)
assertEquals(10, hitpoints.maximum)
}
}
@Test
fun `repeated restores on the same skill accumulate`() {
player.skillSet.setCurrentLevel(Skill.HITPOINTS, 1)
player.apply {
hitpoints.restore(3)
hitpoints.restore(4)
assertEquals(8, hitpoints.current)
assertEquals(10, hitpoints.maximum)
}
}
@Test
fun `cannot restore skills above their maximum level`() {
player.skillSet.setCurrentLevel(Skill.HITPOINTS, 1)
player.apply {
hitpoints.restore(99)
assertEquals(10, hitpoints.current)
assertEquals(10, hitpoints.maximum)
}
}
}
@@ -0,0 +1,26 @@
package org.apollo.game.plugin.api
import org.apollo.game.model.Position
import org.apollo.game.plugin.api.Position.component1
import org.apollo.game.plugin.api.Position.component2
import org.apollo.game.plugin.api.Position.component3
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class PositionTests {
@Test
fun `Positions are destructured in the correct order`() {
val x = 10
val y = 20
val z = 1
val position = Position(x, y, z)
val (x2, y2, z2) = position
assertEquals(x, x2) { "x coordinate mismatch in Position destructuring." }
assertEquals(y, y2) { "y coordinate mismatch in Position destructuring." }
assertEquals(z, z2) { "z coordinate mismatch in Position destructuring." }
}
}
+3 -3
View File
@@ -28,9 +28,9 @@ enum class Fish(val id: Int, val level: Int, val experience: Double, catchSuffix
/**
* The name of this fish, formatted so it can be inserted into a message.
*/
val catchMessage = "You catch ${catchSuffix ?: "a ${catchName()}."}"
val catchMessage by lazy { "You catch ${catchSuffix ?: "a ${catchName()}."}" }
private fun catchName() = Definitions.item(id)!!.name.toLowerCase().removePrefix("raw ")
private fun catchName() = Definitions.item(id).name.toLowerCase().removePrefix("raw ")
}
@@ -59,7 +59,7 @@ enum class FishingTool(
/**
* The name of this tool, formatted so it can be inserted into a message.
*/
val formattedName = Definitions.item(id)!!.name.toLowerCase()
val formattedName by lazy { Definitions.item(id).name.toLowerCase() }
}
+2 -2
View File
@@ -94,7 +94,7 @@ data class MiningTarget(val objectId: Int, val position: Position, val ore: Ore)
fun deplete(world: World) {
val obj = world.findObject(position, objectId)!!
world.expireObject(obj, ore.objects[objectId]!!, ore.respawn)
world.replaceObject(obj, ore.objects[objectId]!!, ore.respawn)
}
/**
@@ -113,7 +113,7 @@ data class MiningTarget(val objectId: Int, val position: Position, val ore: Ore)
/**
* Get the normalized name of the [Ore] represented by this target.
*/
fun oreName() = Definitions.item(ore.id)!!.name.toLowerCase()
fun oreName() = Definitions.item(ore.id).name.toLowerCase()
/**
* Reward a [player] with experience and ore if they have the inventory capacity to take a new ore.
@@ -7,7 +7,7 @@ import org.apollo.cache.def.ItemDefinition
import org.apollo.game.model.World
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.Skill
import org.apollo.game.plugin.api.expireObject
import org.apollo.game.plugin.api.replaceObject
import org.apollo.game.plugin.skills.mining.Ore
import org.apollo.game.plugin.skills.mining.Pickaxe
import org.apollo.game.plugin.skills.mining.TIN_OBJECTS
@@ -59,7 +59,7 @@ class MiningActionTests {
every { target.skillRequirementsMet(player) } returns true
every { target.isSuccessful(player, any()) } returns true
every { world.expireObject(obj, any(), any()) } answers { }
every { world.replaceObject(obj, any(), any()) } answers { }
player.skillSet.setCurrentLevel(Skill.MINING, Ore.TIN.level)
player.startAction(MiningAction(player, Pickaxe.BRONZE, target))
@@ -70,7 +70,7 @@ class MiningActionTests {
after(action.complete()) {
verify { player.sendMessage("You manage to mine some <ore_type>") }
verify { world.expireObject(obj, expiredTinId, Ore.TIN.respawn) }
verify { world.replaceObject(obj, expiredTinId, Ore.TIN.respawn) }
assertTrue(player.inventory.contains(Ore.TIN.id))
assertEquals(player.skillSet.getExperience(Skill.MINING), Ore.TIN.exp)
@@ -88,7 +88,7 @@ class WoodcuttingAction(
val obj = target.getObject(mob.world) ?: stop()
if (mob.inventory.add(target.tree.id)) {
val logName = Definitions.item(target.tree.id)!!.name.toLowerCase()
val logName = Definitions.item(target.tree.id).name.toLowerCase()
mob.sendMessage("You managed to cut some $logName.")
mob.woodcutting.experience += target.tree.exp
}
@@ -97,7 +97,7 @@ class WoodcuttingAction(
// respawn time: http://runescape.wikia.com/wiki/Trees
val respawn = TimeUnit.SECONDS.toMillis(MINIMUM_RESPAWN_TIME + rand(150)) / GameConstants.PULSE_DELAY
mob.world.expireObject(obj, target.tree.stump, respawn.toInt())
mob.world.replaceObject(obj, target.tree.stump, respawn.toInt())
stop()
}
}