mirror of
https://github.com/2006-Scape/apollo.git
synced 2026-07-06 00:38:18 +00:00
Add a DSL for defining ammo types
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
plugin {
|
||||||
|
name = "combat"
|
||||||
|
description = "Extensible framework for combat."
|
||||||
|
authors = [
|
||||||
|
"Gary Tierney <gary.tierney@gmx.com>"
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"util:lookup"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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<Int, Ammo>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<String>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<AmmoBuilder>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Pair<Int, Ammo>> {
|
||||||
|
val ammoTypeSingular = name.removeSuffix("s")
|
||||||
|
val ammoList = mutableListOf<Pair<Int, Ammo>>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user