Add a basic port of the Ruby combat framework

This commit is contained in:
Gary Tierney
2017-12-31 07:28:41 +00:00
parent 106fbfb8f9
commit d392913356
30 changed files with 976 additions and 117 deletions
+2 -1
View File
@@ -5,6 +5,7 @@ plugin {
"Gary Tierney <gary.tierney@gmx.com>"
]
dependencies = [
"util:lookup"
"util:lookup",
"api"
]
}
@@ -0,0 +1,37 @@
import org.apollo.game.message.impl.NpcActionMessage
import org.apollo.game.model.event.impl.LogoutEvent
/**
on :message, :npc_action do |player, message|
target = $world.npc_repository.get message.index
# unless target.attacking
# target_combat_state = get_combat_state target
# target_combat_state.target = player
#
# target.start_action CombatAction.new(target)
# end
player_combat_state = player.get_combat_state
player_combat_state.target = target
player.send HintIconMessage.for_npc(target.index)
player.walking_queue.clear
player.start_action CombatAction.new(player)
end
*/
start {
}
on_player_event { LogoutEvent::class }
.then {
CombatStateManager.remove(it)
}
on { NpcActionMessage::class }
.then {
CombatAction.start(this, it, it.world.npcRepository[index])
}
@@ -1,11 +1,11 @@
ammo("arrow") {
object Arrows : AmmoType({
fired from "shortbows" and "longbows"
projectile {
startHeight = 41
endHeight = 37
delay = 41
lifetime = 6
lifetime = 60
pitch = 15
}
@@ -68,4 +68,4 @@ ammo("arrow") {
attack = 24
}
}
}
})
@@ -0,0 +1,65 @@
Scimitar("Iron Scimitar") {
attackBonuses {
stab = 2
slash = 10
}
meleeStrength = 9
}
Scimitar("Steel Scimitar") {
attackBonuses {
stab = 3
slash = 15
}
meleeStrength = 14
}
// @todo - regexp support
//Scimitar("/(black|white) Scimitar/") {
// attackBonuses {
// stab = 4
// slash = 19
// }
//
// meleeStrength = 14
//}
Scimitar("Mithril Scimitar") {
attackBonuses {
stab = 5
slash = 21
}
meleeStrength = 20
}
Scimitar("Adamant Scimitar") {
attackBonuses {
stab = 6
slash = 29
}
meleeStrength = 28
}
Scimitar("Rune Scimitar") {
attackBonuses {
stab = 7
slash = 45
}
meleeStrength = 44
}
Scimitar("Dragon Scimitar") {
attackBonuses {
stab = 8
slash = 67
}
meleeStrength = 66
}
@@ -0,0 +1,21 @@
import AttackStyle.*
import org.apollo.game.model.Animation
object Scimitar : MeleeWeaponClass({
widgetId = 2423
defaults {
attackSpeed = 4
attackAnimation = Animation(390)
attackType = AttackType.Slash
}
Accurate { button = 2 }
Aggressive { button = 3 }
AltAggressive {
button = 4
attackType = AttackType.Stab
attackAnimation = Animation(391)
}
Defensive { button = 5 }
})
@@ -0,0 +1,3 @@
Bow("Shortbow") {
}
@@ -0,0 +1,28 @@
import AttackStyle.*
import AttackType.Ranged
import org.apollo.game.model.Animation
object Bow : RangedWeaponClass(Arrows, {
widgetId = 1764
defaults {
attackType = Ranged
attackAnimation = Animation(426)
attackSpeed = 4
attackRange = 7
}
Accurate {
button = 1772
}
Rapid {
button = 1770
attackSpeed = 3
}
LongRanged {
button = 1771
}
})
@@ -0,0 +1,141 @@
import AttackStyle.*
import AttackType.Ranged
import org.apollo.game.model.Animation
import org.apollo.game.model.entity.Mob
import org.apollo.game.model.entity.Player
import org.apollo.game.model.entity.Skill
import org.apollo.game.plugins.api.attack
import org.apollo.game.plugins.api.defence
import org.apollo.game.plugins.api.skills
abstract class Attack(
val speed: Int,
val range: Int,
val type: AttackType,
val style: AttackStyle,
private val attackAnimation: Animation,
protected val requirements: MutableList<AttackRequirement>
) {
/**
* Calculate the maximum damage this [Attack] can give out for the [source] [Mob].
*/
abstract fun maxDamage(source: Mob): Int
/**
* Check the requirements for this attack and execute it, making [AttackHitAttempt] rolls
* and depleting any resources used by [AttackRequirement]s.
*/
fun execute(source: Mob, target: Mob) {
val failingRequirement: AttackRequirementResult? = requirements.map({ it.check(source) })
.firstOrNull({ it != AttackRequirementResult.Ok })
when (failingRequirement) {
is AttackRequirementResult.Failed -> {
(source as? Player)?.sendMessage(failingRequirement.message)
return
}
}
// @todo - refactor this out somewhere, all rolls are the same calculations
// (damage, hit + defence)
val effectiveAttackBase = source.skills.attack.currentLevel
//@todo - attack prayers
val effectiveAttackModifier = 1.0
val attackStyleBonus = when (style) {
Accurate -> 3
Controlled -> 1
LongRanged -> 1
else -> 0
}
val effectiveAttack = effectiveAttackBase * effectiveAttackModifier + attackStyleBonus + 8
//@todo - get attack bonus from stats
val attackEquipmentBonus = 0
val maxHitRoll = effectiveAttack * (attackEquipmentBonus + 64)
val effectiveDefenceBase = target.skills.defence.currentLevel
//@todo - defence prayers
val effectiveDefenceModifier = 1.0
val defenceStyleBonus = when (style) {
Defensive, LongRanged -> 3
Controlled -> 1
else -> 0
}
val effectiveDefence = effectiveDefenceBase * effectiveDefenceModifier + defenceStyleBonus + 8
//@todo - get defence bonus from stats
val defenceEquipmentBonus = 0
val maxDefenceRoll = effectiveDefence * (defenceEquipmentBonus + 64)
val accuracy = if (maxHitRoll > maxDefenceRoll) {
1 - (maxDefenceRoll + 2) / (2 * (maxHitRoll + 1))
} else {
maxHitRoll / (2 * (maxDefenceRoll + 1))
}
val maxDamageRoll = maxDamage(source)
val damageOutput = AttackOutput(type)
doAttack(source, target, damageOutput)
requirements.forEach { it.apply(source) }
source.playAnimation(attackAnimation)
damageOutput.rolls.forEach {
val hitRoll = Math.random()
val hitDamage = if (accuracy >= hitRoll) {
scale(0.0 to 0.99, 1.0 to (maxDamageRoll * it.modifier), it.roll)
} else {
0
}
target.damage(hitDamage, if (hitDamage == 0) 0 else 1, false)
}
}
protected abstract fun doAttack(source: Mob, target: Mob, output: AttackOutput)
}
/**
* A basic attack that calculates maximum damage using the <code>effective strength</code> calculation.
*
* @todo - only use attacks for damage output / playing effects. tracking speed/range/type/style can happen elsewhere.
*/
abstract class BasicAttack(
speed: Int,
range: Int,
type: AttackType,
style: AttackStyle,
attackAnimation: Animation,
requirements: MutableList<AttackRequirement>
) : Attack(speed, range, type, style, attackAnimation, requirements) {
override fun maxDamage(source: Mob): Int {
val effectiveStrengthBase = when (type) {
Ranged -> source.skillSet.getCurrentLevel(Skill.RANGED)
else -> source.skillSet.getCurrentLevel(Skill.STRENGTH)
}
// @todo - prayer + others (?)
val effectiveStrengthModifier = 1.0
val hitStyleBonus = when (style) {
Aggressive -> 3
LongRanged, Controlled -> 1
Defensive -> 0
Accurate -> if (type == Ranged) 3 else 0
else -> 0
}
val effectiveStrength = Math.floor(effectiveStrengthBase * effectiveStrengthModifier) + hitStyleBonus + 8
//@todo - get from combat bonuses
val strengthBonus = 0
val baseDamage = 0.5 + effectiveStrength * (strengthBonus + 64) / 640
return Math.floor(baseDamage).toInt()
}
}
@@ -0,0 +1,24 @@
import org.apollo.game.model.entity.Projectile
data class AttackHitAttempt(val delay: Int, val roll: Double, val modifier: Double) {
fun isImmediate(): Boolean {
return delay == 0
}
}
class AttackOutput(val type: AttackType) {
val rolls = mutableListOf<AttackHitAttempt>()
fun hit(modifier: Double = 1.0) {
rolls.add(AttackHitAttempt(0, Math.random(), modifier))
}
fun projectile(projectile: Projectile, modifier: Double = 1.0) {
val source = projectile.position
val target = projectile.destination
val projectileLifetime = projectile.delay + projectile.lifetime + source.getDistance(target) * 5
val projectileTicks = Math.floor(projectileLifetime * 0.02587)
rolls.add(AttackHitAttempt(projectileTicks.toInt(), Math.random(), modifier))
}
}
@@ -0,0 +1,3 @@
import org.apollo.game.model.Animation
import org.apollo.game.model.Graphic
@@ -0,0 +1,6 @@
import org.apollo.game.model.Animation
//@todo - attack factory maybe not necessary when factoring details out of attack
interface AttackFactory {
fun createAttack(speed: Int, range: Int, type: AttackType, style: AttackStyle, animation: Animation): Attack
}
@@ -0,0 +1,65 @@
import AttackRequirementResult.Failed
import AttackRequirementResult.Ok
import org.apollo.cache.def.EquipmentDefinition
import org.apollo.game.model.entity.EquipmentConstants
import org.apollo.game.model.entity.Mob
sealed class AttackRequirementResult {
data class Failed(val message: String) : AttackRequirementResult()
object Ok : AttackRequirementResult()
}
interface AttackRequirement {
/**
* Check if the [mob] initiating the [BasicAttack] meets the requirements.
*/
fun check(mob: Mob): AttackRequirementResult
/**
* Apply this [AttackRequirement] to the [Mob] that initiated the doAttack. Optionally carrying out an action
* that removes any resources used up by the doAttack.
*/
fun apply(mob: Mob)
}
class ItemRequirement(val itemId: Int, val amount: Int = 1) : AttackRequirement {
override fun check(mob: Mob): AttackRequirementResult {
if (mob.inventory.getAmount(itemId) < amount) {
return Failed("You don't have enough items") //@todo -message
}
return Ok
}
override fun apply(mob: Mob) {
mob.inventory.remove(itemId, amount)
}
}
class AmmoRequirement(val ammoType: AmmoType, val amount: Int) : AttackRequirement {
override fun check(mob: Mob): AttackRequirementResult {
val weaponItem = mob.equipment[EquipmentConstants.WEAPON]
val weaponReqLevel = EquipmentDefinition.lookup(weaponItem.id).rangedLevel
val ammoItem = mob.equipment[EquipmentConstants.ARROWS]
val ammo = ammoType.items[ammoItem?.id]
if (ammoItem == null) {
return Failed("You have no ammo left in your quiver!")
}
if (amount > ammoItem.amount) {
return Failed("You don't have enough ammo left in your quiver!")
}
if (ammo == null || ammo.requiredLevel > weaponReqLevel) {
return Failed("You can't use this ammo with your current weapon.")
}
return Ok
}
override fun apply(mob: Mob) {
mob.equipment.removeSlot(EquipmentConstants.ARROWS, amount)
}
}
@@ -0,0 +1,17 @@
enum class AttackStyle {
Accurate,
Aggressive,
Defensive,
Controlled,
AltAggressive,
Rapid,
LongRanged
}
enum class AttackType {
Stab,
Slash,
Crush,
Magic,
Ranged
}
@@ -0,0 +1,21 @@
import org.apollo.game.model.Animation
import org.apollo.game.model.entity.Mob
object MeleeAttackFactory : AttackFactory {
override fun createAttack(speed: Int, range: Int, type: AttackType, style: AttackStyle, animation: Animation): Attack {
return MeleeAttack(speed, range, type, style, animation, mutableListOf())
}
}
class MeleeAttack(
speed: Int,
range: Int,
type: AttackType,
style: AttackStyle,
attackAnimation: Animation,
requirements: MutableList<AttackRequirement>
) : BasicAttack(speed, range, type, style, attackAnimation, requirements) {
override fun doAttack(source: Mob, target: Mob, output: AttackOutput) = output.hit()
}
@@ -0,0 +1,24 @@
import org.apollo.game.model.Animation
import org.apollo.game.model.entity.Mob
abstract class ProjectileAttack(
speed: Int,
range: Int,
type: AttackType,
style: AttackStyle,
attackAnimation: Animation,
requirements: MutableList<AttackRequirement>
) : BasicAttack(speed, range, type, style, attackAnimation, requirements) {
abstract fun projectile(mob: Mob): ProjectileTemplate
override fun doAttack(source: Mob, target: Mob, output: AttackOutput) {
val projectileTemplate = projectile(source)
val projectile = projectileTemplate.factory.invoke(source.world, source.position, target)
projectileTemplate.castGraphic?.let { source.playGraphic(it) }
source.world.spawn(projectile)
output.projectile(projectile)
}
}
@@ -0,0 +1,9 @@
import org.apollo.game.model.Graphic
//@todo - generalize for magic projectiles
data class ProjectileTemplate(
val factory: AmmoProjectileFactory,
val castGraphic: Graphic? = null,
val fumbleGraphic: Graphic? = null,
val hitGraphic: Graphic? = null
)
@@ -0,0 +1,30 @@
import org.apollo.game.model.Animation
import org.apollo.game.model.entity.EquipmentConstants
import org.apollo.game.model.entity.Mob
class RangeAttackFactory(val ammoType: AmmoType) : AttackFactory {
override fun createAttack(speed: Int, range: Int, type: AttackType, style: AttackStyle, animation: Animation): Attack {
return RangeAttack(speed, range, type, style, animation, ammoType)
}
}
class RangeAttack(
speed: Int,
range: Int,
type: AttackType,
style: AttackStyle,
attackAnimation: Animation,
private val ammoType: AmmoType
) : ProjectileAttack(speed, range, type, style, attackAnimation, mutableListOf()) {
init {
requirements.add(AmmoRequirement(ammoType, 1))
}
private fun ammo(mob: Mob): Ammo {
return mob.equipment[EquipmentConstants.ARROWS]?.let { ammoType.items[it.id] }
?: throw IllegalStateException("Couldn't find ammo entry for equipped item")
}
override fun projectile(mob: Mob) = ammo(mob).let { ProjectileTemplate(it.projectileFactory, it.attack) }
}
@@ -0,0 +1 @@
class SpecialAttack()
@@ -0,0 +1,47 @@
import org.apollo.game.action.Action
import org.apollo.game.model.entity.Mob
import org.apollo.game.model.entity.Player
import org.apollo.net.message.Message
class CombatAction<T : Mob>(mob: T) : Action<T>(0, true, mob) {
companion object {
/**
* Starts a [CombatAction] for the specified [Player], terminating the [Message] that triggered.
*/
fun start(message: Message, player: Player, target: Mob) {
player.combatState.target = target
player.interactingMob = target
player.turnTo(target.position)
player.startAction(CombatAction(player))
message.terminate()
}
}
override fun execute() {
val state = mob.combatState
val target = state.target
val attack = state.attack
if (target == null) {
stop()
return
}
if (!state.inRange()) {
// @todo - chase 'til in range
return
} else {
// @todo - chasing will prevent running closer than needed
mob.walkingQueue.clear()
}
if (!state.canAttack()) {
// @todo - idle - waiting to attack, do block animation
return
}
attack.execute(mob, target)
mob.combatAttackTick = mob.world.tick()
}
}
@@ -0,0 +1,46 @@
data class DamageBonuses(val stab: Int, val slash: Int, val crush: Int, val magic: Int, val range: Int)
data class CombatBonuses(
val attack: DamageBonuses,
val defence: DamageBonuses,
val meleeStrength: Int,
val rangedStrength: Int,
val prayer: Int
)
class CombatBonusesBuilder {
var meleeStrength = 0
var rangedStrength = 0
var prayer = 0
var attackBonuses = DamageBonuses(0, 0, 0, 0, 0)
var defenceBonuses = DamageBonuses(0, 0, 0, 0, 0)
fun attack(configurer: DamageBonusesBuilder.() -> Unit) {
val builder = DamageBonusesBuilder()
builder.configurer()
attackBonuses = builder.build()
}
fun defence(configurer: DamageBonusesBuilder.() -> Unit) {
val builder = DamageBonusesBuilder()
builder.configurer()
defenceBonuses = builder.build()
}
fun build(): CombatBonuses {
return CombatBonuses(attackBonuses, defenceBonuses, meleeStrength, rangedStrength, prayer)
}
}
class DamageBonusesBuilder(
var stab: Int = 0,
var slash: Int = 0,
var crush: Int = 0,
var magic: Int = 0,
var range: Int = 0
) {
fun build(): DamageBonuses {
return DamageBonuses(stab, slash, crush, magic, range)
}
}
@@ -0,0 +1,58 @@
import MobAttributeDelegators.attribute
import org.apollo.game.model.entity.EntityType
import org.apollo.game.model.entity.Mob
import org.apollo.game.model.entity.Npc
import org.apollo.game.model.entity.Player
import java.util.*
//@todo - CombatState interface and NpcCombatState/PlayerCombatState to better drive
// behaviours
object CombatStateManager {
private val states = WeakHashMap<Mob, CombatState>()
fun stateFor(mob: Mob): CombatState {
return states.computeIfAbsent(mob, {
val combatStyle = when (mob) {
is Player -> mob.weapon.weaponClass.styles[mob.combatStyle]
is Npc -> Weapons[null].weaponClass.styles[0] // @todo
else -> throw IllegalStateException("Invalid type: ${mob.javaClass.name}")
}
CombatState(it, combatStyle.attack)
})
}
fun remove(it: Mob) {
states.remove(it)
}
}
var Mob.combatStyle: Int by attribute("combat_style", 0)
var Mob.combatAttackTick: Long by attribute("combat_attack_tick", 0)
class CombatState(private val mob: Mob, var attack: Attack) {
var target: Mob? by WeakRefHolder()
fun ticksSinceAttack(): Long {
return mob.world.tick() - mob.combatAttackTick
}
fun inRange(): Boolean {
val target = this.target ?: return false
val distance = mob.position.getDistance(target.position)
val objectType = when (attack.type) {
AttackType.Ranged -> EntityType.PROJECTILE
else -> EntityType.NPC
}
return distance <= attack.range /* && mob.world.collisionManager.raycast(mob.position, target.position, objectType) */
}
fun canAttack(): Boolean {
return ticksSinceAttack() >= attack.speed
}
}
val Mob.combatState: CombatState
get() = CombatStateManager.stateFor(this)
@@ -4,55 +4,31 @@ 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>()
open class AmmoType(configurer: AmmoTypeBuilder.() -> Unit) {
val items : Map<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)
init {
val ammo = AmmoTypeBuilder(this.javaClass.simpleName)
.also(configurer)
.build()
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
)
items = HashMap(ammo)
}
}
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
data class AmmoEnchantment(
val graphic: Graphic,
val effect: AmmoEnchantmentEffect,
val chanceSupplier: AmmoEnchantmentChanceSupplier
)
data class Ammo(
val name: String,
val requiredLevel: Int,
val dropChance: Double,
val projectileFactory: AmmoProjectileFactory,
val attack: Graphic? = null,
val enchantment: AmmoEnchantment? = null
)
typealias AmmoEnchantmentEffect = Mob.() -> Unit
@@ -80,7 +56,7 @@ class AmmoGraphicsBuilder {
var attack: Int? = null
/**
* The graphic used when a [Mob] is hit with an enchanted ammo effect.
* The graphic used when a [Mob] is doAttack with an enchanted ammo effect.
*/
var enchanted: Int? = null
@@ -88,14 +64,14 @@ class AmmoGraphicsBuilder {
}
@AmmoDslMarker
class AmmoDropChance()
class AmmoDropChance
/**
* A DSL builder for an ammo's ranged level requirements.
*/
@AmmoDslMarker
class AmmoRequirementBuilder {
internal var level: Int = 1
var level: Int = 1
/**
* Set the ranged level required to use this ammo.
@@ -111,12 +87,12 @@ class AmmoBuilder(val name: String) {
/**
* The chance that ammo of this type will be dropped rather than broken.
*/
internal var dropChanceValue: Double = 1.0
var dropChanceValue: Double = 1.0
/**
* Internal value to hold a dummy [AmmoDropChance] field that can be used by the overriden `%` operator.
* value to hold a dummy [AmmoDropChance] field that can be used by the overriden `%` operator.
*/
internal val dropChance = AmmoDropChance()
val dropChance = AmmoDropChance()
/**
* `%` operator overload on [Int] for a fluent way to define an [Ammo]'s chance of dropping instead of breaking.
@@ -142,12 +118,12 @@ class AmmoBuilder(val name: String) {
}
/**
* A builder for effects applied to [Mob]'s hit with ammo that can be chanted.
* A builder for effects applied to [Mob]'s doAttack with ammo that can be chanted.
*/
@AmmoDslMarker
class AmmoEnchantmentBuilder {
internal var effect: AmmoEnchantmentEffect? = null
internal var chance: AmmoEnchantmentChanceSupplier? = null
var effect: AmmoEnchantmentEffect? = null
var chance: AmmoEnchantmentChanceSupplier? = null
/**
* Set the effect that will be applied to a [Mob] whenever this enchantment
@@ -231,17 +207,17 @@ class AmmoTypeBuilder(val name: String) {
/**
* The constraints on what weapon classes this ammo type can be fired from.
*/
internal val fired = AmmoTypeUseabilityBuilder()
val fired = AmmoTypeUseabilityBuilder()
/**
* The base projectile factory for projectiles of this ammo type.
*/
internal val projectile = AmmoProjectileFactoryBuilder()
val projectile = AmmoProjectileFactoryBuilder()
/**
* The variants of ammo that belong to this ammo type.
*/
internal val variants = mutableListOf<AmmoBuilder>()
val variants = mutableListOf<AmmoBuilder>()
/**
* String invocation overload that creates a new ammo variant
@@ -257,9 +233,9 @@ class AmmoTypeBuilder(val name: String) {
/**
* Build a list of ItemID -> [Ammo] pairs based on the variants in this [AmmoTypeBuilder].
*/
fun build(): List<Pair<Int, Ammo>> {
fun build(): Map<Int, Ammo> {
val ammoTypeSingular = name.removeSuffix("s")
val ammoList = mutableListOf<Pair<Int, Ammo>>()
val ammoMap = mutableMapOf<Int, Ammo>()
variants.forEach {
val requiredLevel = it.requires.level
@@ -267,27 +243,28 @@ class AmmoTypeBuilder(val name: String) {
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 attack = it.graphics.attack?.let({ Graphic(it, 0, 100) })
val ammoName = "${it.name} $ammoTypeSingular"
val ammo = Ammo(requiredLevel, dropChance, projectileFactory, attack)
val ammo = Ammo(name, requiredLevel, dropChance, projectileFactory, attack)
val ammoId = lookup_item(ammoName)?.id ?: throw RuntimeException("Unable to find ammo named $ammoName")
ammoList.add(Pair(ammoId, ammo))
ammoMap[ammoId] = ammo
val enchantment = it.graphics.enchanted?.let(::Graphic)
val enchantmentGraphic = it.graphics.enchanted?.let(::Graphic)
val enchantmentEffect = it.enchantment.effect
val enchantmentChanceSupplier = it.enchantment.chance
if (enchantment != null && enchantmentEffect != null && enchantmentChanceSupplier != null) {
if (enchantmentGraphic != null && enchantmentEffect != null && enchantmentChanceSupplier != null) {
val enchantment = AmmoEnchantment(enchantmentGraphic, enchantmentEffect, enchantmentChanceSupplier)
val enchantedAmmoName = "$ammoName (e)"
val enchantedAmmo = ammo.toEnchanted(enchantment, enchantmentEffect, enchantmentChanceSupplier)
val enchantedAmmo = Ammo(name, requiredLevel, dropChance, projectileFactory, attack, enchantment)
val enchantedAmmoId = lookup_item(enchantedAmmoName)?.id ?: throw RuntimeException("Unable to find ammo named $enchantedAmmoName")
ammoList.add(Pair(enchantedAmmoId, enchantedAmmo))
ammoMap[enchantedAmmoId] = enchantedAmmo
}
}
return ammoList
return ammoMap
}
}
@@ -0,0 +1,79 @@
import AttackStyle.*
import AttackType.Crush
import Weapons.createWeapon
import org.apollo.game.model.Animation
data class Weapon(val weaponClass: WeaponClass, val bonuses: CombatBonuses, val specialAttack: SpecialAttack? = null)
operator fun WeaponClass.invoke(name: String, configurer: WeaponBuilder.() -> Unit) {
val weaponItem = lookup_item(name) ?: throw IllegalArgumentException("Invalid weapon name: ${name}")
Weapons.weaponMap[weaponItem.id] = createWeapon(this, configurer)
}
object Weapons {
internal val weaponMap = mutableMapOf<Int, Weapon>()
operator fun get(itemId: Int?): Weapon {
return weaponMap[itemId] ?: defaultWeapon
}
internal fun createWeapon(weaponClass: WeaponClass, configurer: WeaponBuilder.() -> Unit = {}): Weapon {
return WeaponBuilder(weaponClass)
.also(configurer)
.build()
}
}
object Unarmed : MeleeWeaponClass({
widgetId = 5855
defaults {
attackSpeed = 4
attackType = Crush
attackAnimation = Animation(422)
}
Accurate {
button = 5860
}
Aggressive {
button = 5862
attackAnimation = Animation(423)
}
Defensive {
button = 5861
}
})
private val defaultWeapon = Weapons.createWeapon(Unarmed)
class WeaponBuilder(private val weaponClass: WeaponClass) {
private val combatBonusesBuilder = CombatBonusesBuilder()
var specialAttack: SpecialAttack? = null
var meleeStrength: Int
get() = combatBonusesBuilder.meleeStrength
set(value) {
combatBonusesBuilder.meleeStrength = value
}
var rangedStrength: Int
get() = combatBonusesBuilder.rangedStrength
set(value) {
combatBonusesBuilder.rangedStrength = value
}
var prayer: Int
get() = combatBonusesBuilder.prayer
set(value) {
combatBonusesBuilder.prayer = value
}
fun attackBonuses(configurer: DamageBonusesBuilder.() -> Unit) = this.combatBonusesBuilder.attack(configurer)
fun defenceBonuses(configurer: DamageBonusesBuilder.() -> Unit) = this.combatBonusesBuilder.defence(configurer)
fun build() = Weapon(weaponClass, combatBonusesBuilder.build(), specialAttack)
}
@@ -0,0 +1,96 @@
import org.apollo.game.model.Animation
data class SpecialBar(val button: Int, val configId: Int)
data class WeaponClassDetails(val widget: Int, val specialBar: SpecialBar?, val styles: List<WeaponClassStyle>)
data class WeaponClassStyle(val button: Int, val configId: Int, val attackStyle: AttackStyle, val attack: Attack, val blockAnimation: Animation?)
typealias WeaponClassConfigurer = WeaponClassDetailsBuilder.() -> Unit
open class WeaponClass(attackFactory: AttackFactory, detailsBuilder: WeaponClassConfigurer) {
val widget: Int
val specialBar: SpecialBar?
val styles: Array<WeaponClassStyle>
init {
val details = WeaponClassDetailsBuilder(attackFactory)
.also(detailsBuilder)
.build()
widget = details.widget
specialBar = details.specialBar
styles = details.styles.toTypedArray()
}
}
class WeaponClassDetailsBuilder(private val attackFactory: AttackFactory) {
private var configureStyleDefaults: WeaponClassStyleBuilder.() -> Unit = {}
private var specialBar: SpecialBar? = null
private val styles = mutableListOf<WeaponClassStyle>()
var widgetId: Int? = null
fun defaults(configurer: WeaponClassStyleBuilder.() -> Unit) {
configureStyleDefaults = configurer
}
fun specialBar(configurer: SpecialBarBuilder.() -> Unit) {
specialBar = SpecialBarBuilder().also(configurer).build()
}
operator fun AttackStyle.invoke(configurer: WeaponClassStyleBuilder.() -> Unit) {
val styleBuilder = WeaponClassStyleBuilder(this)
.also(configureStyleDefaults)
.also(configurer)
styles.add(styleBuilder.build(attackFactory))
}
fun build(): WeaponClassDetails {
return WeaponClassDetails(
widgetId ?: throw IllegalStateException("Weapon class widget id is required"),
specialBar,
styles
)
}
}
class WeaponClassStyleBuilder(val attackStyle: AttackStyle) {
var button: Int? = null
var configId: Int? = null
var attackRange: Int? = 1
var attackSpeed: Int? = null
var attackType: AttackType? = null
var attackAnimation: Animation? = null
var blockAnimation: Animation? = null
var attack: Attack? = null
fun build(attackFactory: AttackFactory): WeaponClassStyle {
val attack = this.attack ?: attackFactory.createAttack(
attackSpeed ?: throw IllegalStateException("BasicAttack speed is required"),
attackRange ?: throw IllegalStateException("BasicAttack range is required"),
attackType ?: throw IllegalStateException("BasicAttack type is required"),
attackStyle,
attackAnimation ?: throw IllegalStateException("BasicAttack animation is required")
)
return WeaponClassStyle(
button ?: throw IllegalStateException("Combat style button is required"),
0,
attackStyle,
attack,
blockAnimation
)
}
}
class SpecialBarBuilder {
var button: Int? = null
var configId: Int? = null
fun build(): SpecialBar {
return SpecialBar(
button ?: throw IllegalStateException("No button configured for special bar"),
configId ?: throw IllegalStateException("No config id configured for special bar")
)
}
}
@@ -0,0 +1,2 @@
abstract class MeleeWeaponClass(detailsBuilder: WeaponClassConfigurer) :
WeaponClass(MeleeAttackFactory, detailsBuilder)
@@ -0,0 +1,2 @@
abstract class RangedWeaponClass(ammoType: AmmoType, detailsBuilder: WeaponClassConfigurer)
: WeaponClass(RangeAttackFactory(ammoType), detailsBuilder)
@@ -0,0 +1,14 @@
import org.apollo.game.model.entity.EquipmentConstants
import org.apollo.game.model.entity.Mob
val Mob.weapon: Weapon
get() = Weapons[this.equipment[EquipmentConstants.WEAPON]?.id]
fun scale(oldScale: Pair<Double, Double>, newScale: Pair<Double, Double>, value: Double): Int {
val oldMin = oldScale.first
val oldMax = oldScale.second
val newMin = newScale.first
val newMax = newScale.second
return Math.round((newMax - newMin) * (value - oldMin) / (oldMax - oldMin) + newMin).toInt();
}
+18
View File
@@ -0,0 +1,18 @@
[x] combat timing
[x] combat tab ui
[x] attack requirements
[x] melee attacks
[x] projectile attacks
[x] max hit calculation
[x] applying damage rolls
[x] attack vs defence (accuracy) checks for ranged/melee
[ ] weapon posture animations
[ ] playing block animations
[ ] special attacks
[ ] bonus calculations / attack modifiers + buffs
[ ] death checks / loot
[ ] persistent attack timer
[ ] chasing
[ ] equipment tab ui
[ ] magic combat
[ ] blocking logout
@@ -0,0 +1,47 @@
import org.apollo.game.message.impl.*
import org.apollo.game.model.entity.EquipmentConstants
import org.apollo.game.model.entity.Player
import org.apollo.game.model.event.impl.LoginEvent
import org.apollo.game.model.inv.SynchronizationInventoryListener
on_player_event { LoginEvent::class }.then { updateCombatTab(player) }
on { ItemOptionMessage::class }
.where { interfaceId == SynchronizationInventoryListener.INVENTORY_ID && option == 2 }
.then { updateCombatTab(it) }
on { ItemActionMessage::class }
.where { interfaceId == SynchronizationInventoryListener.EQUIPMENT_ID && slot == EquipmentConstants.WEAPON }
.then { updateCombatTab(it) }
on { ButtonMessage::class }
.then {
val weapon = it.weapon
val weaponClass = weapon.weaponClass
val newCombatStyle = weaponClass.styles.firstOrNull { it.button == widgetId } ?: return@then
it.combatStyle = weaponClass.styles.indexOf(newCombatStyle)
it.combatState.attack = newCombatStyle.attack
}
fun updateCombatTab(player: Player) {
val weaponItem = player.equipment[EquipmentConstants.WEAPON]
val weaponName = weaponItem?.definition?.name ?: "Unarmed"
val weapon = Weapons[weaponItem?.id]
val weaponClass = weapon.weaponClass
var widget = weaponClass.widget
player.send(SwitchTabInterfaceMessage(0, widget))
if (weaponItem != null) {
player.send(SetWidgetItemModelMessage(++widget, weaponItem.id, 200))
}
/*
if weapon_class.special_bar?
player.send SetWidgetVisibilityMessage.new(weapon_class.special_bar_config, weapon.special_attack?)
end
*/
player.send(SetWidgetTextMessage(widget + 2, weaponName))
player.send(ConfigMessage(43, player.combatStyle)) //@todo - combat style offset
}
+24 -47
View File
@@ -1,13 +1,9 @@
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
// @fixme
class AmmoDslTests {
private val TEST_AMMO_ID : Int = 0
@@ -18,62 +14,43 @@ class AmmoDslTests {
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)
//
// 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)
// 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)
// 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)
// val ammo = AMMO[TEST_AMMO_ID]!!
//
// assertEquals(20, ammo.attack?.id)
}
}