Add ranged combat support

* Clean up the CombatAction and Attack code to make it easier to use for
  range.

* Add collision detection to the distance checks before attacking in the
  CombatAction.

* Create a Ruby DSL for defining projectile types and fix the
  ProjectileUpdateOperation so it uses the correct position offset.

* Fix the packet structure of the HintIconMessageEncoder.
This commit is contained in:
Gary Tierney
2016-01-03 22:24:43 +00:00
parent f80ea82ce7
commit 3082fade1d
35 changed files with 741 additions and 433 deletions
+34
View File
@@ -0,0 +1,34 @@
AMMO = {}
def create_ammo(item_matcher, properties, &block)
items = find_entities :item, item_matcher, -1
fail "Unable to find ammo matching #{item_matcher}" unless items.size > 0
projectile_type = properties[:projectile_type]
fail "Unable to find projectile type #{projectile_type}" unless PROJECTILE_TYPES.key? projectile_type
properties[:projectile_type] = PROJECTILE_TYPES[projectile_type]
items.each do |item_id|
AMMO[item_id] = Ammo.new(item_id, properties)
AMMO[item_id].instance_eval &block if block_given?
end
end
private
class Ammo
attr_reader :item, :weapon_classes, :projectile, :projectile_type, :level_requirement, :drop_rate, :graphic
include Combat::BonusContainer
def initialize(item, weapon_classes:, projectile:, projectile_type:, level_requirement:, drop_rate:, graphic: nil)
@item = item
@weapon_classes = weapon_classes
@projectile = projectile
@projectile_type = projectile_type
@level_requirement = level_requirement
@drop_rate = drop_rate
@graphic = graphic
end
end
+15
View File
@@ -0,0 +1,15 @@
def create_arrow(item, hash)
hash[:projectile_type] = :arrow
hash[:weapon_classes] = [:longbow, :shortbow]
create_ammo item, hash
end
create_projectile_type :arrow, start_height: 41, end_height: 37, delay: 41, speed: 6, slope: 15, radius: 10
create_arrow :bronze_arrow, level_requirement: 1, projectile: 10, graphic: 19, drop_rate: 0.3
create_arrow :iron_arrow, level_requirement: 1, projectile: 9, graphic: 18, drop_rate: 0.35
create_arrow :steel_arrow, level_requirement: 5, projectile: 11, graphic: 20, drop_rate: 0.4
create_arrow :mithril_arrow, level_requirement: 20, projectile: 12, graphic: 21, drop_rate: 0.45
create_arrow :adamant_arrow, level_requirement: 30, projectile: 13, graphic: 22, drop_rate: 0.5
create_arrow :rune_arrow, level_requirement: 40, projectile: 15, graphic: 24, drop_rate: 0.6
+10
View File
@@ -0,0 +1,10 @@
def create_bolt(item, hash)
hash[:projectile_type] = :bolt
hash[:weapon_classes] = [:crossbow]
hash[:projectile] = 27 unless hash.key? :projectile
create_ammo item, hash
end
create_projectile_type :bolt, start_height: 44, end_height: 44, delay: 41, speed: 5, slope: 5, radius: 10
create_bolt :iron_bolts, level_requirement: 1, drop_rate: 0.35
+150 -42
View File
@@ -3,9 +3,13 @@ java_import 'org.apollo.game.model.Animation'
java_import 'org.apollo.game.model.Graphic'
class BaseAttack
attr_reader :requirements, :range
attr_reader :requirements, :range, :speed
def initialize(animation, graphic = nil, range = 1, requirements = [])
def initialize(speed:, animation:, graphic: nil, range: 1, requirements: [])
fail 'Attack speed must be a non-negative number' if speed < 0
fail 'Attack range must be a non-negative number' if range < 0
@speed = speed
@animation = animation
@graphic = graphic
@range = range
@@ -17,7 +21,7 @@ class BaseAttack
unless @graphic.nil?
if @graphic.is_a?(Hash)
source.play_graphic(Graphic.new(@graphic[:id], @graphic[:delay] || 0, @graphic[:height] | 0))
source.play_graphic(Graphic.new(@graphic[:id], @graphic[:delay] || 0, @graphic[:height] || 0))
else
source.play_graphic(Graphic.new(@graphic))
end
@@ -26,63 +30,167 @@ class BaseAttack
apply(source, target)
end
def apply(source, target)
raise "BaseAttack#apply unimplemented"
end
def damage!(source, target, amount, delay = 0)
schedule delay do |task|
task.stop && return if source.dead or target.dead
target_combat_state = get_combat_state target
target_hitpoints = target.skill_set.get_skill(Skill::HITPOINTS).get_current_level
amount = target_hitpoints if target_hitpoints < amount
type = amount > 0 ? 1 : 0
target.play_animation(Animation.new(424)) unless target_combat_state.state == :attacking
target.damage(amount, type, false)
task.stop
end
def apply(_source, _target)
fail 'BaseAttack#apply unimplemented'
end
end
class ProcAttack < BaseAttack
def initialize(block, animation:, graphic:, range: 1, requirements: [])
super(animation, graphic, range, requirements)
def initialize(block, hash)
super(hash)
@block = block
end
def apply(source, target)
self.instance_exec(source, target, &@block)
instance_exec(source, target, &@block)
end
end
##
# A simple ranged attack, which sends a projectile based on the currently equipped ammo and weapon
# and deals damaged delayed by the speed and travel distance of the projectile.
class RangedAttack < BaseAttack
def initialize(animation:, graphic:, requirements: [])
end
def calculate_delay(source, target)
end
def apply(source, target)
end
def projectile!(source, target, projectile_id)
do_ranged_damage! source, target
end
end
##
# A basic melee attack, which deals damage a tick after the attack was made.
class Attack < BaseAttack
def initialize(animation:, graphic: nil, range: 1, requirements: [])
super(animation, graphic, range, requirements)
end
def apply(source, target)
damage! source, target, CombatUtil::calculate_hit(source, target)
do_damage! source, target, CombatUtil.calculate_hit(source, target)
end
end
##
# A DSL-like builder object for creating a new Attack, used mainly (exclusively?) for special attacks.
class AttackDSL
attr_accessor :animation, :speed, :range, :graphic
def initialize()
@requirements = []
@subattacks = []
end
def add_requirement(requirement)
@requirements.push requirement
end
##
# Deal ranged damage.
def range_damage!(hash)
@subattacks.push lambda { |source, target|
do_ranged_damage! source, target, hash
}
end
##
# Deal melee damage.
def damage!(damage_modifier: 1, delay: 0)
@subattacks.push lambda { |source, target|
do_damage! source, target, 1, delay
}
end
##
# Process the attack instructions in this DSL then build and return an {@link Attack} object.
def to_attack
subattacks = @subattacks
attack = lambda do |source, target|
subattacks.each do |subattack|
subattack[source, target]
end
end
# TODO: clean up? Decide where keyword arguments are appropriate and where not
ProcAttack.new(attack, speed: speed, animation: animation, graphic: graphic, range: range, requirements: @requirements)
end
end
private
##
# TODO: refactor
def do_damage!(source, target, amount, delay = 0, secondary = false, &_block)
schedule delay do |task|
task.stop && return if source.dead || target.dead
target_combat_state = get_combat_state target
target_hitpoints = target.skill_set.get_skill(Skill::HITPOINTS).get_current_level
amount = target_hitpoints if target_hitpoints < amount
type = amount > 0 ? 1 : 0
target.play_animation(Animation.new(424)) unless target_combat_state.will_attack?
target.damage(amount, type, secondary)
if target.auto_retaliate && !target_combat_state.is_attacking?
target_combat_state.target = source
target.start_action CombatAction.new(target)
end
task.stop
end
end
##
# TODO: refactor
def do_ranged_damage!(source, target, _damage_modifier = 1, speed_modifier = 1, projectile = nil, projectile_type = nil, projectile_graphic = nil,
delay = 0, secondary = false)
apply = lambda do
distance = source.position.get_distance(target.position)
ammo = EquipmentUtil.equipped_ammo source
if projectile.nil? && projectile_type.nil?
projectile = ammo.projectile
projectile_type = ammo.projectile_type
projectile_graphic = ammo.graphic
end
projectile! source, target, projectile, projectile_type, speed_modifier
do_damage! source, target, rand(1...50), (projectile_type.delay + projectile_type.speed + distance * 5) * 0.02857, secondary
unless projectile_graphic.nil?
source.play_graphic Graphic.new(projectile_graphic, 0, 100)
end
end
if delay == 0
apply.call
else
# account for the one tick delay in the scheduler
schedule(delay - 1) do |task|
apply.call
task.stop
end
end
end
##
# TODO: refactor
def projectile!(source, target, projectile_id, projectile_type, speed_modifier = 1)
distance = source.position.get_distance(target.position)
projectile_parameters = {
angle: -1,
start_height: projectile_type.start_height,
end_height: projectile_type.end_height,
delay: projectile_type.delay * (2 - speed_modifier),
speed: (projectile_type.delay + projectile_type.speed + distance * 5) * speed_modifier,
slope: projectile_type.slope,
radius: projectile_type.radius
}
ProjectileModule.fire_projectile(source.position, target.position, projectile_id, projectile_parameters, target)
end
+49 -11
View File
@@ -1,4 +1,6 @@
java_import 'org.apollo.cache.def.ItemDefinition'
java_import 'org.apollo.game.model.entity.EquipmentConstants'
java_import 'org.apollo.cache.def.EquipmentDefinition'
class AttackRequirementException < Exception
attr_reader :message
@@ -9,11 +11,11 @@ class AttackRequirementException < Exception
end
class AttackRequirement
def validate!(player)
def validate(_player)
throw RuntimeError.new('validate! not implemented')
end
def apply(player)
def apply!(_player)
throw RuntimeError.new('apply not implemented')
end
end
@@ -23,19 +25,19 @@ class SpecialEnergyRequirement < AttackRequirement
@amount = amount
end
def validate!(player)
if player.special_energy < @amount
def validate(player)
if false
player.using_special = false
update_special_bar(player)
raise AttackRequirementException.new('Not enough special attack energy.')
fail AttackRequirementException.new('Not enough special attack energy.')
end
end
def apply(player)
def apply!(player)
player.special_energy = player.special_energy - @amount
player.using_special = false
player.using_special = false
update_special_bar player
end
end
@@ -46,11 +48,11 @@ class ItemRequirement < AttackRequirement
@amount = amount
end
def validate!(player)
def validate(player)
throw AttackRequirementException.new(item_missing_message) unless player.inventory.get_amount(@item) >= @amount
end
def apply(player)
def apply!(player)
player.inventory.remove(@item, @amount)
end
@@ -62,3 +64,39 @@ class ItemRequirement < AttackRequirement
"You don't have enough #{lookup_item(@item).name}s"
end
end
class AmmoRequirement < AttackRequirement
def initialize(amount = 1)
@amount = amount
end
def validate(player)
equipped_weapon_item = player.equipment.get(EquipmentConstants::WEAPON)
equipped_weapon_def = EquipmentDefinition.lookup(equipped_weapon_item.id)
equipped_weapon = EquipmentUtil.equipped_weapon player
equipped_ammo = EquipmentUtil.equipped_ammo player
equipped_ammo_amt = player.equipment.get(EquipmentConstants::AMMO).amount
if equipped_ammo.nil?
fail AttackRequirementException.new('You have no ammo left in your quiver!')
end
if @amount > 1 && equipped_ammo_amt < @amount
fail AttackRequirementException.new('You don\'t have enough ammo left in your quiver!')
end
unless equipped_ammo.weapon_classes.include? equipped_weapon.weapon_class.name
fail AttackRequirementException.new('You can\'t use this ammo with this weapon.')
end
if equipped_ammo.level_requirement > equipped_weapon_def.ranged_level
fail AttackRequirementException.new('You can\'t use this ammo with this weapon.')
end
end
def apply!(player)
equipped_ammo = EquipmentUtil.equipped_ammo player
player.equipment.remove equipped_ammo.item
end
end
+6 -2
View File
@@ -1,6 +1,6 @@
##
# The delay a <i>Mob</i> must wait before attacking again.
declare_attribute(:attack_delay, 0)
# The number of ticks since a <i>Mob<i>s last attack.
declare_attribute(:attack_timer, 100)
##
# A flag indicating whether this <i>Mob</i> is currently in combat.
@@ -22,6 +22,10 @@ declare_attribute(:combat_style, 0, :persistent)
# A flag indicating whether the special bar is flagged for the next attack.
declare_attribute(:using_special, false, :persistent)
##
# A flag indicating whether auto retaliation is enabled.
declare_attribute(:auto_retaliate, true, :persistent)
##
# An integer between 0 and 100 indicating the amount of special energy a <i>Player</i> has.
declare_attribute(:special_energy, 100, :persistent)
+14 -14
View File
@@ -15,30 +15,30 @@ module Combat
def other_bonuses(melee_strength: 0, ranged_strength: 0, prayer: 0)
@other_bonuses = {
:melee_strength => melee_strength,
:ranged_strength => ranged_strength,
:prayer => prayer
melee_strength: melee_strength,
ranged_strength: ranged_strength,
prayer: prayer
}
end
def defence_bonuses(stab: 0, slash: 0, crush: 0, magic: 0, range: 0)
@defence_bonuses = {
:stab => stab,
:slash => slash,
:crush => crush,
:magic => magic,
:range => range
stab: stab,
slash: slash,
crush: crush,
magic: magic,
range: range
}
end
def attack_bonuses(stab: 0, slash: 0, crush: 0, magic: 0, range: 0)
@attack_bonuses = {
:stab => stab,
:slash => slash,
:crush => crush,
:magic => magic,
:range => range
stab: stab,
slash: slash,
crush: crush,
magic: magic,
range: range
}
end
end
end
end
+24 -8
View File
@@ -1,12 +1,28 @@
on :message, :npc_action do |player, message|
player_combat_state = get_combat_state player
player_combat_state.target = $world.npc_repository.get message.index
java_import 'org.apollo.game.message.impl.HintIconMessage'
unless player.attacking
player.start_action CombatAction.new(player)
end
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 = get_combat_state player
player_combat_state.target = target
player.send HintIconMessage.for_npc(target.index)
player.walking_queue.clear
player.start_action CombatAction.new(player) unless player_combat_state.is_attacking?
end
on :message, :player_action do |player, message|
end
end
schedule 0 do |_task|
$world.player_repository.each { |player| player.attack_timer = player.attack_timer + 1 }
$world.npc_repository.each { |npc| npc.attack_timer = npc.attack_timer + 1 }
end
+45 -101
View File
@@ -1,122 +1,66 @@
java_import 'org.apollo.game.action.Action'
# Represents a one off {@code Attack}, which will not continue a combat session
# upon completion.
class AttackAction < Action
def initialize(source, target, delay, attack)
@source = source
@target = target
@delay = delay
@attack = attack
end
def execute
begin
@attack.do(mob, @target)
stop
rescue AttackRequirementException => e
player.send_message e.message
stop
end
end
end
java_import 'org.apollo.game.model.entity.EntityType'
class CombatAction < Action
def initialize(source, attack = nil)
def initialize(source, once = false)
super(0, true, source)
mob.attacking = true
@combat_state = get_combat_state(source)
@attack = attack
@attack_timer = 100
@once = once
end
def can_attack?
next_attack = @combat_state.next_attack true
target = @combat_state.target
current_distance = mob.position.get_distance target.position
attack_collision_type = next_attack.range > 1 ? EntityType::PROJECTILE : EntityType::NPC
in_range = current_distance <= next_attack.range && !$world.intersects(mob.position, target.position, attack_collision_type)
unless in_range
mob.follow @combat_state.target, next_attack.range
return false
end
mob.attack_timer >= next_attack.speed
end
def try_attack
next_attack = @combat_state.next_attack false
if mob.is_a? Player
next_attack.requirements.each { |requirement| requirement.validate mob }
next_attack.requirements.each { |requirement| requirement.apply! mob }
end
next_attack.do(mob, @combat_state.target)
rescue AttackRequirementException => e
mob.send_message e.message
ensure
mob.attack_timer = 0
end
def execute
if @combat_state.target.nil? and @combat_state.queued_attacks.empty?
@combat_state.reset
if @combat_state.target.nil? || !@combat_state.target.is_active || @combat_state.next_attack(true).nil?
stop
end
case @combat_state.state
when :idle
update_idle
when :attacking
update_attacking
when :chasing
update_chasing
else
throw ArgumentError.new('invalid combat state')
end
end
def update_idle
next_attack = @combat_state.next_attack true
current_distance = mob.position.get_distance @combat_state.target.position
if current_distance > next_attack.range
@combat_state.state = :chasing
mob.follow @combat_state.target, next_attack.range
set_delay 0
return
end
if @combat_state.supports_weapon
weapon = EquipmentUtil.equipped_weapon mob
weapon_class = weapon.weapon_class
combat_style = weapon_class.style_at mob.combat_style
return unless can_attack?
if mob.attacking
set_delay weapon_class.speed(combat_style) - 1
else
set_delay 0
mob.attacking = true
end
@combat_state.state = :attacking
else
stop
@combat_state.reset
end
end
def update_attacking
begin
next_attack = @combat_state.next_attack
next_attack.requirements.each do |requirement|
requirement.validate! mob
end
next_attack.requirements.each do |requirement|
requirement.apply mob
end
next_attack.do(mob, @combat_state.target)
rescue AttackRequirementException => e
mob.send_message e.message
ensure
@combat_state.state = :idle
set_delay 0
end
end
def update_chasing
next_attack = @combat_state.next_attack true
current_distance = mob.position.get_distance @combat_state.target.position
if current_distance <= next_attack.range
@combat_state.state = :attacking
mob.following = -1
set_delay 0
end
try_attack
stop if @once
end
def stop
super
@combat_state.reset
mob.attacking = false
mob.following = -1
@combat_state.reset
end
end
end
+19 -16
View File
@@ -1,7 +1,7 @@
def get_combat_state(mob)
mob.is_a?(Player) ? type = :player : type = :npc
unless MOB_COMBAT_STATE_CACHE[type].has_key? mob.index
unless MOB_COMBAT_STATE_CACHE[type].key? mob.index
MOB_COMBAT_STATE_CACHE[type][mob.index] = CombatState.new(mob)
end
@@ -11,8 +11,7 @@ end
private
class CombatState
attr_accessor :state
attr_reader :queued_attacks, :supports_weapon
attr_reader :target
def initialize(mob, supports_weapon = true)
@mob = mob
@@ -21,20 +20,22 @@ class CombatState
end
def reset
@state = :idle
@target = nil
@next_attack = nil
@queued_attacks = []
@mob.reset_interacting_mob
end
def target
@target
def will_attack?
is_attacking? && next_attack(true).speed >= @mob.attack_timer
end
def is_attacking?
!target.nil? && @mob.attacking
end
def target=(target)
@mob.reset_interacting_mob
@mob.interacting_mob = target
@mob.interacting_mob = target unless target.nil?
@target = target
end
@@ -44,23 +45,25 @@ class CombatState
def next_attack(peek = false)
if @queued_attacks.size > 0
peek ? @queued_attacks.first : @queued_attacks.pop
peek ? @queued_attacks.last : @queued_attacks.pop
else
weapon = EquipmentUtil.equipped_weapon @mob
if @mob.using_special and weapon.special_attack?
if @mob.using_special && weapon.special_attack?
weapon.special_attack
else
weapon_class = weapon.weapon_class
combat_style = weapon_class.style_at @mob.combat_style
weapon_class.attack combat_style
combat_style = weapon.weapon_class.selected_style @mob
combat_style.attack
end
end
end
end
MOB_COMBAT_STATE_CACHE = {
:player => {},
:npc => {}
player: {},
npc: {}
}
on :logout do |event|
MOB_COMBAT_STATE_CACHE[:player].delete event.player.index
end
+11
View File
@@ -0,0 +1,11 @@
class CombatStyle
attr_reader :button, :config, :attack_type, :block_animation
attr_accessor :attack
def initialize(button, config, attack_type, block_animation)
@button = button
@config = config
@attack_type = attack_type
@block_animation = block_animation
end
end
+2 -2
View File
@@ -6,7 +6,7 @@ def create_equipment(item, &block)
equipment = Equipment.new
equipment.instance_eval block
find_entities :item, item do |equipment_item|
find_entities :item, item do |_equipment_item|
EQUIPMENT[id] = equipment
end
end
@@ -15,4 +15,4 @@ private
class Equipment
include Combat::BonusContainer
end
end
+15 -4
View File
@@ -6,8 +6,12 @@
<description>Adds fully functioning melee, ranged and magic combat.</description>
<authors>
<author>garyttierney</author>
<author>Shiver</author>
<author>Major</author>
</authors>
<scripts>
<!-- Include the BonusContainer mixin first, as its used throughout -->
<script>ammo.rb</script>
<script>attack.rb</script>
<script>attack_requirements.rb</script>
<script>attributes.rb</script>
@@ -15,9 +19,10 @@
<script>combat.rb</script>
<script>combat_action.rb</script>
<script>combat_state.rb</script>
<script>combat_style.rb</script>
<script>equipment.rb</script>
<script>projectile.rb</script>
<script>util.rb</script>
<script>projectile_type.rb</script>
<script>weapon.rb</script>
<script>weapon_class.rb</script>
<script>weapons/bows.rb</script>
@@ -25,12 +30,18 @@
<script>weapons/scimitars.rb</script>
<script>weapons/swords.rb</script>
<script>weapons/two_handed_swords.rb</script>
<script>ammo/arrows.rb</script>
<script>ammo/bolts.rb</script>
<script>weapons/ranged/bows.rb</script>
<script>weapons/ranged/magic_bows.rb</script>
<script>weapons/ranged/crossbows.rb</script>
<script>weapons/melee/daggers.rb</script>
<script>weapons/melee/scimitars.rb</script>
<script>weapons/melee/swords.rb</script>
<script>weapons/melee/two_handed_swords.rb</script>
<script>weapons/unarmed.rb</script>
<script>widgets/combat_tab.rb</script>
<script>widgets/special_bar.rb</script>
<script>widgets/ancient_magics_tab.rb</script>
<script>widgets/regular_magics_tab.rb</script>
<script>widgets/lunar_magics_tab.rb</script>
</scripts>
<dependencies>
<dependency>attributes</dependency>
-3
View File
@@ -1,3 +0,0 @@
def create_projectile(item, drop_rate:, graphic:, projectile:)
items = i
end
+20
View File
@@ -0,0 +1,20 @@
PROJECTILE_TYPES = {}
def create_projectile_type(name, start_height:, end_height:, delay:, speed:, slope:, radius:)
PROJECTILE_TYPES[name] = ProjectileType.new(start_height, end_height, delay, speed, slope, radius)
end
private
class ProjectileType
attr_reader :start_height, :end_height, :delay, :speed, :slope, :radius
def initialize(start_height, end_height, delay, speed, slope, radius)
@start_height = start_height
@end_height = end_height
@delay = delay
@speed = speed
@slope = slope
@radius = radius
end
end
+16 -12
View File
@@ -1,12 +1,14 @@
java_import 'org.apollo.game.model.entity.EquipmentConstants'
class CombatUtil
def self.current_speed(_mob)
end
def self.calculate_max_hit(source)
strength = source.skill_set.get_skill(Skill::STRENGTH)
strength_stat = 5 #source.bonus_stat(:other, :strength)
strength_stat = 5 # source.bonus_stat(:other, :strength)
effective_strength_damage = (strength.current_level) #* prayer_multiplier
effective_strength_damage = (strength.current_level) # * prayer_multiplier
if [:aggressive, :alt_aggressive].include? source.combat_style
effective_strength_damage += 3
@@ -19,8 +21,8 @@ class CombatUtil
def self.calculate_accuracy(source, target)
weapon = EquipmentUtil.equipped_weapon source
weapon_class = weapon.weapon_class
combat_style = weapon_class.style_at source.combat_style
attack_type = weapon.weapon_class.attack_type combat_style
combat_style = weapon_class.selected_style source
attack_type = combat_style.attack_type
attack_stat = [1, 1].max
defence_stat = [1, 1].max
@@ -28,11 +30,11 @@ class CombatUtil
attack = source.skill_set.get_skill(Skill::ATTACK).current_level.to_f
defence = target.skill_set.get_skill(Skill::DEFENCE).current_level.to_f
attack_prayer_multiplier = 1 #TODO: Prayer
attack_prayer_multiplier = 1 # TODO: Prayer
attack_accuracy = attack_stat * attack * attack_prayer_multiplier
attack_accuracy = 0.1 if attack_accuracy < 0
defence_prayer_multiplier = 1 #TODO: Prayer
defence_prayer_multiplier = 1 # TODO: Prayer
defence_accuracy = defence_stat * defence * defence_prayer_multiplier
defence_accuracy = 1 if defence_accuracy < 0
@@ -42,7 +44,7 @@ class CombatUtil
# Calculates a hit for the given <i>Mob</i> and special attack flag.
def self.calculate_hit(source, target)
accuracy = calculate_accuracy source, target
accuracy = calculate_accuracy(source, target)
max_hit = calculate_max_hit(source) + 1
if rand <= accuracy
@@ -57,14 +59,16 @@ class EquipmentUtil
def self.equipped_weapon(source)
item = source.equipment.get(EquipmentConstants::WEAPON)
if item.nil?
return NAMED_WEAPONS[:unarmed]
end
return NAMED_WEAPONS[:unarmed] if item.nil?
WEAPONS[item.id]
end
def self.equipped_projectile(source)
def self.equipped_ammo(source)
item = source.equipment.get(EquipmentConstants::ARROWS)
return nil if item.nil?
AMMO[item.id]
end
end
end
+60 -41
View File
@@ -1,4 +1,5 @@
java_import 'org.apollo.cache.def.ItemDefinition'
java_import 'org.apollo.cache.def.EquipmentDefinition'
java_import 'org.apollo.game.model.inv.InventoryAdapter'
java_import 'org.apollo.game.model.entity.EquipmentConstants'
java_import 'org.apollo.game.model.entity.AnimationSet'
@@ -16,41 +17,52 @@ def create_weapon(identifier, class_name = nil, named: false, &block)
end
end
private
def create_normal_weapon(item_matcher, class_name = nil, &block)
items = find_entities :item, item_matcher, -1
fail "Unable to find weapon matching #{item_matcher}" if items.empty?
items.each do |item_id|
equipment_def = EquipmentDefinition.lookup(item_id)
next if equipment_def.nil? || equipment_def.slot != EquipmentConstants::WEAPON
definition = ItemDefinition.lookup(item_id)
definition_name = definition.name.downcase.to_s
if class_name.nil?
class_name =
case definition_name
when /[a-zA-Z]+ 2h sword/
:two_handed_sword
when /[a-zA-Z]+ scimitar/
:scimitar
when /[a-zA-Z]+ dagger/
:dagger
else
raise "Couldn't find a suitable weapon class for the given weapon."
when /[a-zA-Z]+ 2h sword/
:two_handed_sword
when /[a-zA-Z]+ scimitar/
:scimitar
when /[a-zA-Z]+ dagger/
:dagger
when /[a-z\s]*longbow/
:longbow
when /[a-z\s]*shortbow/
:shortbow
when /[a-z]+ c'bow/
:crossbow
else
fail "Couldn't find a suitable weapon class for the given weapon."
end
end
WEAPONS[item_id] = Weapon.new(definition.name, WEAPON_CLASSES[class_name])
WEAPONS[item_id].instance_eval &block
WEAPONS[item_id].instance_eval &block if block_given?
end
end
def create_named_weapon(name, class_name, &block)
NAMED_WEAPONS[name] = Weapon.new name.to_s.capitalize, WEAPON_CLASSES[class_name]
NAMED_WEAPONS[name].instance_eval &block
NAMED_WEAPONS[name].instance_eval &block if block_given?
end
# Represents an equippable weapon, and the class it belongs to.
#
#
# * has an optional special_attack
# * belongs to a certain WeaponClass, and inherits bonuses from it.
class Weapon
@@ -65,18 +77,22 @@ class Weapon
end
def special_attack?
not special_attack.nil?
!special_attack.nil?
end
def set_special_attack(energy_requirement:, animation:, graphic: nil, &block)
# todo figure out if ranged or melee
def set_special_attack(speed:, range: 1, energy_requirement:, animation:, graphic: nil, &block)
attack_dsl = AttackDSL.new
attack_dsl.speed = speed
attack_dsl.range = range
attack_dsl.animation = animation
attack_dsl.graphic = graphic
attack_dsl.add_requirement SpecialEnergyRequirement.new(energy_requirement)
attack_dsl.instance_eval &block
requirements = [SpecialEnergyRequirement.new(energy_requirement)]
@special_attack = ProcAttack.new(block, animation: animation, graphic: graphic, requirements: requirements)
@special_attack = attack_dsl.to_attack
end
end
def update_weapon_animations(player)
default_animations = AnimationSet::DEFAULT_ANIMATION_SET
player_animations = player.animation_set
@@ -93,33 +109,33 @@ def update_weapon_animations(player)
weapon_class = weapon.weapon_class
[:stand, :walk, :run, :idle_turn, :turn_around, :turn_left, :turn_right].each do |key|
animation = weapon_class.other_animation(key)
animation = weapon_class.animation(key)
unless animation.nil?
case key
when :stand
player_animations.stand = animation
when :walk
player_animations.walking = animation
when :run
player_animations.running = animation
when :idle_turn
player_animations.idle_turn = animation
when :turn_around
player_animations.turn_around = animation
when :turn_left
player_animations.turn_left = animation
when :turn_right
player_animations.turn_right = animation
else
# type code here
end
next if animation.nil?
case key
when :stand
player_animations.stand = animation
when :walk
player_animations.walking = animation
when :run
player_animations.running = animation
when :idle_turn
player_animations.idle_turn = animation
when :turn_around
player_animations.turn_around = animation
when :turn_left
player_animations.turn_left = animation
when :turn_right
player_animations.turn_right = animation
end
end
end
on :message, :item_option do |player, message|
update_weapon_animations(player) if message.option == 2 and message.interface_id == SynchronizationInventoryListener::INVENTORY_ID
next unless message.interface_id == SynchronizationInventoryListener::INVENTORY_ID &&
message.option == 2
update_weapon_animations(player)
end
on :login do |event|
@@ -127,5 +143,8 @@ on :login do |event|
end
on :message, :item_action do |player, message|
update_weapon_animations(player) if message.interface_id == SynchronizationInventoryListener::EQUIPMENT_ID and message.slot == EquipmentConstants::WEAPON
next unless message.interface_id == SynchronizationInventoryListener::EQUIPMENT_ID &&
message.slot == EquipmentConstants::WEAPON
update_weapon_animations(player)
end
+133 -86
View File
@@ -4,122 +4,169 @@ COMBAT_STYLES = [
:defensive,
:controlled,
:alt_aggressive,
:accurate_ranged,
:rapid,
:long_range
]
MELEE_COMBAT_STYLES = [
:accurate,
:aggressive,
:alt_aggressive,
:controlled,
:defensive
]
RANGE_COMBAT_STYLES = [
:accurate_ranged,
:rapid,
:long_range
]
##
# A model for a weapon class, which contains the necessary inform
class WeaponClass
attr_reader :widget, :name, :special_bar_button, :special_bar_config
##
# Allow this class to set combat bonuses.
include Combat::BonusContainer
def initialize(name, widget)
@name = name
@widget = widget
@styles = {}
@style_attacks = {}
@style_offsets = []
@animations = {}
##
# The widget which is displayed on the combat tab when this weapon class
# is in use.
attr_reader :widget
##
# A unique identifier for this class.
attr_reader :name
##
# The id of the special bar button for this weapon class, if applicable.
attr_reader :special_bar_button
##
# The id of the widget config used to hide this weapon classes special bar, if applicable.
attr_reader :special_bar_config
##
# Create a new WeaponClass with the given name and specified widget id.
#
# @param [Symbol] name The unique name to give this weapon class.
# @param [Number] widget The id of the widget associated with this weapon class.
def initialize(name, widget, type)
@name = name
@widget = widget
@type = type
@styles = {}
@defaults = {}
@animations = {}
end
def add_style(style, button: nil, attack_type: nil, speed: nil, animation: nil, block_animation: nil, range: 1)
raise 'Invalid combat style given' unless COMBAT_STYLES.include? style
##
# Set default properties which are used for styles which don't have
# those properties defined.
#
# @param [Hash] props
# * :attack_type [Symbol] The default attack type for styles in this class.
# * :speed [Number] The default attack speed for styles in this class.
# * :animation [Number] The default attack animation id for styles in this class.
# * :block_animation [Number] The default block animation id for styles in this class.
# * :range [Number] The default attack range for styles in this class.
@styles[style] = {
:attack_type => attack_type,
:speed => speed,
:animation => animation,
:block_animation => block_animation,
:range => range,
:button => button == nil ? @styles.size + 1 : button
}
@style_offsets.push style
if MELEE_COMBAT_STYLES.include? style
@style_attacks[style] = Attack.new(animation: animation)
def defaults(props)
@defaults = props
end
##
# Add a new {@link CombatStyle} to to this class.
#
# @param [Symbol] style The name of the style, one of {@link COMBAT_STYLES}.
# @param [Hash] properties
# * :button [Number] The id of the button which activates this style.
# * :attack_type [Symbol] The attack type for this style.
# * :speed [Number] The attack speed for this style.
# * :animation [Number] The attack animation id for this style.
# * :graphic [Hash|Number] The id, or a hash specifying height and id for the graphic for this style.
# * :requirements [Array] An array of [AttackRequirement] objects to be applied to this styles attack.
# * :block_animation [Number] The block animation id for this style.
# * :range [Number] The attack range for this style.
#
# @param [Proc] block An optional block which is called in the context of an AttackDSL if present, to create an attack for
# this style.
def style(style, properties = {}, &block)
fail 'Invalid combat style given' unless COMBAT_STYLES.include? style
properties = @defaults.merge properties
## The config ID used to set the active combat style, typically 0-3. TODO: does this always work?
config = @styles.size
button, attack_type, block_animation = properties[:button], properties[:attack_type], properties[:block_animation]
@styles[style] = CombatStyle.new(button, config, attack_type, block_animation)
if block_given?
attack_dsl = AttackDSL.new
attack_dsl.speed = properties[:speed]
attack_dsl.graphic = properties[:graphic]
attack_dsl.animation = properties[:animation]
attack_dsl.range = properties[:range]
properties[:requirements].each {|requirement| attack_dsl.add_requirement requirement }
attack_dsl.instance_eval &block
@styles[style].attack = attack_dsl.to_attack
end
return unless @styles[style].attack.nil?
## Get rid of any properties which aren't included in the keyword argument list
properties.delete_if {|key| ![:speed, :animation, :range, :requirements, :graphic].include? key }
if @type == :melee
@styles[style].attack = Attack.new(properties)
elsif @type == :ranged
@styles[style].attack = RangedAttack.new(properties)
end
end
def attack(style)
@style_attacks[style]
##
# Get the currently selected combat style in this class for the given mob.
# @param [Mob] mob
# @return [CombatStyle]
def selected_style(mob)
style = @styles.find { |_key, value| value.button == mob.combat_style }
style = @styles.min_by { |_key, value| value.button } if style.nil?
# We want the last element because Hash#find returns [key, value]
style.last
end
def attack_type(style)
@styles[style][:attack_type]
end
##
# Get the animation id for the given animation type.
def block_animation(style)
@styles[style][:block_animation]
end
def button(style)
@styles[style][:button]
end
def config(style)
@style_offsets.find_index {|v| v == style }
end
def other_animation(type)
def animation(type)
@animations[type]
end
def speed(style)
@styles[style][:speed] || default_speed
end
def style_at(button)
selected_style = @styles.select { |key, hash| hash[:button] == button }.keys[0]
if selected_style == nil
selected_style = @styles.min_by { |key, hash| hash[:button] }.first
end
selected_style
end
def default_speed(speed = nil)
unless speed.nil?
@default_speed = speed
end
@default_speed
end
##
# Set the widget config and button for this classes special attack bar.
def special_bar(config, button)
@special_bar_config = config
@special_bar_button = button
end
##
# Check if this weapon class supports a special attack bar.
def special_bar?
!@special_bar_button.nil? and !@special_bar_config.nil?
!@special_bar_button.nil? && !@special_bar_config.nil?
end
##
def animations(stand: nil, walk: nil, run: nil, idle_turn: nil, turn_around: nil, turn_left: nil, turn_right: nil)
@animations = {
:stand => stand,
:walk => walk,
:run => run,
:idle_turn => idle_turn,
:turn_around => turn_around,
:turn_left => turn_left,
:turn_right => turn_right
stand: stand,
walk: walk,
run: run,
idle_turn: idle_turn,
turn_around: turn_around,
turn_left: turn_left,
turn_right: turn_right
}
end
end
@@ -127,8 +174,8 @@ end
WEAPON_CLASSES = {}
WEAPON_CLASS_INTERFACE_MAP = {}
def create_weapon_class(name, widget:, &block)
weapon_class = WeaponClass.new(name, widget)
def create_weapon_class(name, widget:, type: :melee, &block)
weapon_class = WeaponClass.new(name, widget, type)
weapon_class.instance_eval &block
WEAPON_CLASSES[name.to_sym] = weapon_class
-20
View File
@@ -1,20 +0,0 @@
BOW_WIDGET_ID = 10
BOW_SPECIAL_BAR_ID = 10
#
# create_weapon_class :longbow, widget: BOW_WIDGET_ID do
# special_bar = BOW_SPECIAL_BAR_ID
#
#
# add_style :accurate, speed: 6, range: 7
# add_style :rapid, speed: 6, range: 7
# add_style :long_range, speed: 6, range: 9
# end
#
# create_weapon_class :shortbow, widget: BOW_WIDGET_ID do
# special_bar = BOW_SPECIAL_BAR_ID
#
# add_style :accurate, speed: 4, range: 7
# add_style :rapid, speed: 3, range: 7
# add_style :long_range, speed: 4, range: 9
# end
#
-37
View File
@@ -1,37 +0,0 @@
DAGGER_WIDGET_ID = 89
DAGGER_SPECIAL_CONFIG_ID = 12
DAGGER_SPECIAL_BUTTON_ID = 10
create_weapon_class :dagger, widget: DAGGER_WIDGET_ID do
default_speed 4
attack_bonuses crush: -4, magic: 1
defence_bonuses magic: 1
add_style :accurate, attack_type: :stab, animation: 7041, button: 2
add_style :aggressive, attack_type: :stab, animation: 7041, button: 3
add_style :alt_aggressive, attack_type: :slash, animation: 7048, button: 4
add_style :defensive, attack_type: :stab, animation: 7049, button: 5
end
# Need a separate WeaponClass for the dragon dagger because
# of the differing animations
create_weapon_class :dragon_dagger, widget: DAGGER_WIDGET_ID do
default_speed 4
special_bar DAGGER_SPECIAL_CONFIG_ID, DAGGER_SPECIAL_BUTTON_ID
attack_bonuses crush: -4, magic: 1
defence_bonuses magic: 1
add_style :accurate, attack_type: :stab, animation: 402, button: 2
add_style :aggressive, attack_type: :stab, animation: 402, button: 3
add_style :alt_aggressive, attack_type: :slash, animation: 402, button: 4
add_style :defensive, attack_type: :stab, animation: 402, button: 5
end
create_weapon /(?:drag|dragon) dagger.*/, :dragon_dagger do
set_special_attack energy_requirement: 25, animation: 1062, graphic: {id: 252, height: 100} do |source, target|
damage! source, target, CombatUtil::calculate_hit(source, target)
damage! source, target, CombatUtil::calculate_hit(source, target), 1
end
end
@@ -0,0 +1,37 @@
DAGGER_WIDGET_ID = 89
DAGGER_SPECIAL_CONFIG_ID = 12
DAGGER_SPECIAL_BUTTON_ID = 10
create_weapon_class :dagger, widget: DAGGER_WIDGET_ID do
defaults speed: 4, animation: 7041, attack_type: :stab
attack_bonuses crush: -4, magic: 1
defence_bonuses magic: 1
style :accurate, button: 2
style :aggressive, button: 3
style :alt_aggressive, attack_type: :slash, animation: 7048, button: 4
style :defensive, animation: 7049, button: 5
end
# Need a separate WeaponClass for the dragon dagger because
# of the differing animations
create_weapon_class :dragon_dagger, widget: DAGGER_WIDGET_ID do
defaults speed: 4, animation: 402, attack_type: :stab
special_bar DAGGER_SPECIAL_CONFIG_ID, DAGGER_SPECIAL_BUTTON_ID
attack_bonuses crush: -4, magic: 1
defence_bonuses magic: 1
style :accurate, button: 2
style :aggressive, button: 3
style :alt_aggressive, attack_type: :slash, button: 4
style :defensive, button: 5
end
create_weapon /(?:drag|dragon) dagger.*/, :dragon_dagger do
set_special_attack speed: 4, energy_requirement: 25, animation: 1062, graphic: { id: 252, height: 100 } do
damage! delay: 0
damage! delay: 1
end
end
@@ -0,0 +1,9 @@
create_weapon :granite_maul do
attack_bonuses slash: 92, crush: 80
other_bonuses melee_strength: 70
set_special_attack speed: 0, energy_requirement: 60, animation: 3157, graphic: 1225 do |_source, _target|
damage! delay: 0
damage! delay: 1
end
end
@@ -3,16 +3,16 @@ SCIMITAR_SPECIAL_BAR_CONFIG_ID = 21
SCIMITAR_SPECIAL_BAR_BUTTON_ID = 21
create_weapon_class :scimitar, widget: SCIMITAR_WIDGET_ID do
default_speed 4
defaults speed: 4, animation: 390, attack_type: :slash
special_bar SCIMITAR_SPECIAL_BAR_CONFIG_ID, SCIMITAR_SPECIAL_BAR_BUTTON_ID
attack_bonuses crush: -2
defence_bonuses slash: -1
add_style :accurate, attack_type: :slash, animation: 390, button: 2
add_style :aggressive, attack_type: :slash, animation: 390, button: 3
add_style :alt_aggressive, attack_type: :stab, animation: 391, button: 4
add_style :defensive, attack_type: :slash, animation: 390, button: 5
style :accurate, button: 2
style :aggressive, button: 3
style :alt_aggressive, attack_type: :stab, animation: 391, button: 4
style :defensive, button: 5
end
create_weapon :iron_scimitar do
@@ -46,6 +46,6 @@ create_weapon :rune_scimitar do
end
create_weapon :dragon_scimitar do
attack_bonuses :stab => 8, slash: 67
attack_bonuses stab: 8, slash: 67
other_bonuses melee_strength: 66
end
@@ -1,9 +1,9 @@
TWO_HANDED_SWORD_WIDGET_ID = 82
TWO_HANDED_SWORD_WIDGET_ID = 82
TWO_HANDED_SWORD_SPECIAL_CONFIG_ID = 12
TWO_HANDED_SWORD_SPECIAL_BUTTON_ID = 10
create_weapon_class :two_handed_sword, widget: TWO_HANDED_SWORD_WIDGET_ID do
default_speed 7
defaults speed: 7, animation: 7041, attack_type: :slash
special_bar TWO_HANDED_SWORD_SPECIAL_CONFIG_ID, TWO_HANDED_SWORD_SPECIAL_BUTTON_ID
animations stand: 7047, walk: 7046, run: 7039, idle_turn: 7044, turn_around: 7044, turn_left: 7043, turn_right: 7044
@@ -11,10 +11,10 @@ create_weapon_class :two_handed_sword, widget: TWO_HANDED_SWORD_WIDGET_ID do
attack_bonuses stab: -4, magic: -4
defence_bonuses range: -1
add_style :accurate, attack_type: :slash, animation: 7041, button: 2
add_style :aggressive, attack_type: :crush, animation: 7041, button: 3
add_style :alt_aggressive, attack_type: :crush, animation: 7048, button: 4
add_style :defensive, attack_type: :slash, animation: 7049, button: 5
style :accurate, button: 2
style :aggressive, attack_type: :crush, button: 3
style :alt_aggressive, attack_type: :crush, animation: 7048, button: 4
style :defensive, animation: 7049, button: 5
end
create_weapon :iron_2h_sword do
@@ -51,7 +51,7 @@ create_weapon :dragon_2h_sword do
attack_bonuses slash: 92, crush: 80
other_bonuses melee_strength: 70
set_special_attack energy_requirement: 60, animation: 3157, graphic: 1225 do |source, target|
damage! source, target, 5
set_special_attack speed: 7, energy_requirement: 60, animation: 3157, graphic: 1225 do |_source, _target|
damage!
end
end
end
@@ -0,0 +1,25 @@
BOW_WIDGET_ID = 77
BOW_SPECIAL_CONFIG_ID = 10
BOW_SPECIAL_BUTTON_ID = 8
create_weapon_class :shortbow, widget: BOW_WIDGET_ID, type: :ranged do
defaults animation: 426
special_bar BOW_SPECIAL_CONFIG_ID, BOW_SPECIAL_BUTTON_ID
style :accurate, speed: 4, range: 7, button: 2
style :rapid, speed: 3, range: 7, button: 3
style :long_range, speed: 4, range: 9, button: 4
end
create_weapon :shortbow
create_weapon_class :longbow, widget: BOW_WIDGET_ID, type: :ranged do
defaults animation: 426
special_bar BOW_SPECIAL_CONFIG_ID, BOW_SPECIAL_BUTTON_ID
style :accurate, speed: 6, range: 7, button: 2
style :rapid, speed: 6, range: 7, button: 3
style :long_range, speed: 6, range: 9, button: 4
end
create_weapon :longbow
@@ -0,0 +1,11 @@
CROSSBOW_WIDGET_ID = 79
create_weapon_class :crossbow, widget: CROSSBOW_WIDGET_ID, type: :ranged do
defaults animation: 426, speed: 6, range: 7
style :accurate, range: 7, button: 2
style :rapid, speed: 5, button: 3
style :long_range, range: 9, button: 4
end
create_weapon /rune c'bow/, :crossbow
@@ -0,0 +1,8 @@
create_projectile_type :msb, start_height: 41, end_height: 37, delay: 31, speed: 3, slope: 15, radius: 8
create_weapon :magic_shortbow do
set_special_attack speed: 3, range: 7, energy_requirement: 60, animation: 1074, graphic: { id: 256, height: 100 } do
range_damage! projectile: 249, projectile_type: PROJECTILE_TYPES[:msb]
range_damage! projectile: 249, projectile_type: PROJECTILE_TYPES[:msb], projectile_graphic: 256, delay: 1
end
end
+5 -5
View File
@@ -1,11 +1,11 @@
create_weapon_class :no_weapon, widget: 92 do
default_speed 4
defaults speed: 4, animation: 422
add_style :accurate, animation: 422, block_animation: 424
add_style :aggressive, animation: 423, block_animation: 424
add_style :defensive, animation: 422, block_animation: 424
style :accurate, button: 2
style :aggressive, animation: 423, button: 3
style :defensive, button: 4
end
create_weapon :unarmed, :no_weapon, named: true do
# Todo factor out empty blocks
end
end
+8 -13
View File
@@ -5,9 +5,8 @@ java_import 'org.apollo.game.message.impl.SetInterfaceConfigMessage'
java_import 'org.apollo.game.message.impl.ConfigMessage'
java_import 'org.apollo.game.model.inv.SynchronizationInventoryListener'
on :message, :item_option do |player, message|
update_combat_tab(player) if message.option == 2 and message.interface_id == SynchronizationInventoryListener::INVENTORY_ID
update_combat_tab(player) if message.option == 2 && message.interface_id == SynchronizationInventoryListener::INVENTORY_ID
end
on :login do |event|
@@ -15,10 +14,9 @@ on :login do |event|
end
on :message, :item_action do |player, message|
update_combat_tab(player) if message.interface_id == SynchronizationInventoryListener::EQUIPMENT_ID and message.slot == EquipmentConstants::WEAPON
update_combat_tab(player) if message.interface_id == SynchronizationInventoryListener::EQUIPMENT_ID && message.slot == EquipmentConstants::WEAPON
end
on :message, :button do |player, msg|
weapon = EquipmentUtil.equipped_weapon player
weapon_class = weapon.weapon_class
@@ -26,7 +24,7 @@ on :message, :button do |player, msg|
next unless weapon_class_widget == msg.interface_id
if weapon_class.special_bar? and msg.button == weapon_class.special_bar_button
if weapon_class.special_bar? && msg.button == weapon_class.special_bar_button
player.using_special = !player.using_special
update_special_bar(player)
else
@@ -57,14 +55,11 @@ def update_combat_tab(player)
end
def update_combat_style(player)
weapon = EquipmentUtil.equipped_weapon player
weapon_class = weapon.weapon_class
weapon = EquipmentUtil.equipped_weapon player
combat_style = weapon.weapon_class.selected_style player
# Update the combat style in case we had an invalid style selected,
# Update the combat style in case we had an invalid style selected,
# and therefore reverted to the first combat style
selected_style = weapon_class.style_at(player.combat_style)
player.combat_style = weapon_class.button selected_style
player.send ConfigMessage.new(43, weapon_class.config(selected_style))
player.combat_style = combat_style.button
player.send ConfigMessage.new(43, combat_style.config)
end
@@ -20,4 +20,3 @@ on :login do |event|
update_special_bar player
end
end