diff --git a/game/plugin/entity/spawn/src/spawn.kt b/game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.kt similarity index 80% rename from game/plugin/entity/spawn/src/spawn.kt rename to game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.kt index f70c9a28..3e773269 100644 --- a/game/plugin/entity/spawn/src/spawn.kt +++ b/game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.kt @@ -9,6 +9,10 @@ fun spawnNpc(name: String, x: Int, y: Int, z: Int = 0, id: Int? = null, facing: Spawns.list += Spawn(id, name, Position(x, y, z), facing) } +fun spawnNpc(name: String, position: Position, id: Int? = null, facing: Direction = Direction.NORTH) { + Spawns.list += Spawn(id, name, position, facing) +} + internal data class Spawn( val id: Int?, val name: String, diff --git a/game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.plugin.kts b/game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.plugin.kts new file mode 100644 index 00000000..7d9fb4ce --- /dev/null +++ b/game/plugin/entity/spawn/src/org/apollo/game/plugin/entity/spawn/Spawn.plugin.kts @@ -0,0 +1,20 @@ +package org.apollo.game.plugin.entity.spawn + +import org.apollo.game.model.entity.Npc +import org.apollo.game.plugin.api.Definitions + +start { world -> + for ((id, name, position, facing, animation, graphic) in Spawns.list) { + val definition = requireNotNull(id?.let(Definitions::npc) ?: Definitions.npc(name)) { + "Could not find an Npc named $name to spawn." + } + + val npc = Npc(world, definition.id, position).apply { + turnTo(position.step(1, facing)) + animation?.let(::playAnimation) + graphic?.let(::playGraphic) + } + + world.register(npc) + } +} diff --git a/game/plugin/entity/spawn/src/spawn.plugin.kts b/game/plugin/entity/spawn/src/spawn.plugin.kts deleted file mode 100644 index 87e3bb51..00000000 --- a/game/plugin/entity/spawn/src/spawn.plugin.kts +++ /dev/null @@ -1,19 +0,0 @@ - -import org.apollo.game.model.entity.Npc -import org.apollo.game.plugin.api.Definitions -import org.apollo.game.plugin.entity.spawn.Spawns - -start { world -> - Spawns.list.forEach { spawn -> - val definition = spawn.id?.let(Definitions::npc) ?: Definitions.npc(spawn.name) - ?: throw IllegalArgumentException("Invalid NPC name or ID ${spawn.name}, ${spawn.id}") - - val npc = Npc(world, definition.id, spawn.position) - npc.turnTo(spawn.position.step(1, spawn.facing)) - - spawn.spawnAnimation?.let(npc::playAnimation) - spawn.spawnGraphic?.let(npc::playGraphic) - - world.register(npc) - } -} diff --git a/game/plugin/entity/spawn/test/org/apollo/game/plugin/entity/spawn/SpawnTests.kt b/game/plugin/entity/spawn/test/org/apollo/game/plugin/entity/spawn/SpawnTests.kt new file mode 100644 index 00000000..9e434b29 --- /dev/null +++ b/game/plugin/entity/spawn/test/org/apollo/game/plugin/entity/spawn/SpawnTests.kt @@ -0,0 +1,143 @@ +package org.apollo.game.plugin.entity.spawn + +import org.apollo.cache.def.NpcDefinition +import org.apollo.game.model.* +import org.apollo.game.plugin.testing.junit.ApolloTestingExtension +import org.apollo.game.plugin.testing.junit.api.annotations.NpcDefinitions +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.Disabled +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + +@ExtendWith(ApolloTestingExtension::class) +class SpawnTests { + + @TestMock + lateinit var world: World + + @BeforeEach + fun addNewNpcs() { + var previousSize: Int + + do { + previousSize = world.npcRepository.size() + world.pulse() + } while (previousSize != world.npcRepository.size()) + } + + @MethodSource("npcs spawned by id") + @ParameterizedTest(name = "spawning {1} at {2} (id = {0})") + fun `spawned npcs are in the correct position`(id: Int, name: String, spawn: Position) { + val npc = world.npcRepository.find { it.id == id } + + assertEquals(spawn, npc?.position) { "Failed to find npc $name with id $id." } + } + + @MethodSource("npcs spawned by id with directions") + @ParameterizedTest(name = "spawning {1} with direction {3} (id = {0})") + fun `spawned npcs are facing the correct direction`(id: Int, name: String, spawn: Position, direction: Direction) { + val npc = requireNotNull(world.npcRepository.find { it.id == id }) { "Failed to find npc $name with id $id." } + val facing = spawn.step(1, direction) + + assertEquals(facing, npc.facingPosition) + } + + @Disabled("Currently no way to test if the animation was played") + @MethodSource("npcs spawned by id with animations") + @ParameterizedTest(name = "spawning {1} with animation {3} (id = {0})") + fun `spawned npcs are playing the correct animation`(id: Int, name: String, spawn: Position, animation: Animation) { + val npc = requireNotNull(world.npcRepository.find { it.id == id }) { "Failed to find npc $name with id $id." } + + TODO("How to verify that npc.playAnimation was called with $animation.") + } + + @Disabled("Currently no way to test if the graphic was played") + @MethodSource("npcs spawned by id with graphics") + @ParameterizedTest(name = "spawning {1} with graphic {3} (id = {0})") + fun `spawned npcs are playing the correct graphic`(id: Int, name: String, spawn: Position, graphic: Graphic) { + val npc = requireNotNull(world.npcRepository.find { it.id == id }) { "Failed to find npc $name with id $id." } + + TODO("How to verify that npc.playGraphic was called with $graphic.") + } + + @MethodSource("npcs spawned by name") + @ParameterizedTest(name = "spawning {0}") + fun `spawns are looked up by name if the id is unspecified`(name: String) { + val npc = world.npcRepository.find { it.definition.name === name }!! + val expectedId = name.substringAfterLast("_").toInt() + + assertEquals(expectedId, npc.id) + } + + companion object { + + // This test class has multiple (hidden) order dependencies because of the nature of the spawn + // plugin, where npcs are inserted into the world immediately after world initialisation. + // + // All npcs that should be spawned by the test must be passed to `spawnNpc` _before_ the test world + // is created by the ApolloTestingExtension - which means they must be done inside the initialisation + // block of this companion object. + // + // When npcs are created, however, they look up their NpcDefinition - so all of the definitions must + // be created (via `@NpcDefinitions`) before the initialisation block is executed. + // + // The world must also be pulsed after the spawn plugin executes, so that npcs are registered (i.e. moved + // out of the queue). + + @JvmStatic + fun `npcs spawned by id`(): List { + return npcs.filterNot { it.id == null } + .map { (id, name, position) -> Arguments.of(id, name, position) } + } + + @JvmStatic + fun `npcs spawned by id with directions`(): List { + return npcs.filterNot { it.id == null } + .map { (id, name, position, direction) -> Arguments.of(id, name, position, direction) } + } + + @JvmStatic + fun `npcs spawned by id with animations`(): List { + return npcs.filterNot { it.id == null } + .map { (id, name, position, _, animation) -> Arguments.of(id, name, position, animation) } + } + + @JvmStatic + fun `npcs spawned by id with graphics`(): List { + return npcs.filterNot { it.id == null } + .map { (id, name, position, _, _, graphic) -> Arguments.of(id, name, position, graphic) } + } + + @JvmStatic + fun `npcs spawned by name`(): List { + return npcs.filter { it.id == null } + .map { (_, name) -> Arguments.of(name) } + } + + private val npcs = listOf( + Spawn(0, "hans", Position(1000, 1000, 0), facing = Direction.NORTH, spawnAnimation = Animation(10, 100)), + Spawn(1, "man", Position(1000, 1000, 0), facing = Direction.NORTH, spawnGraphic = Graphic(154, 0, 100)), + Spawn(null, "man_2", Position(1000, 1000, 2), facing = Direction.NORTH), + Spawn(null, "man_3", Position(1000, 1000, 3), facing = Direction.SOUTH), + Spawn(6, "fakename123", Position(1500, 1500, 3), facing = Direction.EAST, spawnAnimation = Animation(112)), + Spawn(12, "fakename123", Position(1500, 1500, 3), facing = Direction.WEST, spawnGraphic = Graphic(964)) + ) + + @NpcDefinitions + val definitions = npcs.map { (id, name) -> + val definitionId = id ?: name.substringAfterLast("_").toInt() + NpcDefinition(definitionId).also { it.name = name } + } + + init { // Must come after NpcDef initialisation, before mocked World initialisation + for ((id, name, position, direction) in npcs) { + spawnNpc(name, position.x, position.y, position.height, id, direction) + } + } + } + +}