Build a framework for a maintainable combat plugin

* Uses a hierarchy of WeaponClasses -> Weapons for performing attacks,
  with WeaponClass having a set of styles and associated Attacks for
  those styles. Weapons and their classes are built with an easy to use
  and clean DSL.

* Adds a BonusContainer mixin, so that Equipment, Weapons, and
  WeaponClasses can all have their own set of bonuses which apply to the
  player.

* Allows attacks to be queued to the Mobs CombatState instance from
  external code, allowing e.g., NPCs or auto-cast to queue attacks to be
  executed.
This commit is contained in:
Gary Tierney
2016-02-29 21:24:34 +00:00
parent 7731eedf3d
commit 348e5cc8dc
21 changed files with 895 additions and 80 deletions
+88
View File
@@ -0,0 +1,88 @@
java_import 'org.apollo.cache.def.ItemDefinition'
java_import 'org.apollo.game.model.Animation'
java_import 'org.apollo.game.model.Graphic'
class BaseAttack
attr_reader :requirements, :range
def initialize(animation, graphic = nil, range = 1, requirements = [])
@animation = animation
@graphic = graphic
@range = range
@requirements = requirements
end
def do(source, target)
source.play_animation(Animation.new(@animation))
unless @graphic.nil?
if @graphic.is_a?(Hash)
source.play_graphic(Graphic.new(@graphic[:id], @graphic[:delay] || 0, @graphic[:height] | 0))
else
source.play_graphic(Graphic.new(@graphic))
end
end
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
end
end
class ProcAttack < BaseAttack
def initialize(block, animation:, graphic:, range: 1, requirements: [])
super(animation, graphic, range, requirements)
@block = block
end
def apply(source, target)
self.instance_exec(source, target, &@block)
end
end
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)
end
end
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)
end
end
@@ -0,0 +1,56 @@
java_import 'org.apollo.cache.def.ItemDefinition'
class AttackRequirementException < Exception
attr_reader :message
def initialize(message)
@message = message
end
end
class AttackRequirement
def validate!(player)
throw RuntimeError.new('validate! not implemented')
end
def apply(player)
throw RuntimeError.new('apply not implemented')
end
end
class SpecialEnergyRequirement < AttackRequirement
def initialize(amount)
@amount = amount
end
def validate!(player)
throw AttackRequirementException.new('Not enough special attack energy.') unless player.special_energy >= @amount
end
def apply(player)
player.special_energy = player.special_energy - @amount
end
end
class ItemRequirement < AttackRequirement
def initialize(item, amount)
@item = item
@amount = amount
end
def validate!(player)
throw AttackRequirementException.new(item_missing_message) unless player.inventory.get_amount(@item) >= @amount
end
def apply(player)
player.inventory.remove(@item, @amount)
end
private
def item_missing_message
definition = ItemDefinition.lookup(@item)
"You don't have enough #{lookup_item(@item).name}s"
end
end
+29
View File
@@ -0,0 +1,29 @@
module CombatModule
##
# The delay a <i>Mob</i> must wait before attacking again.
declare_attribute(:attack_delay, 0)
##
# A flag indicating whether this <i>Mob</i> is currently in combat.
declare_attribute(:attacking, false)
##
# A flag indicating whether our <i>Mob</i> is dead.
declare_attribute(:dead, false)
##
# The amount of ticks a <i>Player</i> must wait before logging out after combat.
declare_attribute(:logout_timer, Time.now.to_i)
##
# The <i>CombatStyle</i> offset that a <i>Mob</i> is currently using.
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)
##
# An integer between 0 and 100 indicating the amount of special energy a <i>Player</i> has.
declare_attribute(:special_energy, 100, :persistent)
end
+44
View File
@@ -0,0 +1,44 @@
module Combat
# A module for units which can have their own bonuses. E.G., weapons, equipment, ammo.
module BonusContainer
def attack_bonus(type)
@attack_bonuses[type]
end
def defence_bonus(type)
@defence_bonuses[type]
end
def other_bonus(type)
@other_bonuses[type]
end
def other_bonuses(melee_strength: 0, ranged_strength: 0, prayer: 0)
@other_bonuses = {
: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
}
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
}
end
end
end
+12
View File
@@ -0,0 +1,12 @@
on :message, :npc_action do |player, message|
player_combat_state = get_combat_state player
player_combat_state.target = $world.npc_repository.get message.index
unless player.attacking
player.start_action CombatAction.new(player)
end
end
on :message, :player_action do |player, message|
end
+106
View File
@@ -0,0 +1,106 @@
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
class CombatAction < Action
def initialize(source, attack = nil)
super(0, true, source)
@combat_state = get_combat_state(source)
@attack = attack
end
def execute
if @combat_state.target.nil? and @combat_state.queued_attacks.empty?
@combat_state.reset
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
if @combat_state.queued_attacks.empty? and @combat_state.supports_weapon
weapon = EquipmentUtil.equipped_weapon mob
weapon_class = weapon.weapon_class
combat_style = weapon_class.style_at mob.combat_style
if mob.attacking
set_delay weapon_class.speed(combat_style) - 1
else
set_delay 0
mob.attacking = true
end
if mob.using_special and weapon.special_attack?
@combat_state.next_attack = weapon.special_attack
else
@combat_state.next_attack = weapon_class.attack(combat_style)
end
@combat_state.state = :attacking
elsif @combat_state.queued_attacks.size > 0
@combat_state.next_attack = @combat_state.queued_attacks.pop
@combat_state.state = :attacking
else
stop
@combat_state.reset
end
end
def update_attacking
raise RuntimeError.new('no attack when in :attacking state') if @combat_state.next_attack.nil?
begin
@combat_state.next_attack.do(mob, @combat_state.target)
@combat_state.state = :idle
set_delay 0
rescue AttackRequirementException => e
mob.send_message e.message
end
end
def update_chasing
end
def stop
super
@combat_state.reset
mob.attacking = false
puts 'stopped combat action'
end
end
+49
View File
@@ -0,0 +1,49 @@
def get_combat_state(mob)
mob.is_a?(Player) ? type = :player : type = :npc
unless MOB_COMBAT_STATE_CACHE[type].has_key? mob.index
MOB_COMBAT_STATE_CACHE[type][mob.index] = CombatState.new(mob)
end
MOB_COMBAT_STATE_CACHE[type][mob.index]
end
private
class CombatState
attr_accessor :state, :next_attack
attr_reader :queued_attacks, :supports_weapon
def initialize(mob, supports_weapon = true)
@mob = mob
@supports_weapon = supports_weapon
reset
end
def reset
@state = :idle
@target = nil
@next_attack = nil
@queued_attacks = []
@mob.reset_interacting_mob
end
def target
@target
end
def target=(target)
@mob.reset_interacting_mob
@mob.interacting_mob = target
@target = target
end
def queue_attack(attack)
@queued_attacks.push(attack)
end
end
MOB_COMBAT_STATE_CACHE = {
:player => {},
:npc => {}
}
+18
View File
@@ -0,0 +1,18 @@
java_import 'org.apollo.cache.def.EquipmentDefinition'
EQUIPMENT = {}
def create_equipment(item, &block)
equipment = Equipment.new
equipment.instance_eval block
find_entities :item, item do |equipment_item|
EQUIPMENT[id] = equipment
end
end
private
class Equipment
include Combat::BonusContainer
end
+33 -14
View File
@@ -1,17 +1,36 @@
<?xml version="1.0"?>
<plugin>
<id>combat</id>
<version>1</version>
<name>Combat</name>
<description>Manages combat between game characters.</description>
<authors>
<author>Ryley</author>
</authors>
<scripts>
<script>wilderness.rb</script>
</scripts>
<dependencies>
<dependency>attributes</dependency>
<dependency>areas</dependency>
</dependencies>
<id>combat</id>
<version>0.1</version>
<name>combat</name>
<description>Adds fully functioning melee, ranged and magic combat.</description>
<authors>
<author>garyttierney</author>
</authors>
<scripts>
<script>attack.rb</script>
<script>attribute.rb</script>
<script>bonus_container.rb</script>
<script>combat.rb</script>
<script>combat_action.rb</script>
<script>combat_state.rb</script>
<script>equipment.rb</script>
<script>projectile.rb</script>
<script>util.rb</script>
<script>weapon.rb</script>
<script>weapon_class.rb</script>
<script>weapons/bows.rb</script>
<script>weapons/scimitars.rb</script>
<script>weapons/swords.rb</script>
<script>weapons/two_handed_swords.rb</script>
<script>weapons/unarmed.rb</script>
<script>widgets/combat_tab.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>
<dependency>util</dependency>
</dependencies>
</plugin>
+3
View File
@@ -0,0 +1,3 @@
def create_projectile(item, drop_rate:, graphic:, projectile:)
items = i
end
+70
View File
@@ -0,0 +1,70 @@
java_import 'org.apollo.game.model.entity.EquipmentConstants'
class CombatUtil
def self.calculate_max_hit(source)
strength = source.skill_set.get_skill(Skill::STRENGTH)
strength_stat = 5 #source.bonus_stat(:other, :strength)
effective_strength_damage = (strength.current_level) #* prayer_multiplier
if [:aggressive, :alt_aggressive].include? source.combat_style
effective_strength_damage += 3
end
(1.3 + (effective_strength_damage / 10) + (strength_stat / 80) +
((effective_strength_damage * strength_stat) / 640))
end
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
attack_stat = [1, 1].max
defence_stat = [1, 1].max
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_accuracy = attack_stat * attack * attack_prayer_multiplier
attack_accuracy = 0.1 if attack_accuracy < 0
defence_prayer_multiplier = 1 #TODO: Prayer
defence_accuracy = defence_stat * defence * defence_prayer_multiplier
defence_accuracy = 1 if defence_accuracy < 0
base = attack_accuracy / defence_accuracy
base > 1 ? 0.9 : base * 0.9
end
# Calculates a hit for the given <i>Mob</i> and special attack flag.
def self.calculate_hit(source, target)
accuracy = calculate_accuracy source, target
max_hit = calculate_max_hit(source) + 1
if rand <= accuracy
return rand(max_hit)
else
return 0
end
end
end
class EquipmentUtil
def self.equipped_weapon(source)
item = source.equipment.get(EquipmentConstants::WEAPON)
if item.nil?
return NAMED_WEAPONS[:no_weapon]
end
WEAPONS[item.id]
end
def self.equipped_projectile(source)
item = source.equipment.get(EquipmentConstants::ARROWS)
end
end
+135
View File
@@ -0,0 +1,135 @@
java_import 'org.apollo.cache.def.ItemDefinition'
java_import 'org.apollo.game.model.inv.InventoryAdapter'
java_import 'org.apollo.game.model.entity.EquipmentConstants'
java_import 'org.apollo.game.model.entity.AnimationSet'
java_import 'org.apollo.game.model.Animation'
java_import 'org.apollo.game.model.inv.SynchronizationInventoryListener'
WEAPONS = {}
NAMED_WEAPONS = {}
COMBAT_STYLES = [
:accurate,
:aggressive,
:defensive,
:controlled,
:alt_aggressive
]
def create_weapon(identifier, class_name = nil, named: false, &block)
if named
create_named_weapon(identifier, class_name, &block)
else
create_normal_weapon(identifier, class_name, &block)
end
end
private
def create_normal_weapon(item_matcher, class_name = nil, &block)
items = find_entities :item, item_matcher.to_s.gsub(/_/, ' '), -1
items.each do |item_id|
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
else
raise "Couldn't find a suitable weapon class for the given weapon."
end
end
WEAPONS[item_id] = Weapon.new(WEAPON_CLASSES[class_name])
WEAPONS[item_id].instance_eval &block
end
end
def create_named_weapon(name, class_name, &block)
NAMED_WEAPONS[name] = Weapon.new WEAPON_CLASSES[class_name]
NAMED_WEAPONS[name].instance_eval &block
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
attr_reader :weapon_class, :special_attack
include Combat::BonusContainer
def initialize(weapon_class)
@weapon_class = weapon_class
@special_attack = nil
end
def special_attack?
not special_attack.nil?
end
def set_special_attack(energy_requirement:, animation:, graphic: nil, &block)
end
end
def update_weapon_animations(player)
default_animations = AnimationSet::DEFAULT_ANIMATION_SET
player_animations = player.animation_set
player_animations.stand = default_animations.stand
player_animations.walking = default_animations.walking
player_animations.running = default_animations.running
player_animations.idle_turn = default_animations.idle_turn
player_animations.turn_around = default_animations.turn_around
player_animations.turn_left = default_animations.turn_left
player_animations.turn_right = default_animations.turn_right
weapon = EquipmentUtil.equipped_weapon(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)
puts animation
puts 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
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
end
on :login do |event|
update_weapon_animations(event.player)
end
on :message, :item_action do |player, message|
update_weapon_animations(player) if message.interface_id == SynchronizationInventoryListener::EQUIPMENT_ID and message.slot == EquipmentConstants::WEAPON
end
+119
View File
@@ -0,0 +1,119 @@
COMBAT_STYLES = [
:accurate,
:aggressive,
: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
]
class WeaponClass
attr_reader :widget, :name
include Combat::BonusContainer
def initialize(name, widget)
@name = name
@widget = widget
@styles = {}
@style_attacks = {}
@style_offsets = []
@animations = {}
end
def add_style(style, attack_type: nil, speed: nil, animation: nil, block_animation: nil, range: 1)
raise 'Invalid combat style given' unless COMBAT_STYLES.include? style
@styles[style] = {
:attack_type => attack_type,
:speed => speed,
:animation => animation,
:block_animation => block_animation,
:range => range
}
@style_offsets.push style
if MELEE_COMBAT_STYLES.include? style
@style_attacks[style] = Attack.new(animation: animation)
end
end
def attack(style)
@style_attacks[style]
end
def attack_type(style)
@styles[style][:attack_type]
end
def block_animation(style)
@styles[style][:block_animation]
end
def other_animation(type)
@animations[type]
end
def speed(style)
@styles[style][:speed] || default_speed
end
def style_at(offset)
@style_offsets[offset]
end
def default_speed(speed = nil)
unless speed.nil?
@default_speed = speed
end
@default_speed
end
def special_bar(id = nil)
unless id.nil?
@special_bar = id
end
@special_bar
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
}
end
end
WEAPON_CLASSES = {}
WEAPON_CLASS_INTERFACE_MAP = {}
def create_weapon_class(name, widget:, &block)
weapon_class = WeaponClass.new(name, widget)
weapon_class.instance_eval &block
WEAPON_CLASSES[name.to_sym] = weapon_class
end
+20
View File
@@ -0,0 +1,20 @@
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
#
+50
View File
@@ -0,0 +1,50 @@
SCIMITAR_WIDGET_ID = 81
SCIMITAR_SPECIAL_BAR_ID = 21
create_weapon_class :scimitar, widget: SCIMITAR_WIDGET_ID do
default_speed 4
special_bar SCIMITAR_SPECIAL_BAR_ID
attack_bonuses crush: -2
defence_bonuses slash: -1
add_style :accurate, attack_type: :slash, animation: 390
add_style :aggressive, attack_type: :slash, animation: 390
add_style :alt_aggressive, attack_type: :stab, animation: 391
add_style :defensive, attack_type: :slash, animation: 390
end
create_weapon :iron_scimitar do
attack_bonuses stab: 2, slash: 10
other_bonuses melee_strength: 9
end
create_weapon :steel_scimitar do
attack_bonuses stab: 3, slash: 15
other_bonuses melee_strength: 14
end
create_weapon /(black|white) scimitar/ do
attack_bonuses stab: 4, slash: 19
other_bonuses melee_strength: 14
end
create_weapon :mithril_scimitar do
attack_bonuses stab: 5, slash: 21
other_bonuses melee_strength: 20
end
create_weapon :adamant_scimitar do
attack_bonuses stab: 6, slash: 29
other_bonuses melee_strength: 28
end
create_weapon :rune_scimitar do
attack_bonuses stab: 7, slash: 45
other_bonuses melee_strength: 44
end
create_weapon :dragon_scimitar do
attack_bonuses :stab => 8, slash: 67
other_bonuses melee_strength: 66
end
@@ -0,0 +1,52 @@
TWO_HANDED_SWORD_WIDGET_ID = 82
TWO_HANDED_SWORD_SPECIAL_ID = 12
create_weapon_class :two_handed_sword, widget: TWO_HANDED_SWORD_WIDGET_ID do
default_speed 7
special_bar TWO_HANDED_SWORD_SPECIAL_ID
animations stand: 7047, walk: 7046, run: 7039, idle_turn: 7044, turn_around: 7044, turn_left: 7043, turn_right: 7044
attack_bonuses stab: -4, magic: -4
defence_bonuses range: -1
add_style :accurate, attack_type: :slash, animation: 7041
add_style :aggressive, attack_type: :crush, animation: 7041
add_style :alt_aggressive, attack_type: :crush, animation: 7048
add_style :defensive, attack_type: :slash, animation: 7049
end
create_weapon :iron_2h_sword do
attack_bonuses slash: 13, crush: 10
other_bonuses melee_strength: 14
end
create_weapon :steel_2h_sword do
attack_bonuses slash: 21, crush: 16
other_bonuses melee_strength: 22
end
create_weapon /(?:black|white) 2h sword/ do
attack_bonuses slash: 27, crush: 21
other_bonuses melee_strength: 26
end
create_weapon :mithril_2h_sword do
attack_bonuses slash: 30, crush: 24
other_bonuses melee_strength: 26
end
create_weapon :adamant_2h_sword do
attack_bonuses slash: 43, crush: 30
other_bonuses melee_strength: 31
end
create_weapon :rune_2h_sword do
attack_bonuses slash: 69, crush: 50
other_bonuses melee_strength: 70
end
create_weapon :dragon_2h_sword do
attack_bonuses slash: 92, crush: 80
other_bonuses melee_strength: 70
end
+11
View File
@@ -0,0 +1,11 @@
create_weapon_class :unarmed, widget: -1 do
default_speed 4
add_style :accurate, animation: 422, block_animation: 424
add_style :aggressive, animation: 423, block_animation: 424
add_style :defensive, animation: 422, block_animation: 424
end
create_weapon :no_weapon, :unarmed, named: true do
# Todo factor out empty blocks
end
-66
View File
@@ -1,66 +0,0 @@
require 'java'
java_import 'org.apollo.game.model.entity.Player'
java_import 'org.apollo.game.message.impl.SetWidgetTextMessage'
java_import 'org.apollo.game.message.impl.OpenOverlayMessage'
declare_attribute(:wilderness_level, 0, :transient)
# Constants constants related to the wilderness
module WildernessConstants
# The wilderness level overlay interface id
OVERLAY_INTERFACE_ID = 197
# The wilderness level string id
LEVEL_STRING_ID = 199
end
# Determines the wilderness level for the specified position
def wilderness_level(position)
((position.y - 3520) / 8).ceil + 1
end
area_action :wilderness_level do
on_entry do |player, position|
player.wilderness_level = wilderness_level(position)
player.interface_set.open_overlay(WildernessConstants::OVERLAY_INTERFACE_ID)
id = WildernessConstants::LEVEL_STRING_ID
player.send(SetWidgetTextMessage.new(id, "Level: #{player.wilderness_level}"))
show_action(player, ATTACK_ACTION)
end
while_in do |player, position|
current = player.wilderness_level
updated = wilderness_level(position)
if current != updated
player.wilderness_level = updated
id = WildernessConstants::LEVEL_STRING_ID
player.send(SetWidgetTextMessage.new(id, "Level: #{player.wilderness_level}"))
end
end
on_exit do |player, position|
player.wilderness_level = 0
player.interface_set.close
player.send(OpenOverlayMessage.new(-1))
hide_action(player, ATTACK_ACTION)
end
end
# Monkey patch the existing player class to add method of checking whether or not a player is
# within the wilderness
class Player
def in_wilderness
wilderness_level > 0
end
end
area name: :wilderness, coordinates: [2945, 3522, 3390, 3972, 0], actions: :wilderness_level