mirror of
https://github.com/2006-Scape/apollo.git
synced 2026-07-03 00:38:21 +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