diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/Definitions.kt b/game/plugin/api/src/org/apollo/game/plugin/api/Definitions.kt index 2512d72c..d083dc9c 100644 --- a/game/plugin/api/src/org/apollo/game/plugin/api/Definitions.kt +++ b/game/plugin/api/src/org/apollo/game/plugin/api/Definitions.kt @@ -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) } diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/player.kt b/game/plugin/api/src/org/apollo/game/plugin/api/Player.kt similarity index 85% rename from game/plugin/api/src/org/apollo/game/plugin/api/player.kt rename to game/plugin/api/src/org/apollo/game/plugin/api/Player.kt index 069ffe97..9205bc0b 100644 --- a/game/plugin/api/src/org/apollo/game/plugin/api/player.kt +++ b/game/plugin/api/src/org/apollo/game/plugin/api/Player.kt @@ -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) } diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/position.kt b/game/plugin/api/src/org/apollo/game/plugin/api/Position.kt similarity index 100% rename from game/plugin/api/src/org/apollo/game/plugin/api/position.kt rename to game/plugin/api/src/org/apollo/game/plugin/api/Position.kt diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/util.kt b/game/plugin/api/src/org/apollo/game/plugin/api/Random.kt similarity index 100% rename from game/plugin/api/src/org/apollo/game/plugin/api/util.kt rename to game/plugin/api/src/org/apollo/game/plugin/api/Random.kt diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/World.kt b/game/plugin/api/src/org/apollo/game/plugin/api/World.kt new file mode 100644 index 00000000..a44163c1 --- /dev/null +++ b/game/plugin/api/src/org/apollo/game/plugin/api/World.kt @@ -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 = region.find(position, EntityType.GROUND_ITEM) { item -> item.id == GOLD_COINS } + * ``` + */ +fun Region.find(position: Position, vararg types: EntityType, predicate: (T) -> Boolean): Sequence { + return getEntities(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(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 { + 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 { + 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() + } + } + +} \ No newline at end of file diff --git a/game/plugin/api/src/org/apollo/game/plugin/api/world.kt b/game/plugin/api/src/org/apollo/game/plugin/api/world.kt deleted file mode 100644 index 36ab4859..00000000 --- a/game/plugin/api/src/org/apollo/game/plugin/api/world.kt +++ /dev/null @@ -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 Region.find(position: Position, predicate: (T) -> Boolean, vararg types: EntityType): Sequence { - return getEntities(position, *types).asSequence().filter(predicate) -} - -fun Region.findObjects(position: Position, id: Int): Sequence { - return find(position, { it.id == id }, DYNAMIC_OBJECT, STATIC_OBJECT) -} - -fun Region.findObject(position: Position, id: Int): GameObject? { - return find(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 { - 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() - } - } -} \ No newline at end of file diff --git a/game/plugin/api/test/org/apollo/game/plugin/api/DefinitionsTests.kt b/game/plugin/api/test/org/apollo/game/plugin/api/DefinitionsTests.kt new file mode 100644 index 00000000..43af9485 --- /dev/null +++ b/game/plugin/api/test/org/apollo/game/plugin/api/DefinitionsTests.kt @@ -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 } } + + } + +} \ No newline at end of file diff --git a/game/plugin/api/test/org/apollo/game/plugin/api/PlayerTests.kt b/game/plugin/api/test/org/apollo/game/plugin/api/PlayerTests.kt new file mode 100644 index 00000000..a1b1f3b7 --- /dev/null +++ b/game/plugin/api/test/org/apollo/game/plugin/api/PlayerTests.kt @@ -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) + } + } + +} \ No newline at end of file diff --git a/game/plugin/api/test/org/apollo/game/plugin/api/PositionTests.kt b/game/plugin/api/test/org/apollo/game/plugin/api/PositionTests.kt new file mode 100644 index 00000000..452d2f00 --- /dev/null +++ b/game/plugin/api/test/org/apollo/game/plugin/api/PositionTests.kt @@ -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." } + } + +} \ No newline at end of file diff --git a/game/plugin/skills/fishing/src/fishing.kt b/game/plugin/skills/fishing/src/fishing.kt index 807ac994..ccf58455 100644 --- a/game/plugin/skills/fishing/src/fishing.kt +++ b/game/plugin/skills/fishing/src/fishing.kt @@ -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() } } diff --git a/game/plugin/skills/mining/src/mining.kt b/game/plugin/skills/mining/src/mining.kt index c98dd8a8..c3b773f2 100644 --- a/game/plugin/skills/mining/src/mining.kt +++ b/game/plugin/skills/mining/src/mining.kt @@ -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. diff --git a/game/plugin/skills/mining/test/MiningActionTests.kt b/game/plugin/skills/mining/test/MiningActionTests.kt index f9fe0319..2bd964f5 100644 --- a/game/plugin/skills/mining/test/MiningActionTests.kt +++ b/game/plugin/skills/mining/test/MiningActionTests.kt @@ -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 ") } - 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) diff --git a/game/plugin/skills/woodcutting/src/woodcutting.plugin.kts b/game/plugin/skills/woodcutting/src/woodcutting.plugin.kts index bdb0b032..2966914e 100644 --- a/game/plugin/skills/woodcutting/src/woodcutting.plugin.kts +++ b/game/plugin/skills/woodcutting/src/woodcutting.plugin.kts @@ -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() } }