seh 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +0 -1
- data/Gemfile +2 -2
- data/Gemfile.lock +25 -0
- data/README.md +82 -8
- data/Rakefile +31 -0
- data/examples/epic_battle.rb +240 -0
- data/examples/event.rb +1 -0
- data/examples/event/damage.rb +31 -0
- data/examples/event/hostile.rb +11 -0
- data/examples/event/melee_attack.rb +32 -0
- data/examples/event_color.rb +57 -0
- data/examples/mob.rb +10 -0
- data/lib/seh.rb +4 -7
- data/lib/seh/event.rb +126 -143
- data/lib/seh/event_bind.rb +15 -0
- data/lib/seh/event_bind_disconnector.rb +17 -0
- data/lib/seh/event_target.rb +30 -22
- data/lib/seh/event_type.rb +11 -21
- data/lib/seh/version.rb +1 -1
- data/seh.gemspec +2 -2
- data/spec/seh/event_bind_disconnector_spec.rb +23 -0
- data/spec/seh/event_bind_spec.rb +22 -0
- data/spec/seh/event_spec.rb +209 -0
- data/spec/seh/event_target_spec.rb +87 -0
- data/spec/seh/event_type_spec.rb +134 -0
- data/spec/seh_spec.rb +25 -0
- metadata +23 -18
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
seh (0.2.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.1.3)
|
10
|
+
rspec (2.12.0)
|
11
|
+
rspec-core (~> 2.12.0)
|
12
|
+
rspec-expectations (~> 2.12.0)
|
13
|
+
rspec-mocks (~> 2.12.0)
|
14
|
+
rspec-core (2.12.2)
|
15
|
+
rspec-expectations (2.12.1)
|
16
|
+
diff-lcs (~> 1.1.3)
|
17
|
+
rspec-mocks (2.12.1)
|
18
|
+
|
19
|
+
PLATFORMS
|
20
|
+
java
|
21
|
+
ruby
|
22
|
+
|
23
|
+
DEPENDENCIES
|
24
|
+
rspec (~> 2.12.0)
|
25
|
+
seh!
|
data/README.md
CHANGED
@@ -1,13 +1,87 @@
|
|
1
1
|
|
2
|
-
#
|
2
|
+
# Seh
|
3
|
+
|
4
|
+
Structured event handler. Pure ruby event handling similar to w3c dom events. Check out `examples/epic_battle.rb`
|
5
|
+
|
6
|
+
## Why Seh?
|
7
|
+
|
8
|
+
Seh was developed as a dependency of my ruby game engine. I couldn't find another ruby event library with the features I wanted.
|
9
|
+
|
10
|
+
You might want to use Seh if:
|
11
|
+
* You need events/hooks/triggers/slots+signals
|
12
|
+
* You need a synchronous system, because an event must be fully resolved before things can continue
|
13
|
+
* An object/target/actor/thing might be affected by other things it doesn't know about
|
14
|
+
* Your objects have underlying hierarchical / graph / observer relationships
|
15
|
+
* You're hoping for *complex emergent behaviors*
|
16
|
+
|
17
|
+
You probably don't want to use Seh if:
|
18
|
+
* You need distributed or asynchronous events/hooks/triggers/slots+signals
|
19
|
+
* You're thinking "gee, this seems complicated, I really just need simple hooks" -> Seh is probably over-engineering for you
|
20
|
+
|
21
|
+
## Features
|
22
|
+
|
23
|
+
Seh is driven by `Seh::Event` objects targeting objects of type `Seh::EventTarget`. Events are typed using `Seh::EventType` (which is mostly used implicitly).
|
24
|
+
|
25
|
+
In most event systems, such as w3c dom events in a web browser, events are handled by child nodes before the event bubbles to ancestor nodes. In Seh, each `EventTarget` which sees the `Event` may add callbacks to the event, *before any callbacks on the event are executed*. After each `EventTarget` has been visited, the `Event` executes its callbacks. This allows an ancestor object to affect an outcome on a descendant.
|
26
|
+
|
27
|
+
Major Features
|
28
|
+
* `EventTarget` is a mixin allowing any object to be the target of an `Event`
|
29
|
+
* Create an `Event`, add some targets - objects which mixin `EventTarget` - and dispatch the event. Each `Event` is one-use-only
|
30
|
+
* `Event` has a list of types, e.g. :snow, :bad_weather, :sunshine; Types may be any object which defines equality ==
|
31
|
+
* Bind callbacks to an `EventTarget`, e.g. `my_target.bind(:snow) { "It's snowing!" }`
|
32
|
+
* Bind target callbacks using a type boolean expression, e.g. `my_target.bind( Seh::or :hurricane, Seh::and(:rain, :sunshine, Seh::not(:cold)) ) {
|
33
|
+
"It's either a hurricane, or sunny and raining and not cold." }`
|
34
|
+
* Disconnect callback binds, `my_bind = my_target.bind(:only_needed) { "for awhile" }; my_bind.disconnect`
|
35
|
+
* `EventTarget#observers` defaults to `[]`, and can be overridden to create an object graph. Each `EventTarget` recursively reachable by `EventTarget#observers` receives the event as if it was a top-level target.
|
36
|
+
* `Event` makes it easy for events to "inherit" from one another, so that you may develop a rich event hierarchy on your application side. Note that there's no real ruby class inheritance, e.g. in `examples/event/`, the `Event::damage()` method calls `Event::hostile()`, making each damage event a hostile event.
|
37
|
+
* `Event` inherits from `OpenStruct` to easily define attributes on the event
|
38
|
+
* Event stages provide fine-grained control over callbacks
|
39
|
+
|
40
|
+
## Understanding Event Stages & Callbacks
|
41
|
+
|
42
|
+
See `Seh::Event#dispatch`. When `#dispach` is called:
|
43
|
+
|
44
|
+
1. determine the full set of targets affected by this event
|
45
|
+
2. run callbacks on targets which match this event's types
|
46
|
+
3. run stage callbacks contained in this event; typically targets will append stage callbacks to this event using Event#bind, #start, #finish
|
47
|
+
4. Callback execution order: (1) start callbacks; (2) stage callbacks - in the order stages were added to the event; (3) finish callbacks
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
target = Seh::EventTarget::Default.new # Default includes EventTarget
|
51
|
+
target.bind(:fireball) do |event|
|
52
|
+
puts "1"
|
53
|
+
event.finish { puts "4" }
|
54
|
+
end
|
55
|
+
|
56
|
+
event = Seh::Event.new
|
57
|
+
event.type :fireball
|
58
|
+
event.target target
|
59
|
+
event.start { puts "2" }
|
60
|
+
|
61
|
+
event.add_stage :burn_enemy
|
62
|
+
event.bind(:burn_enemy) { puts "3" }
|
63
|
+
|
64
|
+
event.dispatch
|
65
|
+
```
|
66
|
+
|
67
|
+
The output of the above code is:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
1
|
71
|
+
2
|
72
|
+
3
|
73
|
+
4
|
74
|
+
```
|
3
75
|
|
4
76
|
## Roadmap
|
5
|
-
* documentation
|
6
|
-
* unit tests (my bad)
|
7
|
-
* examples
|
8
77
|
|
9
|
-
|
10
|
-
|
78
|
+
I'm working on efficiency, so tens of thousands of events can be used each second in a game engine. There's a toy benchmark in 'rake benchmark'.
|
79
|
+
|
80
|
+
## Release Notes
|
81
|
+
|
82
|
+
* v0.3.0 - updated api, significantly expanded test coverage, documentation, examples, released under the [BSD license](http://opensource.org/licenses/BSD-3-Clause).
|
83
|
+
* v0.1.0 - an older version of the API that's not backwards-compatible
|
84
|
+
|
85
|
+
## License
|
11
86
|
|
12
|
-
|
13
|
-
Please send (welcome) feedback and bug reports to ryan.berckmans@gmail.com.
|
87
|
+
Seh is released under the [BSD license](http://opensource.org/licenses/BSD-3-Clause).
|
data/Rakefile
CHANGED
@@ -1 +1,32 @@
|
|
1
|
+
require 'rake'
|
1
2
|
require 'bundler/gem_tasks'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
desc "run a simple Seh benchmark"
|
10
|
+
task :benchmark do
|
11
|
+
$:.unshift File.expand_path(File.dirname(__FILE__) + "/examples")
|
12
|
+
require 'seh'
|
13
|
+
require_relative 'examples/mob'
|
14
|
+
require_relative 'examples/event/damage'
|
15
|
+
|
16
|
+
bob = Mob.new
|
17
|
+
fred = Mob.new
|
18
|
+
|
19
|
+
damage_shield = Seh::EventTarget::Default.new
|
20
|
+
damage_shield.bind(:damage) { |event| event.start { event.damage_add -= 0 } }
|
21
|
+
bob.observers << damage_shield
|
22
|
+
|
23
|
+
fred.bind(:hostile) { "oh no, hostile on fred" }
|
24
|
+
|
25
|
+
start_time = Time.now
|
26
|
+
100000.times do
|
27
|
+
e = Seh::Event.new
|
28
|
+
Event::damage e, bob, fred, 0
|
29
|
+
e.dispatch
|
30
|
+
end
|
31
|
+
puts "took: #{Time.now - start_time}"
|
32
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'seh'
|
4
|
+
require_relative 'event'
|
5
|
+
require_relative 'event_color'
|
6
|
+
|
7
|
+
# run ruby examples/epic_battle.rb
|
8
|
+
#
|
9
|
+
# The output is colorized to help visualize the Seh::Event objects.
|
10
|
+
#
|
11
|
+
# This example uses most of the Seh API and simulates a battle between two combatants, Fred and Jim.
|
12
|
+
# Fred and Jim will continue melee-attacking each other until one is dead. Each has abilities which dynamically influence the result of the combat.
|
13
|
+
#
|
14
|
+
# examples/event/hostile.rb is a template for a Seh::Event representing a hostile action
|
15
|
+
#
|
16
|
+
# examples/event/damage.rb is a template for a Seh::Event representing one combatant doing damage to another. damage.rb calls hostile.rb, making each damage event also a hostile event.
|
17
|
+
#
|
18
|
+
# examples/event/melee_attack.rb is a template for a Seh::Event representing an attempted melee attack by one combatant on another. A successfully melee attack will create and dispach a new damage event, representing the damage inflicted by the strike.
|
19
|
+
#
|
20
|
+
# A melee attack may be thwarted by a dodge or riposte, which are modeled as callbacks on melee_attack events. The melee attack event has no direct knowledge that it may be affected by a dodge or riposte.
|
21
|
+
#
|
22
|
+
# Damage may be reduced by the shield of the ancients ability, or reflected by the reflexive barrier ability. The damage event has no direct knowledge of these abilities, which are modeled as callbacks.
|
23
|
+
#
|
24
|
+
# The battle is orated by BenevolentOverlord, a singleton which is wired to Combatant#observers. This causes BenevolentOverlord to receive each event targeting any instance of Combatant.
|
25
|
+
#
|
26
|
+
|
27
|
+
# An observer which reports on the battle. BenevolentOverlord sees each event affecting any Combatant due to Combatant#observers
|
28
|
+
class BenevolentOverlord
|
29
|
+
include Seh::EventTarget
|
30
|
+
def initialize
|
31
|
+
melee_battle_damage_callbacks
|
32
|
+
melee_battle_hostile_callbacks
|
33
|
+
melee_battle_melee_attack_callbacks
|
34
|
+
end
|
35
|
+
|
36
|
+
def melee_battle_melee_attack_callbacks
|
37
|
+
bind :melee_attack do |event|
|
38
|
+
event.bind :melee_miss do
|
39
|
+
puts_color_event event.object_id, "#{event.attacker} misses #{event.defender}. (event id #{event.object_id})"
|
40
|
+
end
|
41
|
+
|
42
|
+
event.bind :melee_hit do
|
43
|
+
puts_color_event event.object_id, "#{event.attacker} successfully hits #{event.defender} for an initial damage of #{event.attack_damage}! (event id #{event.object_id})"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def melee_battle_damage_callbacks
|
49
|
+
bind :damage do |event|
|
50
|
+
event.start { puts_color_event event.object_id, "#{event.damager} tries to do #{event.damage} damage to #{event.receiver} (event id #{event.object_id})" }
|
51
|
+
event.finish { puts_color_event event.object_id, "#{event.damager} did #{event.damage > 0 ? event.damage : 0} damage to #{event.receiver} (event id #{event.object_id})" }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def melee_battle_hostile_callbacks
|
56
|
+
bind :hostile do |event|
|
57
|
+
# hostile is pretty spammy so comment it out
|
58
|
+
# event.start { puts_color_event event.object_id, "hostile start: #{event.aggressor} on #{event.aggressee} (event id #{event.object_id})" }
|
59
|
+
# event.finish { puts_color_event event.object_id, "hostile finish: #{event.aggressor} on #{event.aggressee} (event id #{event.object_id})" }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
INSTANCE = BenevolentOverlord.new
|
64
|
+
end
|
65
|
+
|
66
|
+
class Combatant
|
67
|
+
include Seh::EventTarget
|
68
|
+
|
69
|
+
attr_accessor :name, :hp, :affects
|
70
|
+
def initialize name
|
71
|
+
@name = name
|
72
|
+
@hp = 40
|
73
|
+
@affects = {}
|
74
|
+
end
|
75
|
+
|
76
|
+
def observers
|
77
|
+
[BenevolentOverlord::INSTANCE]
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_s
|
81
|
+
@name
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def coin_flip
|
86
|
+
Random.rand(2) > 0
|
87
|
+
end
|
88
|
+
|
89
|
+
def roll_damage
|
90
|
+
Random.rand(40) + 1
|
91
|
+
end
|
92
|
+
|
93
|
+
class CombatantDied < Exception
|
94
|
+
attr_reader :combatant
|
95
|
+
def initialize combatant
|
96
|
+
@combatant = combatant
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def melee_attack attacker, defender
|
101
|
+
melee_attack_event = Seh::Event.new
|
102
|
+
Event::melee_attack melee_attack_event, attacker, defender, coin_flip, roll_damage
|
103
|
+
melee_attack_event.dispatch
|
104
|
+
raise CombatantDied, attacker if attacker.hp < 1
|
105
|
+
raise CombatantDied, defender if defender.hp < 1
|
106
|
+
end
|
107
|
+
|
108
|
+
############################
|
109
|
+
# rpg/effects.rb
|
110
|
+
|
111
|
+
# grant combatant the ability to dodge melee attacks
|
112
|
+
def dodge combatant
|
113
|
+
puts "#{combatant} gains the ability to dodge"
|
114
|
+
combatant.bind :melee_attack do |event|
|
115
|
+
event.bind :melee_determine_hit do
|
116
|
+
if event.defender == combatant && event.attack_hit && coin_flip
|
117
|
+
event.attack_hit = false
|
118
|
+
event.abort
|
119
|
+
puts_color_event event.object_id, "#{event.attacker} narrowly misses #{event.defender}, who dodges! (event id #{event.object_id})"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# grant combatant the ability to riposte, i.e. to block and counterattack a melee attack
|
126
|
+
def riposte combatant
|
127
|
+
puts "#{combatant} gains the ability to riposte"
|
128
|
+
combatant.bind :melee_attack do |event|
|
129
|
+
event.bind :melee_determine_hit do
|
130
|
+
if event.defender == combatant && event.attack_hit && coin_flip && coin_flip
|
131
|
+
event.attack_hit = false
|
132
|
+
event.abort
|
133
|
+
puts_color_event event.object_id, "#{event.attacker} attacks #{event.defender}, who parries and responds with a lightning-fast riposte! (event id #{event.object_id})"
|
134
|
+
melee_attack event.defender, event.attacker
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# reduce incoming damage
|
141
|
+
SHIELD_AMOUNT = 12
|
142
|
+
def shield_of_the_ancients combatant
|
143
|
+
return if combatant.affects.key? :shield_of_ancients
|
144
|
+
puts "#{combatant} casts shield of the ancients"
|
145
|
+
combatant.affects[:shield_of_ancients] = combatant.bind(:damage) do |event|
|
146
|
+
event.bind :damage_modify do
|
147
|
+
next unless event.receiver == combatant
|
148
|
+
event.damage_reduction += SHIELD_AMOUNT
|
149
|
+
puts_color_event event.object_id, "#{combatant}'s shield of the ancients reduces his damage taken by #{SHIELD_AMOUNT} (event id #{event.object_id})"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# for one damage event, reverse the damage back to the damager
|
155
|
+
def reflexive_barrier combatant
|
156
|
+
return if combatant.affects.key? :reflexive_barrier
|
157
|
+
puts "#{combatant} casts reflexive barrier"
|
158
|
+
combatant.affects[:reflexive_barrier] = combatant.bind(:damage) do |event|
|
159
|
+
event.bind :damage_targets do
|
160
|
+
next unless event.receiver == combatant
|
161
|
+
temp = event.damager
|
162
|
+
event.damager = event.receiver
|
163
|
+
event.receiver = temp
|
164
|
+
puts_color_event event.object_id, "#{combatant} uses his reflexive barrier to reflect damage back at #{event.receiver} (event id #{event.object_id})"
|
165
|
+
combatant.affects[:reflexive_barrier].disconnect
|
166
|
+
combatant.affects.delete :reflexive_barrier
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# disable the next hostile action when the aggressee is the combatant
|
172
|
+
def serenity combatant
|
173
|
+
return if combatant.affects.key? :serenity
|
174
|
+
puts "#{combatant}: casting serenity"
|
175
|
+
combatant.affects[:serenity] = combatant.bind :hostile do |event|
|
176
|
+
next unless event.aggressee == combatant
|
177
|
+
event.abort
|
178
|
+
puts_color_event event.object_id, "#{combatant}'s serenity prevents hostile action by #{event.aggressor} (event id #{event.object_id})"
|
179
|
+
combatant.affects[:serenity].disconnect
|
180
|
+
combatant.affects.delete :serenity
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def permanent_affects combatant
|
185
|
+
shield_of_the_ancients combatant if coin_flip
|
186
|
+
dodge combatant if coin_flip
|
187
|
+
riposte combatant if coin_flip
|
188
|
+
end
|
189
|
+
|
190
|
+
def onetime_affects combatant
|
191
|
+
reflexive_barrier combatant if coin_flip && coin_flip && coin_flip
|
192
|
+
serenity combatant if coin_flip && coin_flip && coin_flip
|
193
|
+
end
|
194
|
+
|
195
|
+
############################
|
196
|
+
# put example all together
|
197
|
+
|
198
|
+
def run
|
199
|
+
def status *combatants
|
200
|
+
s = ''
|
201
|
+
combatants.each { |m| s += m.to_s + ": " + m.hp.to_s + 'hp; ' }
|
202
|
+
puts s
|
203
|
+
end
|
204
|
+
|
205
|
+
2.times do
|
206
|
+
fred = Combatant.new "Fred"
|
207
|
+
jim = Combatant.new "Jim"
|
208
|
+
puts "#{fred} and #{jim} rush into furious combat!"
|
209
|
+
|
210
|
+
permanent_affects fred
|
211
|
+
permanent_affects jim
|
212
|
+
|
213
|
+
begin
|
214
|
+
status fred, jim
|
215
|
+
|
216
|
+
onetime_affects fred
|
217
|
+
onetime_affects jim
|
218
|
+
|
219
|
+
if coin_flip
|
220
|
+
first = fred
|
221
|
+
second = jim
|
222
|
+
else
|
223
|
+
first = jim
|
224
|
+
second = fred
|
225
|
+
end
|
226
|
+
|
227
|
+
melee_attack first, second
|
228
|
+
melee_attack second, first
|
229
|
+
melee_attack first, second if coin_flip
|
230
|
+
melee_attack second, first if coin_flip
|
231
|
+
rescue CombatantDied => died
|
232
|
+
puts "#{died.combatant} died!\n\n"
|
233
|
+
break
|
234
|
+
end while true
|
235
|
+
puts "Myerael, arch-angel of battle, reincarnates #{fred} and #{jim}.."
|
236
|
+
end
|
237
|
+
puts "..and promptly slays them. The end."
|
238
|
+
end
|
239
|
+
|
240
|
+
run if $0 == __FILE__
|
data/examples/event.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Dir[File.dirname(__FILE__) + '/event/*.rb'].each {|file| require file }
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "seh"
|
2
|
+
require_relative "hostile"
|
3
|
+
|
4
|
+
module Event
|
5
|
+
class << self
|
6
|
+
def damage event, damager, receiver, damage
|
7
|
+
hostile event, damager, receiver
|
8
|
+
|
9
|
+
event.target damager, receiver
|
10
|
+
event.type :damage
|
11
|
+
event.damager = damager
|
12
|
+
event.receiver = receiver
|
13
|
+
|
14
|
+
event.damage = damage
|
15
|
+
event.damage_add = 0 # bonus damage
|
16
|
+
event.damage_multiply = 1.0 # % bonus damage and stacks with damage_add
|
17
|
+
event.damage_reduction = 0 # directly subtracted from semifinal damage
|
18
|
+
|
19
|
+
event.add_stage :damage_targets # use stage to modify event.damager/receiver
|
20
|
+
event.add_stage :damage_modify # use stage to modify event.damage_add, damage_multiply, damage_reduction
|
21
|
+
event.add_stage :damage_apply # damage is applied during this stage, do not interfere
|
22
|
+
|
23
|
+
event.bind :damage_apply do
|
24
|
+
final_damage = ((event.damage + event.damage_add) * event.damage_multiply - event.damage_reduction).round
|
25
|
+
event.receiver.hp -= final_damage if final_damage > 0 # as a design decision, don't allow negative damage to heal
|
26
|
+
event.damage = final_damage
|
27
|
+
end
|
28
|
+
event
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|