From 5bd663f5052b1d81671d18d2129bfe3ffdbc4b70 Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Sun, 24 Sep 2017 15:02:51 +0100 Subject: [PATCH] Add a DSL for defining ammo types --- game/plugin/entity/combat/build.gradle | 10 + game/plugin/entity/combat/src/ammo.dsl.kt | 293 ++++++++++++++++++ .../entity/combat/src/ammo/arrows.plugin.kts | 71 +++++ .../plugin/entity/combat/test/AmmoDslTests.kt | 79 +++++ 4 files changed, 453 insertions(+) create mode 100644 game/plugin/entity/combat/build.gradle create mode 100644 game/plugin/entity/combat/src/ammo.dsl.kt create mode 100644 game/plugin/entity/combat/src/ammo/arrows.plugin.kts create mode 100644 game/plugin/entity/combat/test/AmmoDslTests.kt diff --git a/game/plugin/entity/combat/build.gradle b/game/plugin/entity/combat/build.gradle new file mode 100644 index 00000000..440a9124 --- /dev/null +++ b/game/plugin/entity/combat/build.gradle @@ -0,0 +1,10 @@ +plugin { + name = "combat" + description = "Extensible framework for combat." + authors = [ + "Gary Tierney " + ] + dependencies = [ + "util:lookup" + ] +} \ No newline at end of file diff --git a/game/plugin/entity/combat/src/ammo.dsl.kt b/game/plugin/entity/combat/src/ammo.dsl.kt new file mode 100644 index 00000000..b7a75270 --- /dev/null +++ b/game/plugin/entity/combat/src/ammo.dsl.kt @@ -0,0 +1,293 @@ +import org.apollo.game.model.Graphic +import org.apollo.game.model.Position +import org.apollo.game.model.World +import org.apollo.game.model.entity.Mob +import org.apollo.game.model.entity.Projectile + +val AMMO = mutableMapOf() + +/** + * Create and register a new collection of [Ammo] based on the given + * [AmmoTypeBuilder]. + */ +public fun ammo(name: String, builder: AmmoTypeBuilder.() -> Unit) { + val ammoType = AmmoTypeBuilder(name) + builder(ammoType) + + val ammoList = ammoType.build() + ammoList.forEach { (id, ammo) -> AMMO[id] = ammo } +} + +open class Ammo( + val requiredLevel: Int, + val dropChance: Double, + val projectileFactory: AmmoProjectileFactory, + val attack: Graphic? = null +) { + fun toEnchanted( + enchantment: Graphic, + enchantmentEffect: AmmoEnchantmentEffect, + enchantmentChanceSupplier: AmmoEnchantmentChanceSupplier + ): EnchantedAmmo { + return EnchantedAmmo( + enchantment, + enchantmentEffect, + enchantmentChanceSupplier, + requiredLevel, + dropChance, + projectileFactory + ) + } +} + +class EnchantedAmmo( + val enchantment: Graphic, + val enchantmentEffect: AmmoEnchantmentEffect, + val enchantmentChanceSupplier: AmmoEnchantmentChanceSupplier, + requiredLevel: Int, + dropChance: Double, + projectileFactory: AmmoProjectileFactory, + attack: Graphic? = null +) : Ammo( + requiredLevel, + dropChance, + projectileFactory, + attack +) + +typealias AmmoEnchantmentEffect = Mob.() -> Unit +typealias AmmoEnchantmentChanceSupplier = Mob.() -> Double +typealias AmmoProjectileFactory = (World, Position, Mob) -> Projectile + +/** + * A [DslMarker] for the ammo DSL. + */ +@DslMarker annotation class AmmoDslMarker + +/** + * A builder for the graphics related to firing ammo. + */ +@AmmoDslMarker +class AmmoGraphicsBuilder { + /** + * The graphic used in the projectile fired. + */ + var projectile: Int? = null + + /** + * The graphic used when the attacker fires the shot. + */ + var attack: Int? = null + + /** + * The graphic used when a [Mob] is hit with an enchanted ammo effect. + */ + var enchanted: Int? = null + + operator fun invoke(builder: AmmoGraphicsBuilder.() -> Unit) = builder(this) +} + +@AmmoDslMarker +class AmmoDropChance() + +/** + * A DSL builder for an ammo's ranged level requirements. + */ +@AmmoDslMarker +class AmmoRequirementBuilder { + internal var level: Int = 1 + + /** + * Set the ranged level required to use this ammo. + */ + infix fun level(requirement: Int): AmmoRequirementBuilder { + this.level = requirement + return this + } +} + +@AmmoDslMarker +class AmmoBuilder(val name: String) { + /** + * The chance that ammo of this type will be dropped rather than broken. + */ + internal var dropChanceValue: Double = 1.0 + + /** + * Internal value to hold a dummy [AmmoDropChance] field that can be used by the overriden `%` operator. + */ + internal val dropChance = AmmoDropChance() + + /** + * `%` operator overload on [Int] for a fluent way to define an [Ammo]'s chance of dropping instead of breaking. + */ + operator fun Int.rem(placeholder: AmmoDropChance) { + dropChanceValue = this / 100.0 + } + + /** + * The graphics played when this ammo is used. + */ + val graphics = AmmoGraphicsBuilder() + + /** + * An optional enchantment associated with this ammo. + */ + val enchantment = AmmoEnchantmentBuilder() + + /** + * The level requirements needed to use this ammo. + */ + val requires = AmmoRequirementBuilder() +} + +/** + * A builder for effects applied to [Mob]'s hit with ammo that can be chanted. + */ +@AmmoDslMarker +class AmmoEnchantmentBuilder { + internal var effect: AmmoEnchantmentEffect? = null + internal var chance: AmmoEnchantmentChanceSupplier? = null + + /** + * Set the effect that will be applied to a [Mob] whenever this enchantment + * is succesfully applied. + */ + infix fun effect(effect: AmmoEnchantmentEffect): AmmoEnchantmentBuilder { + this.effect = effect + return this + } + + /** + * Set the function used to calculate the chance of an enchantment triggering + * on a projectile fired by a given [Mob]. + */ + infix fun chance(chanceSupplier: AmmoEnchantmentChanceSupplier): AmmoEnchantmentBuilder { + this.chance = chanceSupplier + return this + } +} + +/** + * A builder for the weapon classes an [AmmoType] can be used with. + */ +@AmmoDslMarker +class AmmoTypeUseabilityBuilder { + /** + * A list of weapon class names that the [AmmoType] can be used with. + */ + val weaponClasses = mutableListOf() + + /** + * Include [Ammo] under the current ammo type as useable by the given `weaponClass`. + */ + infix fun from(weaponClass: String): AmmoTypeUseabilityBuilder = and(weaponClass) + + /** + * Include [Ammo] under the current ammo type as useable by the given `weaponClass`. + */ + infix fun and(weaponClass: String): AmmoTypeUseabilityBuilder { + weaponClasses.add(weaponClass) + return this + } +} + +/** + * A builder DSL for an [AmmoProjectileFactory]. + */ +@AmmoDslMarker +class AmmoProjectileFactoryBuilder { + + var startHeight: Int = 40 + var endHeight: Int = 35 + var delay: Int? = null + var lifetime: Int? = null + var pitch: Int? = null + + operator fun invoke(builder: AmmoProjectileFactoryBuilder.() -> Unit) = builder(this) + + /** + * Build an [AmmoProjectileFactory] for the [Ammo] variant with the given graphic ID. + */ + fun build(variant: Int): AmmoProjectileFactory = { world, source, target -> + Projectile.ProjectileBuilder(world) + .startHeight(startHeight) + .endHeight(endHeight) + .delay(delay!!) + .lifetime(lifetime!!) + .pitch(pitch!!) + .graphic(variant) + .source(source) + .target(target) + .build() + } +} + +/** + * A builder for a collection of [Ammo] of the same type. + */ +@AmmoDslMarker +class AmmoTypeBuilder(val name: String) { + /** + * The constraints on what weapon classes this ammo type can be fired from. + */ + internal val fired = AmmoTypeUseabilityBuilder() + + /** + * The base projectile factory for projectiles of this ammo type. + */ + internal val projectile = AmmoProjectileFactoryBuilder() + + /** + * The variants of ammo that belong to this ammo type. + */ + internal val variants = mutableListOf() + + /** + * String invocation overload that creates a new ammo variant + * given an [AmmoBuilder] + */ + operator fun String.invoke(builder: AmmoBuilder.() -> Unit) { + val variant = AmmoBuilder(this) + builder(variant) + + variants.add(variant) + } + + /** + * Build a list of ItemID -> [Ammo] pairs based on the variants in this [AmmoTypeBuilder]. + */ + fun build(): List> { + val ammoTypeSingular = name.removeSuffix("s") + val ammoList = mutableListOf>() + + variants.forEach { + val requiredLevel = it.requires.level + val dropChance = it.dropChanceValue + + val projectileGraphicId = it.graphics.projectile ?: throw RuntimeException("Every ammo requires a projectile id") + val projectileFactory = projectile.build(projectileGraphicId) + val attack = it.graphics.attack?.let(::Graphic) + + val ammoName = "${it.name} $ammoTypeSingular" + val ammo = Ammo(requiredLevel, dropChance, projectileFactory, attack) + val ammoId = lookup_item(ammoName)?.id ?: throw RuntimeException("Unable to find ammo named $ammoName") + + ammoList.add(Pair(ammoId, ammo)) + + val enchantment = it.graphics.enchanted?.let(::Graphic) + val enchantmentEffect = it.enchantment.effect + val enchantmentChanceSupplier = it.enchantment.chance + + if (enchantment != null && enchantmentEffect != null && enchantmentChanceSupplier != null) { + val enchantedAmmoName = "$ammoName (e)" + val enchantedAmmo = ammo.toEnchanted(enchantment, enchantmentEffect, enchantmentChanceSupplier) + val enchantedAmmoId = lookup_item(enchantedAmmoName)?.id ?: throw RuntimeException("Unable to find ammo named $enchantedAmmoName") + + ammoList.add(Pair(enchantedAmmoId, enchantedAmmo)) + } + } + + return ammoList + } +} diff --git a/game/plugin/entity/combat/src/ammo/arrows.plugin.kts b/game/plugin/entity/combat/src/ammo/arrows.plugin.kts new file mode 100644 index 00000000..3bb2f045 --- /dev/null +++ b/game/plugin/entity/combat/src/ammo/arrows.plugin.kts @@ -0,0 +1,71 @@ +ammo("arrow") { + fired from "shortbows" and "longbows" + + projectile { + startHeight = 41 + endHeight = 37 + delay = 41 + lifetime = 6 + pitch = 15 + } + + "bronze" { + requires level 1 + 30% dropChance + + graphics { + projectile = 10 + attack = 19 + } + } + + "iron" { + requires level 1 + 35% dropChance + + graphics { + projectile = 9 + attack = 18 + } + } + + "steel" { + requires level 5 + 40% dropChance + + graphics { + projectile = 11 + attack = 20 + } + } + + "mithril" { + requires level 20 + 45% dropChance + + graphics { + projectile = 12 + attack = 21 + } + } + + "adamant" { + requires level 30 + 50% dropChance + + graphics { + projectile = 13 + attack = 22 + } + } + + "rune" { + requires level 40 + 60% dropChance + + graphics { + projectile = 15 + attack = 24 + } + } +} \ No newline at end of file diff --git a/game/plugin/entity/combat/test/AmmoDslTests.kt b/game/plugin/entity/combat/test/AmmoDslTests.kt new file mode 100644 index 00000000..1795d558 --- /dev/null +++ b/game/plugin/entity/combat/test/AmmoDslTests.kt @@ -0,0 +1,79 @@ +import org.apollo.cache.def.ItemDefinition +import org.apollo.game.model.Graphic +import org.apollo.game.model.Position +import org.apollo.game.model.World +import org.apollo.game.model.entity.Player +import org.apollo.util.security.PlayerCredentials +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class AmmoDslTests { + + private val TEST_AMMO_ID : Int = 0 + + @Before + fun setupTestAmmo() { + val testItem = ItemDefinition(TEST_AMMO_ID) + testItem.name = "bronze arrow" + + ItemDefinition.init(arrayOf(testItem)) + + ammo("arrow") { + projectile { + startHeight = 10 + endHeight = 20 + delay = 30 + lifetime = 40 + pitch = 60 + } + + "bronze" { + requires level 1 + 20% dropChance + + graphics { + projectile = 10 + attack = 20 + } + } + } + } + + @Test + fun `Ammo variants should inherit projectile parameters from their ammo type`() { + val ammo = AMMO[TEST_AMMO_ID]!! + val world = World() + val target = Player(world, PlayerCredentials("fake", "fake", 1, 1, "fake"), Position(1, 1)) + val source = Position(1, 1) + val ammoProjectile = ammo.projectileFactory(World(), source, target) + + assertEquals(10, ammoProjectile.startHeight) + assertEquals(20, ammoProjectile.endHeight) + assertEquals(30, ammoProjectile.delay) + assertEquals(40, ammoProjectile.lifetime) + assertEquals(60, ammoProjectile.pitch) + assertEquals(10, ammoProjectile.graphic) + } + + @Test + fun `Ammo variants should set their level requirements correctly`() { + val ammo = AMMO[TEST_AMMO_ID]!! + + assertEquals(1, ammo.requiredLevel) + } + + @Test + fun `Ammo variants should set their drop chance correctly`() { + val ammo = AMMO[TEST_AMMO_ID]!! + + assertEquals(ammo.dropChance, 0.2, 0.0) + } + + @Test + fun `Ammo variants should set their graphics correctly`() { + val ammo = AMMO[TEST_AMMO_ID]!! + + assertEquals(20, ammo.attack?.id) + } +} \ No newline at end of file