state_machines-mermaid 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/lib/state_machines/mermaid/renderer.rb +192 -0
- data/lib/state_machines/mermaid/version.rb +7 -0
- data/lib/state_machines-mermaid.rb +9 -0
- data/test/mermaid_renderer_test.rb +208 -0
- data/test/support/models/battle.rb +190 -0
- data/test/support/models/character.rb +183 -0
- data/test/support/models/commander.rb +23 -0
- data/test/support/models/dragon.rb +237 -0
- data/test/support/models/mage.rb +261 -0
- data/test/support/models/regiment.rb +244 -0
- data/test/support/models/troll.rb +167 -0
- data/test/test_helper.rb +17 -0
- metadata +162 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'character'
|
|
4
|
+
|
|
5
|
+
# Mage with spell casting and specialization states
|
|
6
|
+
class Mage < Character
|
|
7
|
+
attr_accessor :spell_power, :focus, :school_of_magic, :mana_pool, :spell_book, :affinities, :masteries, :traits,
|
|
8
|
+
:trials_completed, :current_target, :channel_start_time, :cooldown_end_time
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
super
|
|
12
|
+
@spell_power = 50
|
|
13
|
+
@focus = 100
|
|
14
|
+
@mana_pool = 200
|
|
15
|
+
@spell_book = []
|
|
16
|
+
@known_spells = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Mental state for concentration
|
|
20
|
+
state_machine :concentration, initial: :focused do
|
|
21
|
+
state :focused do
|
|
22
|
+
def spell_critical_chance
|
|
23
|
+
0.2
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
state :distracted do
|
|
28
|
+
def spell_failure_chance
|
|
29
|
+
0.3
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
state :deep_meditation do
|
|
34
|
+
def mana_regeneration_rate
|
|
35
|
+
5.0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def immune_to_interruption?
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
state :interrupted do
|
|
44
|
+
def can_cast?
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
event :meditate do
|
|
50
|
+
transition focused: :deep_meditation, if: -> { mana < 50 && safe_to_meditate? }
|
|
51
|
+
transition distracted: :focused, if: :regain_composure?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
event :disturb do
|
|
55
|
+
transition %i[focused deep_meditation] => :distracted, unless: :iron_will?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
event :break_concentration do
|
|
59
|
+
transition any - :interrupted => :interrupted
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
event :refocus do
|
|
63
|
+
transition %i[distracted interrupted] => :focused, if: :concentration_check?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
after_transition to: :deep_meditation, &:begin_mana_regeneration
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Spell school specialization with complex hierarchy
|
|
70
|
+
state_machine :spell_school, initial: :apprentice do
|
|
71
|
+
# Apprentice level
|
|
72
|
+
state :apprentice
|
|
73
|
+
|
|
74
|
+
# Fire magic tree
|
|
75
|
+
state :fire do
|
|
76
|
+
def elemental_affinity
|
|
77
|
+
:fire
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
state :ember
|
|
81
|
+
state :flame
|
|
82
|
+
state :inferno
|
|
83
|
+
|
|
84
|
+
# Ice magic tree
|
|
85
|
+
state :ice do
|
|
86
|
+
def elemental_affinity
|
|
87
|
+
:ice
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
state :frost
|
|
91
|
+
state :freeze
|
|
92
|
+
state :blizzard
|
|
93
|
+
|
|
94
|
+
# Arcane magic tree
|
|
95
|
+
state :arcane do
|
|
96
|
+
def elemental_affinity
|
|
97
|
+
:arcane
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
state :missile
|
|
101
|
+
state :explosion
|
|
102
|
+
state :singularity
|
|
103
|
+
|
|
104
|
+
# Master of multiple schools
|
|
105
|
+
state :archmage do
|
|
106
|
+
def can_dual_cast?
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Fire progression
|
|
112
|
+
event :study_fire do
|
|
113
|
+
transition apprentice: :ember, if: :has_fire_affinity?
|
|
114
|
+
transition ember: :flame, if: -> { spell_power > 75 && knows_spell?(:fireball) }
|
|
115
|
+
transition flame: :inferno, if: -> { spell_power > 150 && fire_mastery_complete? }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Ice progression
|
|
119
|
+
event :study_ice do
|
|
120
|
+
transition apprentice: :frost, if: :has_ice_affinity?
|
|
121
|
+
transition frost: :freeze, if: -> { spell_power > 75 && knows_spell?(:ice_lance) }
|
|
122
|
+
transition freeze: :blizzard, if: -> { spell_power > 150 && ice_mastery_complete? }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Arcane progression
|
|
126
|
+
event :study_arcane do
|
|
127
|
+
transition apprentice: :missile, if: :has_arcane_affinity?
|
|
128
|
+
transition missile: :explosion, if: -> { spell_power > 75 && knows_spell?(:arcane_blast) }
|
|
129
|
+
transition explosion: :singularity, if: -> { spell_power > 150 && arcane_mastery_complete? }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Become archmage
|
|
133
|
+
event :transcend do
|
|
134
|
+
transition %i[inferno blizzard singularity] => :archmage, if: :worthy_of_archmage?
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
before_transition any => any do |mage, transition|
|
|
138
|
+
mage.update_spell_list(transition.to)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Spell casting state
|
|
143
|
+
state_machine :casting_state, initial: :ready do
|
|
144
|
+
state :ready
|
|
145
|
+
state :preparing
|
|
146
|
+
state :channeling
|
|
147
|
+
state :releasing
|
|
148
|
+
state :cooldown
|
|
149
|
+
|
|
150
|
+
event :begin_cast do
|
|
151
|
+
transition ready: :preparing, if: :has_target?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
event :channel do
|
|
155
|
+
transition preparing: :channeling, if: :channel_started?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
event :release do
|
|
159
|
+
transition channeling: :releasing
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
event :complete do
|
|
163
|
+
transition releasing: :cooldown
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
event :reset do
|
|
167
|
+
transition cooldown: :ready, if: :cooldown_expired?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
after_transition to: :releasing, &:cast_spell!
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Mage-specific methods
|
|
174
|
+
def safe_to_meditate?
|
|
175
|
+
!in_combat? && !enemies_nearby?
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def regain_composure?
|
|
179
|
+
focus > 50
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def iron_will?
|
|
183
|
+
has_trait?(:iron_will) || focus > 150
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def concentration_check?
|
|
187
|
+
rand(100) < focus
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def begin_mana_regeneration
|
|
191
|
+
@regenerating_mana = true
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def has_fire_affinity?
|
|
195
|
+
@affinities&.include?(:fire)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def has_ice_affinity?
|
|
199
|
+
@affinities&.include?(:ice)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def has_arcane_affinity?
|
|
203
|
+
@affinities&.include?(:arcane)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def knows_spell?(spell_name)
|
|
207
|
+
@known_spells[spell_name] == true
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def fire_mastery_complete?
|
|
211
|
+
@masteries && @masteries[:fire] >= 100
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def ice_mastery_complete?
|
|
215
|
+
@masteries && @masteries[:ice] >= 100
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def arcane_mastery_complete?
|
|
219
|
+
@masteries && @masteries[:arcane] >= 100
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def worthy_of_archmage?
|
|
223
|
+
level >= 50 && completed_all_trials?
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def update_spell_list(school)
|
|
227
|
+
# Update available spells based on school
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def has_target?
|
|
231
|
+
@current_target != nil
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def channel_started?
|
|
235
|
+
@channel_start_time != nil
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def cooldown_expired?
|
|
239
|
+
@cooldown_end_time && Time.now > @cooldown_end_time
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def cast_spell!
|
|
243
|
+
# Execute the spell
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def in_combat?
|
|
247
|
+
status == 'combat'
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def enemies_nearby?
|
|
251
|
+
false
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def has_trait?(trait)
|
|
255
|
+
@traits&.include?(trait)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def completed_all_trials?
|
|
259
|
+
@trials_completed && @trials_completed.size >= 5
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Regiment with formation and morale states
|
|
4
|
+
class Regiment
|
|
5
|
+
attr_accessor :morale, :soldiers, :commander, :supplies, :casualties, :battle_experience, :threats, :enemy_positions,
|
|
6
|
+
:immediate_threats, :enemy_activity_level, :defending_critical_position, :battle_start_time, :pike_wall_ready
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@morale = 100
|
|
10
|
+
@soldiers = 100
|
|
11
|
+
@supplies = 100
|
|
12
|
+
@casualties = 0
|
|
13
|
+
@battle_experience = 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
state_machine :formation, initial: :column do
|
|
17
|
+
state :column do
|
|
18
|
+
def movement_speed
|
|
19
|
+
2.0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def defense_rating
|
|
23
|
+
1.0
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
state :line do
|
|
28
|
+
def firepower
|
|
29
|
+
3.0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def defense_rating
|
|
33
|
+
1.5
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
state :square do
|
|
38
|
+
def cavalry_defense
|
|
39
|
+
5.0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def movement_speed
|
|
43
|
+
0.5
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
state :wedge do
|
|
48
|
+
def charge_bonus
|
|
49
|
+
4.0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def breakthrough_chance
|
|
53
|
+
0.7
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
state :scattered do
|
|
58
|
+
def cohesion
|
|
59
|
+
0.1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def rally_difficulty
|
|
63
|
+
10
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
state :circle do
|
|
68
|
+
def all_around_defense
|
|
69
|
+
3.0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def surrounded_bonus
|
|
73
|
+
2.0
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
event :deploy do
|
|
78
|
+
transition column: :line, if: :sufficient_space?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
event :form_square do
|
|
82
|
+
transition %i[column line] => :square, if: :cavalry_threat?
|
|
83
|
+
transition wedge: :square, if: :emergency_defense?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
event :form_wedge do
|
|
87
|
+
transition %i[column line] => :wedge, if: -> { morale > 70 && commander_present? }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
event :form_circle do
|
|
91
|
+
transition %i[line square] => :circle, if: :surrounded?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
event :break_formation do
|
|
95
|
+
transition any - :scattered => :scattered, if: -> { morale < 20 || casualties > 50 }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
event :rally do
|
|
99
|
+
transition scattered: :column, if: -> { commander_alive? && morale > 40 }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
event :reform do
|
|
103
|
+
transition %i[square wedge circle] => :line, unless: :under_immediate_threat?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
before_transition any => :scattered, &:apply_panic_casualties
|
|
107
|
+
|
|
108
|
+
after_transition to: :square, &:prepare_pikes
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Supply state
|
|
112
|
+
state_machine :supply_state, initial: :supplied do
|
|
113
|
+
state :supplied do
|
|
114
|
+
def combat_effectiveness
|
|
115
|
+
1.0
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
state :low_supplies do
|
|
120
|
+
def combat_effectiveness
|
|
121
|
+
0.8
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
state :foraging do
|
|
126
|
+
def vulnerable_to_ambush?
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
state :starving do
|
|
132
|
+
def combat_effectiveness
|
|
133
|
+
0.4
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def desertion_rate
|
|
137
|
+
0.1
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
event :consume_supplies do
|
|
142
|
+
transition supplied: :low_supplies, if: -> { supplies < 30 }
|
|
143
|
+
transition low_supplies: :starving, if: -> { supplies <= 0 }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
event :send_foragers do
|
|
147
|
+
transition %i[low_supplies starving] => :foraging, if: :safe_to_forage?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
event :resupply do
|
|
151
|
+
transition %i[low_supplies foraging starving] => :supplied
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
after_transition on: :resupply, to: :supplied do |regiment|
|
|
155
|
+
regiment.supplies = 100
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
event :foraging_success do
|
|
159
|
+
transition foraging: :low_supplies
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
after_transition on: :foraging_success, to: :low_supplies do |regiment|
|
|
163
|
+
regiment.supplies += 20
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Battle readiness
|
|
168
|
+
state_machine :readiness, initial: :fresh do
|
|
169
|
+
state :fresh
|
|
170
|
+
state :engaged
|
|
171
|
+
state :exhausted
|
|
172
|
+
state :victorious
|
|
173
|
+
state :routed
|
|
174
|
+
|
|
175
|
+
event :enter_battle do
|
|
176
|
+
transition fresh: :engaged
|
|
177
|
+
transition exhausted: :engaged, if: :desperate_defense?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
event :win_engagement do
|
|
181
|
+
transition engaged: :victorious, if: -> { morale > 60 && casualties < 30 }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
event :lose_engagement do
|
|
185
|
+
transition engaged: :routed, if: -> { morale < 30 || casualties > 60 }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
event :exhaust do
|
|
189
|
+
transition engaged: :exhausted, if: -> { battle_duration > 480 }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
event :rest do
|
|
193
|
+
transition %i[exhausted victorious] => :fresh
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def sufficient_space?
|
|
198
|
+
true # Check battlefield conditions
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def cavalry_threat?
|
|
202
|
+
@threats&.include?(:cavalry)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def emergency_defense?
|
|
206
|
+
morale < 40 && surrounded?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def surrounded?
|
|
210
|
+
@enemy_positions && @enemy_positions.count >= 3
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def commander_present?
|
|
214
|
+
commander&.alive? && commander.with_regiment?
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def commander_alive?
|
|
218
|
+
commander&.alive?
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def under_immediate_threat?
|
|
222
|
+
@immediate_threats && !@immediate_threats.empty?
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def apply_panic_casualties
|
|
226
|
+
@casualties += soldiers * 0.1
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def prepare_pikes
|
|
230
|
+
@pike_wall_ready = true
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def safe_to_forage?
|
|
234
|
+
!surrounded? && @enemy_activity_level < 3
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def desperate_defense?
|
|
238
|
+
@defending_critical_position
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def battle_duration
|
|
242
|
+
@battle_start_time ? Time.now - @battle_start_time : 0
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'character'
|
|
4
|
+
|
|
5
|
+
# Troll with regeneration mechanics
|
|
6
|
+
class Troll < Character
|
|
7
|
+
attr_accessor :regeneration_rate, :rage_level, :last_damage_type, :berserker_stacks, :last_fire_damage_time,
|
|
8
|
+
:recovery_started, :health_changes, :enemies_nearby
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
super
|
|
12
|
+
@regeneration_rate = 5
|
|
13
|
+
@rage_level = 0
|
|
14
|
+
@berserker_stacks = 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
state_machine :regeneration, initial: :normal do
|
|
18
|
+
state :normal do
|
|
19
|
+
def regen_multiplier
|
|
20
|
+
1.0
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
state :accelerated do
|
|
25
|
+
def regen_multiplier
|
|
26
|
+
2.0
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
state :berserk do
|
|
31
|
+
def regen_multiplier
|
|
32
|
+
3.0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def damage_taken_reduction
|
|
36
|
+
0.5
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
state :suppressed do
|
|
41
|
+
def regen_multiplier
|
|
42
|
+
0
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
state :dormant do
|
|
47
|
+
def regen_multiplier
|
|
48
|
+
0.1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def appears_dead?
|
|
52
|
+
true
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
event :enrage do
|
|
57
|
+
transition normal: :accelerated, if: -> { health < 75 }
|
|
58
|
+
transition accelerated: :berserk, if: -> { rage_level > 75 && health < 50 }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
event :take_fire_damage do
|
|
62
|
+
transition %i[normal accelerated berserk] => :suppressed
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
event :take_acid_damage do
|
|
66
|
+
transition %i[normal accelerated] => :suppressed
|
|
67
|
+
transition berserk: :accelerated # Berserk provides some resistance
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
event :cool_down do
|
|
71
|
+
transition suppressed: :normal, if: -> { time_since_fire_damage > 60 }
|
|
72
|
+
transition %i[accelerated berserk] => :normal, unless: -> { health < 50 }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
event :play_dead do
|
|
76
|
+
transition %i[normal suppressed] => :dormant, if: -> { health < 10 }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
event :rise_again do
|
|
80
|
+
transition dormant: :normal, if: -> { health > 25 }
|
|
81
|
+
transition dormant: :berserk, if: -> { rage_level > 90 }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
after_transition any => :berserk do |troll|
|
|
85
|
+
troll.regeneration_rate = 20
|
|
86
|
+
troll.berserker_stacks = 5
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
after_transition any => :suppressed do |troll|
|
|
90
|
+
troll.regeneration_rate = 0
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
after_transition to: :dormant do |troll|
|
|
94
|
+
troll.regeneration_rate = 1
|
|
95
|
+
troll.begin_slow_recovery
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
around_transition any => any do |troll, _transition, block|
|
|
99
|
+
health_before = troll.health
|
|
100
|
+
block.call
|
|
101
|
+
health_after = troll.health
|
|
102
|
+
troll.track_health_change(health_before - health_after)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Combat stance
|
|
107
|
+
state_machine :combat_stance, initial: :defensive do
|
|
108
|
+
state :defensive do
|
|
109
|
+
def armor_bonus
|
|
110
|
+
5
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
state :aggressive do
|
|
115
|
+
def attack_bonus
|
|
116
|
+
3
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
state :reckless do
|
|
121
|
+
def attack_bonus
|
|
122
|
+
6
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def armor_penalty
|
|
126
|
+
3
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
event :change_stance do
|
|
131
|
+
transition defensive: :aggressive, if: :confident?
|
|
132
|
+
transition aggressive: :reckless, if: :bloodthirsty?
|
|
133
|
+
transition %i[aggressive reckless] => :defensive, if: :threatened?
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def time_since_fire_damage
|
|
138
|
+
return Float::INFINITY unless @last_fire_damage_time
|
|
139
|
+
|
|
140
|
+
Time.now - @last_fire_damage_time
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def begin_slow_recovery
|
|
144
|
+
@recovery_started = Time.now
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def track_health_change(amount)
|
|
148
|
+
@health_changes ||= []
|
|
149
|
+
@health_changes << { amount: amount, time: Time.now }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def confident?
|
|
153
|
+
health > 75 && berserker_stacks.positive?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def bloodthirsty?
|
|
157
|
+
rage_level > 80 && health > 50
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def threatened?
|
|
161
|
+
health < 30 || surrounded?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def surrounded?
|
|
165
|
+
@enemies_nearby && @enemies_nearby.count > 3
|
|
166
|
+
end
|
|
167
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'minitest/reporters'
|
|
5
|
+
|
|
6
|
+
require 'state_machines/test_helper'
|
|
7
|
+
require 'state_machines-mermaid'
|
|
8
|
+
|
|
9
|
+
StateMachines::Machine.ignore_method_conflicts = true
|
|
10
|
+
|
|
11
|
+
Dir[File.expand_path('support/models/**/*.rb', __dir__)].sort.each { |file| require file }
|
|
12
|
+
|
|
13
|
+
class Minitest::Test
|
|
14
|
+
include StateMachines::TestHelper
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Minitest::Reporters.use!(Minitest::Reporters::ProgressReporter.new)
|