diff --git a/.gitignore b/.gitignore
index c852a9f9b..403765d89 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
.*
+!.idea/
!.gitignore
!.rubocop.yml
!.editorconfig
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 000000000..cc1164cca
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,4 @@
+*
+
+!.gitignore
+!kotlinc.xml
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 000000000..a2e87d82d
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/org/apollo/build/plugin/ApolloPluginExtension.groovy b/buildSrc/src/main/groovy/org/apollo/build/plugin/ApolloPluginExtension.groovy
index ea4c910e4..297007310 100644
--- a/buildSrc/src/main/groovy/org/apollo/build/plugin/ApolloPluginExtension.groovy
+++ b/buildSrc/src/main/groovy/org/apollo/build/plugin/ApolloPluginExtension.groovy
@@ -2,9 +2,7 @@ package org.apollo.build.plugin
import org.apollo.build.plugin.tasks.ApolloScriptCompileTask
import org.gradle.api.Project
-import org.gradle.api.Task
import org.gradle.api.file.FileTree
-import org.gradle.api.tasks.testing.Test
class ApolloPluginExtension {
final Project project
@@ -79,7 +77,7 @@ class ApolloPluginExtension {
main {
kotlin {
srcDir this.srcDir
- exclude '*.kts'
+ exclude '**/*.kts'
}
}
@@ -98,7 +96,7 @@ class ApolloPluginExtension {
def buildTask = project.tasks['classes']
FileTree scripts = project.fileTree(srcDir).matching {
- include '*.kts'
+ include '**/*.kts'
}
project.tasks.create('compileScripts', ApolloScriptCompileTask) {
diff --git a/game/plugin/api/src/org/apollo/game/plugins/api/attributes.kt b/game/plugin/api/src/org/apollo/game/plugins/api/attributes.kt
new file mode 100644
index 000000000..ee1dfd786
--- /dev/null
+++ b/game/plugin/api/src/org/apollo/game/plugins/api/attributes.kt
@@ -0,0 +1,45 @@
+import org.apollo.game.model.entity.Mob
+import org.apollo.game.model.entity.attr.*
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty
+
+object MobAttributeDelegators {
+ inline fun attribute(name: String, defaultValue: T): MobAttributeDelegate {
+ return MobAttributeDelegate(name, T::class, defaultValue, false)
+ }
+
+ inline fun persistentAttribute(name: String, defaultValue: T): MobAttributeDelegate {
+ return MobAttributeDelegate(name, T::class, defaultValue, true)
+ }
+
+}
+
+class MobAttributeDelegate(val name: String, val type: KClass, val defaultValue: T, persistent: Boolean = true) {
+ init {
+ val attrType = when {
+ type == Int::class || type == Long::class -> AttributeType.LONG
+ type == Boolean::class -> AttributeType.BOOLEAN
+ type == Double::class -> AttributeType.DOUBLE
+ type == String::class -> AttributeType.STRING
+ else -> throw IllegalArgumentException("Can only store primitive attributes, not: ${type.qualifiedName}")
+ }
+
+ val definition = AttributeDefinition(defaultValue, if (persistent) AttributePersistence.PERSISTENT else AttributePersistence.TRANSIENT, attrType)
+ AttributeMap.define(name, definition)
+ }
+
+ operator fun getValue(thisRef: Mob, property: KProperty<*>): T {
+ return thisRef.attributes[name]?.value as T? ?: defaultValue
+ }
+
+ operator fun setValue(thisRef: Mob, property: KProperty<*>, value: T) {
+ val attr = when {
+ type == Double::class || type == Int::class || type == Long::class -> NumericalAttribute(value as Number)
+ type == Boolean::class -> BooleanAttribute(value as Boolean)
+ type == String::class -> StringAttribute(value as String, false)
+ else -> throw IllegalArgumentException("Can only store primitive attributes, not: ${type.qualifiedName}")
+ }
+
+ thisRef.setAttribute(name, attr)
+ }
+}
diff --git a/game/plugin/api/src/org/apollo/game/plugins/api/player.kt b/game/plugin/api/src/org/apollo/game/plugins/api/player.kt
new file mode 100644
index 000000000..98e25c928
--- /dev/null
+++ b/game/plugin/api/src/org/apollo/game/plugins/api/player.kt
@@ -0,0 +1,28 @@
+package org.apollo.game.plugins.api
+
+import org.apollo.game.model.entity.Mob
+import org.apollo.game.model.entity.Skill
+import org.apollo.game.model.entity.SkillSet
+
+val Mob.skills: SkillSet get() = skillSet
+val SkillSet.attack: Skill get() = getSkill(Skill.ATTACK)
+val SkillSet.defence: Skill get() = getSkill(Skill.DEFENCE)
+val SkillSet.strength: Skill get() = getSkill(Skill.STRENGTH)
+val SkillSet.hitpoints: Skill get() = getSkill(Skill.HITPOINTS)
+val SkillSet.ranged: Skill get() = getSkill(Skill.RANGED)
+val SkillSet.prayer: Skill get() = getSkill(Skill.PRAYER)
+val SkillSet.magic: Skill get() = getSkill(Skill.MAGIC)
+val SkillSet.cooking: Skill get() = getSkill(Skill.COOKING)
+val SkillSet.woodcutting: Skill get() = getSkill(Skill.WOODCUTTING)
+val SkillSet.fletching: Skill get() = getSkill(Skill.FLETCHING)
+val SkillSet.fishing: Skill get() = getSkill(Skill.FISHING)
+val SkillSet.firemaking: Skill get() = getSkill(Skill.FIREMAKING)
+val SkillSet.crafting: Skill get() = getSkill(Skill.CRAFTING)
+val SkillSet.smithing: Skill get() = getSkill(Skill.SMITHING)
+val SkillSet.mining: Skill get() = getSkill(Skill.MINING)
+val SkillSet.herblore: Skill get() = getSkill(Skill.HERBLORE)
+val SkillSet.agility: Skill get() = getSkill(Skill.AGILITY)
+val SkillSet.thieving: Skill get() = getSkill(Skill.THIEVING)
+val SkillSet.slayer: Skill get() = getSkill(Skill.SLAYER)
+val SkillSet.farming: Skill get() = getSkill(Skill.FARMING)
+val SkillSet.runecraft: Skill get() = getSkill(Skill.RUNECRAFT)
diff --git a/game/plugin/api/src/util.kt b/game/plugin/api/src/util.kt
index 01264cbfe..dd80ef59b 100644
--- a/game/plugin/api/src/util.kt
+++ b/game/plugin/api/src/util.kt
@@ -1,9 +1,21 @@
-package org.apollo.game.plugin.api
-
-import java.util.Random
+import java.lang.ref.WeakReference
+import java.util.*
+import kotlin.reflect.KProperty
val RAND = Random()
fun rand(bounds: Int): Int {
return RAND.nextInt(bounds)
-}
\ No newline at end of file
+}
+
+class WeakRefHolder(private var _value: WeakReference = WeakReference(null)) {
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
+ return _value.get()
+ }
+
+ operator fun setValue(
+ thisRef: Any?, property: KProperty<*>, value: T?
+ ) {
+ _value = WeakReference(value)
+ }
+}
diff --git a/game/plugin/entity/combat/build.gradle b/game/plugin/entity/combat/build.gradle
new file mode 100644
index 000000000..fe9c71f44
--- /dev/null
+++ b/game/plugin/entity/combat/build.gradle
@@ -0,0 +1,11 @@
+plugin {
+ name = "combat"
+ description = "Extensible framework for combat."
+ authors = [
+ "Gary Tierney "
+ ]
+ dependencies = [
+ "util:lookup",
+ "api"
+ ]
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/combat.plugin.kts b/game/plugin/entity/combat/src/combat.plugin.kts
new file mode 100644
index 000000000..7cbcf5753
--- /dev/null
+++ b/game/plugin/entity/combat/src/combat.plugin.kts
@@ -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])
+ }
diff --git a/game/plugin/entity/combat/src/equipment/ammo/arrows.kt b/game/plugin/entity/combat/src/equipment/ammo/arrows.kt
new file mode 100644
index 000000000..9ff35e8aa
--- /dev/null
+++ b/game/plugin/entity/combat/src/equipment/ammo/arrows.kt
@@ -0,0 +1,71 @@
+object Arrows : AmmoType({
+ fired from "shortbows" and "longbows"
+
+ projectile {
+ startHeight = 41
+ endHeight = 37
+ delay = 41
+ lifetime = 60
+ 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/src/equipment/weapons/melee/scimitars.plugin.kts b/game/plugin/entity/combat/src/equipment/weapons/melee/scimitars.plugin.kts
new file mode 100644
index 000000000..2f1ca47e1
--- /dev/null
+++ b/game/plugin/entity/combat/src/equipment/weapons/melee/scimitars.plugin.kts
@@ -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
+}
diff --git a/game/plugin/entity/combat/src/equipment/weapons/melee_classes.kt b/game/plugin/entity/combat/src/equipment/weapons/melee_classes.kt
new file mode 100644
index 000000000..e0f61b8f5
--- /dev/null
+++ b/game/plugin/entity/combat/src/equipment/weapons/melee_classes.kt
@@ -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 }
+})
diff --git a/game/plugin/entity/combat/src/equipment/weapons/ranged/bows.plugin.kts b/game/plugin/entity/combat/src/equipment/weapons/ranged/bows.plugin.kts
new file mode 100644
index 000000000..33ee19a51
--- /dev/null
+++ b/game/plugin/entity/combat/src/equipment/weapons/ranged/bows.plugin.kts
@@ -0,0 +1,3 @@
+Bow("Shortbow") {
+
+}
diff --git a/game/plugin/entity/combat/src/equipment/weapons/ranged_classes.kt b/game/plugin/entity/combat/src/equipment/weapons/ranged_classes.kt
new file mode 100644
index 000000000..4bc815696
--- /dev/null
+++ b/game/plugin/entity/combat/src/equipment/weapons/ranged_classes.kt
@@ -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
+ }
+})
diff --git a/game/plugin/entity/combat/src/framework/attack/attack.kt b/game/plugin/entity/combat/src/framework/attack/attack.kt
new file mode 100644
index 000000000..0dceb956e
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/attack.kt
@@ -0,0 +1,81 @@
+import org.apollo.game.model.Animation
+import org.apollo.game.model.entity.Mob
+import org.apollo.game.model.entity.Player
+
+
+abstract class Attack(
+ val speed: Int,
+ val range: Int,
+ val type: AttackType,
+ val style: AttackStyle,
+ private val attackAnimation: Animation,
+ protected val requirements: MutableList
+) {
+ /**
+ * 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
+ }
+ }
+
+ val maxHitRoll = calculateBasicMaxRoll(RollType.Attack, source)
+ val maxDefenceRoll = calculateBasicMaxRoll(RollType.Defence, target)
+
+ 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 effective strength 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
+) : Attack(speed, range, type, style, attackAnimation, requirements) {
+ override fun maxDamage(source: Mob): Int {
+ return calculateBasicMaxRoll(RollType.Damage, source)
+ }
+}
diff --git a/game/plugin/entity/combat/src/framework/attack/attack_damage.kt b/game/plugin/entity/combat/src/framework/attack/attack_damage.kt
new file mode 100644
index 000000000..2e46f549c
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/attack_damage.kt
@@ -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()
+
+ 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))
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/attack/attack_effect.kt b/game/plugin/entity/combat/src/framework/attack/attack_effect.kt
new file mode 100644
index 000000000..c3b4c5222
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/attack_effect.kt
@@ -0,0 +1,3 @@
+import org.apollo.game.model.Animation
+import org.apollo.game.model.Graphic
+
diff --git a/game/plugin/entity/combat/src/framework/attack/attack_factory.kt b/game/plugin/entity/combat/src/framework/attack/attack_factory.kt
new file mode 100644
index 000000000..69f0e8308
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/attack_factory.kt
@@ -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
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/attack/attack_requirement.kt b/game/plugin/entity/combat/src/framework/attack/attack_requirement.kt
new file mode 100644
index 000000000..81beefc9b
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/attack_requirement.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/attack/attack_type.kt b/game/plugin/entity/combat/src/framework/attack/attack_type.kt
new file mode 100644
index 000000000..c886999a8
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/attack_type.kt
@@ -0,0 +1,17 @@
+enum class AttackStyle(val attackBonus: Int = 0, val defenceBonus: Int = 0, val strengthBonus: Int = 0) {
+ Accurate(attackBonus = 3, strengthBonus = 3),
+ Aggressive(strengthBonus = 3),
+ Defensive(defenceBonus = 3),
+ Controlled(attackBonus = 1, strengthBonus = 1, defenceBonus = 1),
+ AltAggressive(strengthBonus = 3),
+ Rapid,
+ LongRanged(attackBonus = 1,strengthBonus = 1, defenceBonus = 3)
+}
+
+enum class AttackType {
+ Stab,
+ Slash,
+ Crush,
+ Magic,
+ Ranged
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/attack/attack_utils.kt b/game/plugin/entity/combat/src/framework/attack/attack_utils.kt
new file mode 100644
index 000000000..c37411250
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/attack_utils.kt
@@ -0,0 +1,74 @@
+import AttackType.Magic
+import AttackType.Ranged
+import CombatBonus.MeleeStrength
+import CombatBonus.RangedStrength
+import org.apollo.game.model.entity.Mob
+import org.apollo.game.plugins.api.*
+
+enum class RollType {
+ Attack,
+ Defence,
+ Damage
+}
+
+fun calculateBasicMaxRoll(rollType: RollType, mob: Mob, modifiers: List = emptyList()): Int {
+ val style = mob.combatState.attack.style
+ val attackType = mob.combatState.attack.type
+
+ if (attackType == Magic) {
+ throw IllegalStateException("Basic roll calculator called for a magic attack. Unsupported")
+ }
+
+ val styleBonus = when (rollType) {
+ RollType.Attack -> style.attackBonus
+ RollType.Defence -> style.defenceBonus
+ RollType.Damage -> {
+ if (style == AttackStyle.Accurate && attackType != Ranged) {
+ 0
+ } else {
+ style.strengthBonus
+ }
+ }
+ }
+
+ val baseSkill = when (rollType) {
+ RollType.Attack -> {
+ if (attackType == Ranged) {
+ mob.skills.ranged
+ } else {
+ mob.skills.attack
+ }
+ }
+ RollType.Defence -> mob.skills.defence
+ RollType.Damage -> {
+ if (attackType == Ranged) {
+ mob.skills.ranged
+ } else {
+ mob.skills.strength
+ }
+ }
+ }
+
+ val equipmentBonuses = mob.combatState.bonuses
+ val equipmentBonus = when (rollType) {
+ RollType.Attack -> equipmentBonuses.attack[attackType]
+ RollType.Defence -> equipmentBonuses.defence[attackType]
+ RollType.Damage -> {
+ if (attackType == Ranged) {
+ equipmentBonuses[RangedStrength]
+ } else {
+ equipmentBonuses[MeleeStrength]
+ }
+ }
+ }
+
+ val modifier = modifiers.reduce { a, b -> a + b }
+ val effectiveLevel = baseSkill.currentLevel * modifier + styleBonus + 8
+ val maxRoll = if (rollType == RollType.Damage) {
+ 0.5 + effectiveLevel * (equipmentBonus + 64) / 640
+ } else {
+ effectiveLevel * (equipmentBonus + 64)
+ }
+
+ return maxRoll.toInt()
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/attack/melee_attack.kt b/game/plugin/entity/combat/src/framework/attack/melee_attack.kt
new file mode 100644
index 000000000..0efd38472
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/melee_attack.kt
@@ -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
+) : BasicAttack(speed, range, type, style, attackAnimation, requirements) {
+
+ override fun doAttack(source: Mob, target: Mob, output: AttackOutput) = output.hit()
+
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/attack/projectile_attack.kt b/game/plugin/entity/combat/src/framework/attack/projectile_attack.kt
new file mode 100644
index 000000000..c05a1f80e
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/projectile_attack.kt
@@ -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
+) : 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)
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/attack/projectile_template.kt b/game/plugin/entity/combat/src/framework/attack/projectile_template.kt
new file mode 100644
index 000000000..976d34caa
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/projectile_template.kt
@@ -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
+)
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/attack/range_attack.kt b/game/plugin/entity/combat/src/framework/attack/range_attack.kt
new file mode 100644
index 000000000..e453e457f
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/range_attack.kt
@@ -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) }
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/attack/special_attack.kt b/game/plugin/entity/combat/src/framework/attack/special_attack.kt
new file mode 100644
index 000000000..b5891061f
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/attack/special_attack.kt
@@ -0,0 +1 @@
+class SpecialAttack()
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/combat.kt b/game/plugin/entity/combat/src/framework/combat.kt
new file mode 100644
index 000000000..019be7c1c
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/combat.kt
@@ -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(mob: T) : Action(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()
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/combat_bonuses.kt b/game/plugin/entity/combat/src/framework/combat_bonuses.kt
new file mode 100644
index 000000000..b5d81f100
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/combat_bonuses.kt
@@ -0,0 +1,74 @@
+enum class CombatBonus {
+ MeleeStrength,
+ RangedStrength,
+ Prayer
+}
+
+data class AttackBonuses(private val bonuses: Map) {
+ companion object {
+ fun default() = AttackBonusesBuilder().build()
+ }
+
+ operator fun get(key: AttackType): Int = bonuses[key]!!
+}
+
+data class CombatBonuses(
+ val attack: AttackBonuses,
+ val defence: AttackBonuses,
+ private val combatBonuses: Map
+) {
+ companion object {
+ fun default() = CombatBonusesBuilder().build()
+ }
+
+ operator fun get(key: CombatBonus): Int = combatBonuses[key]!!
+}
+
+class CombatBonusesBuilder {
+ var meleeStrength = 0
+ var rangedStrength = 0
+ var prayer = 0
+ var attackBonuses = AttackBonuses.default()
+ var defenceBonuses = AttackBonuses.default()
+
+ fun attack(configurer: AttackBonusesBuilder.() -> Unit) {
+ val builder = AttackBonusesBuilder()
+ builder.configurer()
+
+ attackBonuses = builder.build()
+ }
+
+ fun defence(configurer: AttackBonusesBuilder.() -> Unit) {
+ val builder = AttackBonusesBuilder()
+ builder.configurer()
+
+ defenceBonuses = builder.build()
+ }
+
+ fun build(): CombatBonuses {
+ return CombatBonuses(attackBonuses, defenceBonuses, mapOf(
+ CombatBonus.MeleeStrength to meleeStrength,
+ CombatBonus.RangedStrength to rangedStrength,
+ CombatBonus.Prayer to prayer
+ ))
+ }
+}
+
+class AttackBonusesBuilder(
+ var stab: Int = 0,
+ var slash: Int = 0,
+ var crush: Int = 0,
+ var magic: Int = 0,
+ var range: Int = 0
+) {
+
+ fun build(): AttackBonuses {
+ return AttackBonuses(mapOf(
+ AttackType.Stab to stab,
+ AttackType.Slash to slash,
+ AttackType.Crush to crush,
+ AttackType.Magic to magic,
+ AttackType.Ranged to range
+ ))
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/combat_state.kt b/game/plugin/entity/combat/src/framework/combat_state.kt
new file mode 100644
index 000000000..c5ce78eef
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/combat_state.kt
@@ -0,0 +1,59 @@
+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()
+
+ 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()
+ var bonuses = CombatBonuses.default()
+
+ 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)
+
diff --git a/game/plugin/entity/combat/src/framework/equipment/ammo.kt b/game/plugin/entity/combat/src/framework/equipment/ammo.kt
new file mode 100644
index 000000000..caa9db18d
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/equipment/ammo.kt
@@ -0,0 +1,270 @@
+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
+
+open class AmmoType(configurer: AmmoTypeBuilder.() -> Unit) {
+ val items : Map
+
+ init {
+ val ammo = AmmoTypeBuilder(this.javaClass.simpleName)
+ .also(configurer)
+ .build()
+
+ items = HashMap(ammo)
+ }
+}
+
+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
+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 doAttack 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 {
+ 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.
+ */
+ var dropChanceValue: Double = 1.0
+
+ /**
+ * value to hold a dummy [AmmoDropChance] field that can be used by the overriden `%` operator.
+ */
+ 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 doAttack with ammo that can be chanted.
+ */
+@AmmoDslMarker
+class AmmoEnchantmentBuilder {
+ var effect: AmmoEnchantmentEffect? = null
+ 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.
+ */
+ val fired = AmmoTypeUseabilityBuilder()
+
+ /**
+ * The base projectile factory for projectiles of this ammo type.
+ */
+ val projectile = AmmoProjectileFactoryBuilder()
+
+ /**
+ * The variants of ammo that belong to this ammo type.
+ */
+ 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(): Map {
+ val ammoTypeSingular = name.removeSuffix("s")
+ val ammoMap = mutableMapOf()
+
+ 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(it, 0, 100) })
+
+ val ammoName = "${it.name} $ammoTypeSingular"
+ val ammo = Ammo(name, requiredLevel, dropChance, projectileFactory, attack)
+ val ammoId = lookup_item(ammoName)?.id ?: throw RuntimeException("Unable to find ammo named $ammoName")
+
+ ammoMap[ammoId] = ammo
+
+ val enchantmentGraphic = it.graphics.enchanted?.let(::Graphic)
+ val enchantmentEffect = it.enchantment.effect
+ val enchantmentChanceSupplier = it.enchantment.chance
+
+ if (enchantmentGraphic != null && enchantmentEffect != null && enchantmentChanceSupplier != null) {
+ val enchantment = AmmoEnchantment(enchantmentGraphic, enchantmentEffect, enchantmentChanceSupplier)
+ val enchantedAmmoName = "$ammoName (e)"
+ val enchantedAmmo = Ammo(name, requiredLevel, dropChance, projectileFactory, attack, enchantment)
+ val enchantedAmmoId = lookup_item(enchantedAmmoName)?.id ?: throw RuntimeException("Unable to find ammo named $enchantedAmmoName")
+
+ ammoMap[enchantedAmmoId] = enchantedAmmo
+ }
+ }
+
+ return ammoMap
+ }
+}
diff --git a/game/plugin/entity/combat/src/framework/equipment/weapon.kt b/game/plugin/entity/combat/src/framework/equipment/weapon.kt
new file mode 100644
index 000000000..97eb84499
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/equipment/weapon.kt
@@ -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()
+
+ 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: AttackBonusesBuilder.() -> Unit) = this.combatBonusesBuilder.attack(configurer)
+ fun defenceBonuses(configurer: AttackBonusesBuilder.() -> Unit) = this.combatBonusesBuilder.defence(configurer)
+
+ fun build() = Weapon(weaponClass, combatBonusesBuilder.build(), specialAttack)
+}
diff --git a/game/plugin/entity/combat/src/framework/equipment/weapon_class.kt b/game/plugin/entity/combat/src/framework/equipment/weapon_class.kt
new file mode 100644
index 000000000..06393d673
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/equipment/weapon_class.kt
@@ -0,0 +1,95 @@
+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)
+data class WeaponClassStyle(val button: 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
+
+ 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()
+
+ 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"),
+ 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")
+ )
+ }
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/equipment/weapons/melee.kt b/game/plugin/entity/combat/src/framework/equipment/weapons/melee.kt
new file mode 100644
index 000000000..35f50e39f
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/equipment/weapons/melee.kt
@@ -0,0 +1,2 @@
+abstract class MeleeWeaponClass(detailsBuilder: WeaponClassConfigurer) :
+ WeaponClass(MeleeAttackFactory, detailsBuilder)
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/equipment/weapons/ranged.kt b/game/plugin/entity/combat/src/framework/equipment/weapons/ranged.kt
new file mode 100644
index 000000000..96a45669c
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/equipment/weapons/ranged.kt
@@ -0,0 +1,2 @@
+abstract class RangedWeaponClass(ammoType: AmmoType, detailsBuilder: WeaponClassConfigurer)
+ : WeaponClass(RangeAttackFactory(ammoType), detailsBuilder)
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/framework/utils.kt b/game/plugin/entity/combat/src/framework/utils.kt
new file mode 100644
index 000000000..0c8d08d31
--- /dev/null
+++ b/game/plugin/entity/combat/src/framework/utils.kt
@@ -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, newScale: Pair, 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();
+}
\ No newline at end of file
diff --git a/game/plugin/entity/combat/src/todo.txt b/game/plugin/entity/combat/src/todo.txt
new file mode 100644
index 000000000..115faf127
--- /dev/null
+++ b/game/plugin/entity/combat/src/todo.txt
@@ -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
diff --git a/game/plugin/entity/combat/src/ui/combat_tab.plugin.kts b/game/plugin/entity/combat/src/ui/combat_tab.plugin.kts
new file mode 100644
index 000000000..505eca66d
--- /dev/null
+++ b/game/plugin/entity/combat/src/ui/combat_tab.plugin.kts
@@ -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
+}
diff --git a/game/plugin/entity/combat/test/AmmoDslTests.kt b/game/plugin/entity/combat/test/AmmoDslTests.kt
new file mode 100644
index 000000000..2dd405ac3
--- /dev/null
+++ b/game/plugin/entity/combat/test/AmmoDslTests.kt
@@ -0,0 +1,56 @@
+
+import org.apollo.cache.def.ItemDefinition
+import org.junit.Before
+import org.junit.Test
+
+// @fixme
+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))
+ }
+
+ @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
diff --git a/game/src/main/java/org/apollo/game/model/World.java b/game/src/main/java/org/apollo/game/model/World.java
index 991a41fc6..3473eb8cf 100644
--- a/game/src/main/java/org/apollo/game/model/World.java
+++ b/game/src/main/java/org/apollo/game/model/World.java
@@ -66,6 +66,11 @@ public enum RegistrationStatus {
}
+ /**
+ * A counter for the number of ticks ran.
+ */
+ private long tickCounter = 0;
+
/**
* The logger for this class.
*/
@@ -208,6 +213,15 @@ public int getReleaseNumber() {
return releaseNumber;
}
+ /**
+ * Get the current value of the {@link #tickCounter} (the number of ticks since the game started).
+ *
+ * @return The current value of the tick counter;
+ */
+ public long tick() {
+ return tickCounter;
+ }
+
/**
* Initialises the world by loading definitions from the specified file
* system.
@@ -277,6 +291,7 @@ public void pulse() {
unregisterNpcs();
registerNpcs();
scheduler.pulse();
+ tickCounter++;
}
/**
diff --git a/game/src/main/java/org/apollo/game/model/area/Region.java b/game/src/main/java/org/apollo/game/model/area/Region.java
index df7a9a170..0f2693366 100644
--- a/game/src/main/java/org/apollo/game/model/area/Region.java
+++ b/game/src/main/java/org/apollo/game/model/area/Region.java
@@ -400,7 +400,7 @@ private void checkPosition(Position position) {
*/
private void record(T entity, EntityUpdateType update) {
UpdateOperation> operation = entity.toUpdateOperation(this, update);
- RegionUpdateMessage message = operation.toMessage(), inverse = operation.inverse();
+ RegionUpdateMessage message = operation.toMessage();
int height = entity.getPosition().getHeight();
Set updates = this.updates.get(height);
@@ -411,12 +411,12 @@ private void record(T entity, EntityUpdateT
if (update == EntityUpdateType.REMOVE) {
removedObjects.get(height).add(message);
} else { // TODO should this really be possible?
- removedObjects.get(height).remove(inverse);
+ removedObjects.get(height).remove(operation.inverse());
}
}
if (update == EntityUpdateType.REMOVE && !type.isTransient()) {
- updates.remove(inverse);
+ updates.remove(operation.inverse());
}
updates.add(message);
diff --git a/game/src/main/java/org/apollo/game/model/area/collision/CollisionManager.java b/game/src/main/java/org/apollo/game/model/area/collision/CollisionManager.java
index 4b54b36ac..10d02dabe 100644
--- a/game/src/main/java/org/apollo/game/model/area/collision/CollisionManager.java
+++ b/game/src/main/java/org/apollo/game/model/area/collision/CollisionManager.java
@@ -145,14 +145,15 @@ public void apply(CollisionUpdate update) {
}
/**
- * Casts a ray into the world to check for impenetrable objects from the given {@code start} position to the
+ * Casts a ray into the world to check for impenetrable objects from the given {@code start} position to the
* {@code end} position using Bresenham's line algorithm.
*
* @param start The start position of the ray.
* @param end The end position of the ray.
+ * @param objectType
* @return {@code true} if an impenetrable object was hit, {@code false} otherwise.
*/
- public boolean raycast(Position start, Position end) {
+ public boolean raycast(Position start, Position end, EntityType objectType) {
Preconditions.checkArgument(start.getHeight() == end.getHeight(), "Positions must be on the same height");
if (start.equals(end)) {
@@ -221,7 +222,7 @@ public boolean raycast(Position start, Position end) {
Direction direction = Direction.fromDeltas(currX - lastX, currY - lastY);
Position last = new Position(lastX, lastY, start.getHeight());
- if (!traversable(last, EntityType.PROJECTILE, direction)) {
+ if (!traversable(last, objectType, direction)) {
return false;
}
diff --git a/game/src/main/java/org/apollo/game/model/entity/AnimationMap.java b/game/src/main/java/org/apollo/game/model/entity/AnimationMap.java
new file mode 100644
index 000000000..a50662e84
--- /dev/null
+++ b/game/src/main/java/org/apollo/game/model/entity/AnimationMap.java
@@ -0,0 +1,194 @@
+package org.apollo.game.model.entity;
+
+/**
+ * A map of animations for {@link Player}s. Contains animations such as standing, turning, running, etc.
+ *
+ * @author Steve Soltys
+ */
+public class AnimationMap {
+ /**
+ * The default animation set for {@link Player}s.
+ */
+ public static final AnimationMap DEFAULT_ANIMATION_SET = new AnimationMap(0x328, 0x337, 0x333, 0x334, 0x335, 0x336, 0x338);
+
+ /**
+ * The animation for standing in place.
+ */
+ private int stand;
+
+ /**
+ * The animation for turning while idle.
+ */
+ private int idleTurn;
+
+ /**
+ * The animation for walking.
+ */
+ private int walking;
+
+ /**
+ * The animation for turning 180 degrees.
+ */
+ private int turnAround;
+
+ /**
+ * The animation for turning 90 degrees right.
+ */
+ private int turnRight;
+
+ /**
+ * The animation for turning 90 degrees left.
+ */
+ private int turnLeft;
+
+ /**
+ * The animation for running.
+ */
+ private int running;
+
+ public AnimationMap(int stand, int idleTurn, int walking, int turnAround, int turnRight, int turnLeft, int running) {
+ this.stand = stand;
+ this.idleTurn = idleTurn;
+ this.walking = walking;
+ this.turnAround = turnAround;
+ this.turnRight = turnRight;
+ this.turnLeft = turnLeft;
+ this.running = running;
+ }
+
+ /**
+ * Duplicates this animation set.
+ *
+ * @return the duplicated animation set.
+ */
+ @Override
+ public AnimationMap clone() {
+ return new AnimationMap(stand, idleTurn, walking, turnAround, turnRight, turnLeft, running);
+ }
+
+ /**
+ * Gets the animation for standing in place.
+ *
+ * @return the animation.
+ */
+ public int getStand() {
+ return stand;
+ }
+
+ /**
+ * Sets the animation for standing in place.
+ *
+ * @param stand the animation.
+ */
+ public void setStand(int stand) {
+ this.stand = stand;
+ }
+
+ /**
+ * Gets the animation for turning while idle.
+ *
+ * @return the animation.
+ */
+ public int getIdleTurn() {
+ return idleTurn;
+ }
+
+ /**
+ * Sets the animation for standing in place.
+ *
+ * @param idleTurn the animation.
+ */
+ public void setIdleTurn(int idleTurn) {
+ this.idleTurn = idleTurn;
+ }
+
+ /**
+ * Gets the animation for walking.
+ *
+ * @return the animation.
+ */
+ public int getWalking() {
+ return walking;
+ }
+
+ /**
+ * Sets the animation for walking.
+ *
+ * @param walking the animation.
+ */
+ public void setWalking(int walking) {
+ this.walking = walking;
+ }
+
+ /**
+ * Gets the animation for turning 180 degrees.
+ *
+ * @return the animation.
+ */
+ public int getTurnAround() {
+ return turnAround;
+ }
+
+ /**
+ * Sets the animation for turning 180 degrees.
+ *
+ * @param turnAround the animation.
+ */
+ public void setTurnAround(int turnAround) {
+ this.turnAround = turnAround;
+ }
+
+ /**
+ * Gets the animation for turning 90 degrees right.
+ *
+ * @return the animation.
+ */
+ public int getTurnRight() {
+ return turnRight;
+ }
+
+ /**
+ * Sets the animation for turning 90 degrees right.
+ *
+ * @param turnRight the animation.
+ */
+ public void setTurnRight(int turnRight) {
+ this.turnRight = turnRight;
+ }
+
+ /**
+ * Gets the animation for turning 90 degrees left.
+ *
+ * @return the animation.
+ */
+ public int getTurnLeft() {
+ return turnLeft;
+ }
+
+ /**
+ * Sets the animation for turning 90 degrees left.
+ *
+ * @param turnLeft the animation.
+ */
+ public void setTurnLeft(int turnLeft) {
+ this.turnLeft = turnLeft;
+ }
+
+ /**
+ * Gets the animation for running.
+ *
+ * @return the animation.
+ */
+ public int getRunning() {
+ return running;
+ }
+
+ /**
+ * Sets the animation for running.
+ *
+ * @param running the animation.
+ */
+ public void setRunning(int running) {
+ this.running = running;
+ }
+}
diff --git a/game/src/main/java/org/apollo/game/model/entity/Player.java b/game/src/main/java/org/apollo/game/model/entity/Player.java
index 76c06db95..35eacba4d 100644
--- a/game/src/main/java/org/apollo/game/model/entity/Player.java
+++ b/game/src/main/java/org/apollo/game/model/entity/Player.java
@@ -64,6 +64,11 @@
*/
public final class Player extends Mob {
+ /**
+ * The {@code Mob}s current set of animations used in appearance updates.
+ */
+ private AnimationMap animations = AnimationMap.DEFAULT_ANIMATION_SET;
+
/**
* The default viewing distance, in tiles.
*/
@@ -317,6 +322,15 @@ public boolean friendsWith(String username) {
return friends.contains(username.toLowerCase());
}
+ /**
+ * Gets the current set of animations used for character movement.
+ *
+ * @return The animation set.
+ */
+ public final AnimationMap getAnimations() {
+ return animations;
+ }
+
/**
* Gets the player's appearance.
*
@@ -797,6 +811,15 @@ public void sendUserLists() {
}
}
+ /**
+ * Sets the set of animations to use in {@code Mob} appearance updates.
+ *
+ * @param animations The set of animations to use.
+ */
+ public final void setAnimations(AnimationMap animations) {
+ this.animations = animations;
+ }
+
/**
* Sets the player's appearance.
*
diff --git a/game/src/main/java/org/apollo/game/release/r317/PlayerSynchronizationMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r317/PlayerSynchronizationMessageEncoder.java
index b628ded53..26ac6fc11 100644
--- a/game/src/main/java/org/apollo/game/release/r317/PlayerSynchronizationMessageEncoder.java
+++ b/game/src/main/java/org/apollo/game/release/r317/PlayerSynchronizationMessageEncoder.java
@@ -9,6 +9,7 @@
import org.apollo.game.model.Item;
import org.apollo.game.model.Position;
import org.apollo.game.model.entity.EquipmentConstants;
+import org.apollo.game.model.entity.AnimationMap;
import org.apollo.game.model.entity.setting.Gender;
import org.apollo.game.model.inv.Inventory;
import org.apollo.game.sync.block.AnimationBlock;
@@ -207,13 +208,14 @@ private static void putAppearanceBlock(AppearanceBlock block, GamePacketBuilder
playerProperties.put(DataType.BYTE, color);
}
- playerProperties.put(DataType.SHORT, 0x328); // stand
- playerProperties.put(DataType.SHORT, 0x337); // stand turn
- playerProperties.put(DataType.SHORT, 0x333); // walk
- playerProperties.put(DataType.SHORT, 0x334); // turn 180
- playerProperties.put(DataType.SHORT, 0x335); // turn 90 cw
- playerProperties.put(DataType.SHORT, 0x336); // turn 90 ccw
- playerProperties.put(DataType.SHORT, 0x338); // run
+ AnimationMap animations = block.getAnimations();
+ playerProperties.put(DataType.SHORT, animations.getStand()); // stand
+ playerProperties.put(DataType.SHORT, animations.getIdleTurn()); // stand turn
+ playerProperties.put(DataType.SHORT, animations.getWalking()); // walk
+ playerProperties.put(DataType.SHORT, animations.getTurnAround()); // turn 180
+ playerProperties.put(DataType.SHORT, animations.getTurnRight()); // turn 90 cw
+ playerProperties.put(DataType.SHORT, animations.getTurnLeft()); // turn 90 ccw
+ playerProperties.put(DataType.SHORT, animations.getRunning()); // run
playerProperties.put(DataType.LONG, block.getName());
playerProperties.put(DataType.BYTE, block.getCombatLevel());
diff --git a/game/src/main/java/org/apollo/game/release/r377/PlayerSynchronizationMessageEncoder.java b/game/src/main/java/org/apollo/game/release/r377/PlayerSynchronizationMessageEncoder.java
index 5ef61f80c..77d1f4017 100644
--- a/game/src/main/java/org/apollo/game/release/r377/PlayerSynchronizationMessageEncoder.java
+++ b/game/src/main/java/org/apollo/game/release/r377/PlayerSynchronizationMessageEncoder.java
@@ -2,36 +2,14 @@
import org.apollo.cache.def.EquipmentDefinition;
import org.apollo.game.message.impl.PlayerSynchronizationMessage;
-import org.apollo.game.model.Animation;
-import org.apollo.game.model.Appearance;
-import org.apollo.game.model.Direction;
-import org.apollo.game.model.Graphic;
-import org.apollo.game.model.Item;
-import org.apollo.game.model.Position;
+import org.apollo.game.model.*;
import org.apollo.game.model.entity.EquipmentConstants;
+import org.apollo.game.model.entity.AnimationMap;
import org.apollo.game.model.entity.setting.Gender;
import org.apollo.game.model.inv.Inventory;
-import org.apollo.game.sync.block.AnimationBlock;
-import org.apollo.game.sync.block.AppearanceBlock;
-import org.apollo.game.sync.block.ChatBlock;
-import org.apollo.game.sync.block.ForceChatBlock;
-import org.apollo.game.sync.block.ForceMovementBlock;
-import org.apollo.game.sync.block.GraphicBlock;
-import org.apollo.game.sync.block.HitUpdateBlock;
-import org.apollo.game.sync.block.InteractingMobBlock;
-import org.apollo.game.sync.block.SecondaryHitUpdateBlock;
-import org.apollo.game.sync.block.SynchronizationBlockSet;
-import org.apollo.game.sync.block.TurnToPositionBlock;
-import org.apollo.game.sync.seg.AddPlayerSegment;
-import org.apollo.game.sync.seg.MovementSegment;
-import org.apollo.game.sync.seg.SegmentType;
-import org.apollo.game.sync.seg.SynchronizationSegment;
-import org.apollo.game.sync.seg.TeleportSegment;
-import org.apollo.net.codec.game.DataOrder;
-import org.apollo.net.codec.game.DataTransformation;
-import org.apollo.net.codec.game.DataType;
-import org.apollo.net.codec.game.GamePacket;
-import org.apollo.net.codec.game.GamePacketBuilder;
+import org.apollo.game.sync.block.*;
+import org.apollo.game.sync.seg.*;
+import org.apollo.net.codec.game.*;
import org.apollo.net.meta.PacketType;
import org.apollo.net.release.MessageEncoder;
@@ -208,13 +186,14 @@ private static void putAppearanceBlock(AppearanceBlock block, GamePacketBuilder
playerProperties.put(DataType.BYTE, color);
}
- playerProperties.put(DataType.SHORT, 0x328); // stand
- playerProperties.put(DataType.SHORT, 0x337); // stand turn
- playerProperties.put(DataType.SHORT, 0x333); // walk
- playerProperties.put(DataType.SHORT, 0x334); // turn 180
- playerProperties.put(DataType.SHORT, 0x335); // turn 90 cw
- playerProperties.put(DataType.SHORT, 0x336); // turn 90 ccw
- playerProperties.put(DataType.SHORT, 0x338); // run
+ AnimationMap animations = block.getAnimations();
+ playerProperties.put(DataType.SHORT, animations.getStand()); // stand
+ playerProperties.put(DataType.SHORT, animations.getIdleTurn()); // stand turn
+ playerProperties.put(DataType.SHORT, animations.getWalking()); // walk
+ playerProperties.put(DataType.SHORT, animations.getTurnAround()); // turn 180
+ playerProperties.put(DataType.SHORT, animations.getTurnRight()); // turn 90 cw
+ playerProperties.put(DataType.SHORT, animations.getTurnLeft()); // turn 90 ccw
+ playerProperties.put(DataType.SHORT, animations.getRunning()); // run
playerProperties.put(DataType.LONG, block.getName());
playerProperties.put(DataType.BYTE, block.getCombatLevel());
diff --git a/game/src/main/java/org/apollo/game/sync/block/AppearanceBlock.java b/game/src/main/java/org/apollo/game/sync/block/AppearanceBlock.java
index a2a54356c..9a23d00b8 100644
--- a/game/src/main/java/org/apollo/game/sync/block/AppearanceBlock.java
+++ b/game/src/main/java/org/apollo/game/sync/block/AppearanceBlock.java
@@ -1,6 +1,7 @@
package org.apollo.game.sync.block;
import org.apollo.game.model.Appearance;
+import org.apollo.game.model.entity.AnimationMap;
import org.apollo.game.model.inv.Inventory;
/**
@@ -10,6 +11,11 @@
*/
public final class AppearanceBlock extends SynchronizationBlock {
+ /**
+ * The player's movement animations.
+ */
+ private final AnimationMap animations;
+
/**
* The player's appearance.
*/
@@ -53,32 +59,35 @@ public final class AppearanceBlock extends SynchronizationBlock {
/**
* Creates the appearance block. Assumes that the player is not appearing as an npc.
*
- * @param name The player's username, encoded to base 37.
+ * @param name The player's username, encoded to base 37.
* @param appearance The {@link Appearance}.
- * @param combat The player's combat.
- * @param skill The player's skill, or 0 if showing the combat level.
- * @param equipment The player's equipment.
- * @param headIcon The head icon id of the player.
- * @param isSkulled Whether or not the player is skulled.
- */
- AppearanceBlock(long name, Appearance appearance, int combat, int skill, Inventory equipment, int headIcon, boolean isSkulled) {
- this(name, appearance, combat, skill, equipment, headIcon, isSkulled, -1);
+ * @param combat The player's combat.
+ * @param skill The player's skill, or 0 if showing the combat level.
+ * @param equipment The player's equipment.
+ * @param headIcon The head icon id of the player.
+ * @param isSkulled Whether or not the player is skulled.
+ * @param animations
+ */
+ AppearanceBlock(long name, Appearance appearance, int combat, int skill, Inventory equipment, int headIcon, boolean isSkulled, AnimationMap animations) {
+ this(name, animations, appearance, combat, skill, equipment, headIcon, isSkulled, -1);
}
/**
* Creates the appearance block.
*
- * @param name The player's username, encoded to base 37.
+ * @param name The player's username, encoded to base 37.
+ * @param animations
* @param appearance The {@link Appearance}.
- * @param combat The player's combat.
- * @param skill The player's skill, or 0 if showing the combat level.
- * @param equipment The player's equipment.
- * @param headIcon The prayer icon id of this player.
- * @param isSkulled Whether or not the player is skulled.
- * @param npcId The npc id of the player, if they are appearing as an npc, (otherwise {@code -1}).
- */
- AppearanceBlock(long name, Appearance appearance, int combat, int skill, Inventory equipment, int headIcon, boolean isSkulled, int npcId) {
+ * @param combat The player's combat.
+ * @param skill The player's skill, or 0 if showing the combat level.
+ * @param equipment The player's equipment.
+ * @param headIcon The prayer icon id of this player.
+ * @param isSkulled Whether or not the player is skulled.
+ * @param npcId The npc id of the player, if they are appearing as an npc, (otherwise {@code -1}).
+ */
+ AppearanceBlock(long name, AnimationMap animations, Appearance appearance, int combat, int skill, Inventory equipment, int headIcon, boolean isSkulled, int npcId) {
this.name = name;
+ this.animations = animations;
this.appearance = appearance;
this.combat = combat;
this.skill = skill;
@@ -97,6 +106,15 @@ public boolean appearingAsNpc() {
return npcId != -1;
}
+ /**
+ * Gets the player's {@link AnimationMap}.
+ *
+ * @return The player's animations.
+ */
+ public AnimationMap getAnimations() {
+ return animations;
+ }
+
/**
* Gets the player's {@link Appearance}.
*
diff --git a/game/src/main/java/org/apollo/game/sync/block/SynchronizationBlock.java b/game/src/main/java/org/apollo/game/sync/block/SynchronizationBlock.java
index 238ed7bfd..e664e3f1d 100644
--- a/game/src/main/java/org/apollo/game/sync/block/SynchronizationBlock.java
+++ b/game/src/main/java/org/apollo/game/sync/block/SynchronizationBlock.java
@@ -37,7 +37,7 @@ public static SynchronizationBlock createAppearanceBlock(Player player) {
int combat = player.getSkillSet().getCombatLevel();
int id = player.hasNpcDefinition() ? player.getDefinition().getId() : -1;
- return new AppearanceBlock(player.getEncodedName(), player.getAppearance(), combat, 0, player.getEquipment(), player.getPrayerIcon(), player.isSkulled(), id);
+ return new AppearanceBlock(player.getEncodedName(), player.getAnimations(), player.getAppearance(), combat, 0, player.getEquipment(), player.getPrayerIcon(), player.isSkulled(), id);
}
/**
diff --git a/game/src/main/kotlin/stub.kt b/game/src/main/kotlin/stub.kt
deleted file mode 100644
index 0f49676b1..000000000
--- a/game/src/main/kotlin/stub.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * NOTE: This file is a stub, intended only for use within an IDE. It should be updated
- * each time [org.apollo.game.plugin.kotlin.KotlinPluginScript] has a new method added to it.
- *
- * Until IntelliJ IDEA starts to support ScriptTemplateDefinitions this is
- * required to resolve references within plugin code.
- */
-
-import org.apollo.game.command.Command
-import org.apollo.game.message.impl.ButtonMessage
-import org.apollo.game.model.World
-import org.apollo.game.model.entity.setting.PrivilegeLevel
-import org.apollo.game.model.event.Event
-import org.apollo.game.model.event.PlayerEvent
-import org.apollo.game.plugin.kotlin.KotlinEventHandler
-import org.apollo.game.plugin.kotlin.KotlinPlayerHandlerProxyTrait
-import org.apollo.net.message.Message
-import kotlin.reflect.KClass
-
-fun on(type: () -> KClass): KotlinPlayerHandlerProxyTrait = null!!
-fun on_player_event(type: () -> KClass): KotlinPlayerHandlerProxyTrait = null!!
-fun on_event(type: () -> KClass): KotlinEventHandler = null!!
-fun on_command(command: String, privileges: PrivilegeLevel): KotlinPlayerHandlerProxyTrait = null!!
-fun on_button(button: Int): KotlinPlayerHandlerProxyTrait = null!!
-
-fun start(callback: (World) -> Unit) {}
-fun stop(callback: (World) -> Unit) = {}
-