gemwarrior 0.15.9 → 0.15.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/gemwarrior +146 -146
- data/gemwarrior.gemspec +4 -2
- data/lib/gemwarrior/arena.rb +81 -81
- data/lib/gemwarrior/battle.rb +690 -690
- data/lib/gemwarrior/evaluator.rb +836 -836
- data/lib/gemwarrior/game.rb +191 -191
- data/lib/gemwarrior/game_assets.rb +100 -100
- data/lib/gemwarrior/game_options.rb +15 -15
- data/lib/gemwarrior/inventory.rb +243 -243
- data/lib/gemwarrior/repl.rb +705 -705
- data/lib/gemwarrior/version.rb +6 -6
- metadata +8 -8
data/lib/gemwarrior/battle.rb
CHANGED
@@ -1,690 +1,690 @@
|
|
1
|
-
# lib/gemwarrior/battle.rb
|
2
|
-
# Monster battle
|
3
|
-
|
4
|
-
require_relative 'game_options'
|
5
|
-
|
6
|
-
module Gemwarrior
|
7
|
-
class Battle
|
8
|
-
# CONSTANTS
|
9
|
-
ERROR_ATTACK_OPTION_INVALID = 'That will not do anything against the monster.'
|
10
|
-
BEAST_MODE_ATTACK_LO = 100
|
11
|
-
BEAST_MODE_ATTACK_HI = 200
|
12
|
-
ATTEMPT_SUCCESS_LO_DEFAULT = 0
|
13
|
-
ATTEMPT_SUCCESS_HI_DEFAULT = 100
|
14
|
-
MISS_CAP_DEFAULT = 20
|
15
|
-
LV4_ROCK_SLIDE_MOD_LO = 6
|
16
|
-
LV5_MOD_LO = 7
|
17
|
-
LV5_MOD_HI = 14
|
18
|
-
ESCAPE_TEXT = '** POOF **'
|
19
|
-
|
20
|
-
attr_accessor :world,
|
21
|
-
:player,
|
22
|
-
:monster,
|
23
|
-
:player_is_defending
|
24
|
-
|
25
|
-
def initialize(options)
|
26
|
-
self.world = options.fetch(:world)
|
27
|
-
self.player = options.fetch(:player)
|
28
|
-
self.monster = options.fetch(:monster)
|
29
|
-
self.player_is_defending = false
|
30
|
-
end
|
31
|
-
|
32
|
-
def start(is_arena = false, is_ambush = false)
|
33
|
-
# begin battle!
|
34
|
-
Audio.play_synth(:battle_start)
|
35
|
-
print_battle_header unless is_arena
|
36
|
-
|
37
|
-
# print opponent announcement, depending on reason for battle
|
38
|
-
if is_arena
|
39
|
-
print ' Your opponent is now...'
|
40
|
-
Animation.run(phrase: "#{monster.name.upcase}", speed: :slow, oneline: true)
|
41
|
-
print "!\n"
|
42
|
-
elsif is_ambush
|
43
|
-
puts " You are ambushed by #{monster.name}!".colorize(:yellow)
|
44
|
-
else
|
45
|
-
puts " You decide to attack #{monster.name}!"
|
46
|
-
end
|
47
|
-
|
48
|
-
puts " #{monster.name} cries out: \"#{monster.battlecry}\""
|
49
|
-
|
50
|
-
# first strike! (unless arena or emerald)
|
51
|
-
# shifty woman time, if emerald
|
52
|
-
if monster.name.eql?('emerald')
|
53
|
-
if world.shifty_to_jewel && !world.shifty_has_jeweled
|
54
|
-
puts
|
55
|
-
puts ' Suddenly, the Shifty Woman appears out of nowhere, and stands between you and Emerald!'
|
56
|
-
STDIN.getc
|
57
|
-
print ' '
|
58
|
-
Person.new.speak('Hey, friend. Now it is time to finally get my revenge on ol\' Emer!')
|
59
|
-
STDIN.getc
|
60
|
-
puts ' She brandishes a now-sharpened-to-a-fine-point-and-glowing version of the Sand Jewel you gave to her and plunges it deep into Emerald\'s side, causing him to scream in agony.'
|
61
|
-
puts
|
62
|
-
print ' '
|
63
|
-
Person.new.speak('THAT was for murdering my parents!')
|
64
|
-
STDIN.getc
|
65
|
-
puts ' She uses the hand not gripping the shiny blade to conjure up a small bolt of lightning, which she then flings directly at Emerald\'s chest.'
|
66
|
-
puts
|
67
|
-
print ' '
|
68
|
-
Person.new.speak('And THAT was for stealing the ShinyThing(tm)!')
|
69
|
-
STDIN.getc
|
70
|
-
puts ' Emerald growls mightily, looking quite put off at the turn of events.'
|
71
|
-
STDIN.getc
|
72
|
-
puts ' He is not without strength, however, and pulls the weapon from his body with one hand while conjuring a fireball with his other, sending it right back at the Shifty Woman. Her glee at delivering a seemingly crushing blow is thwarted as she crumples to the floor.'
|
73
|
-
STDIN.getc
|
74
|
-
puts ' Both combatants are breathing heavily and groaning through their injuries. The Shifty Woman looks pretty hurt, and begins eyeing an exit from this mess.'
|
75
|
-
STDIN.getc
|
76
|
-
print ' '
|
77
|
-
Person.new.speak('I may not have been able to finish you off, but my friend here will succeed where I and the land of Jool have failed. Goodbye and good luck!')
|
78
|
-
puts ' The Shifty Woman regains her compsure just enough to limp off to a back door, disappearing.'
|
79
|
-
STDIN.getc
|
80
|
-
|
81
|
-
if GameOptions.data['debug_mode']
|
82
|
-
puts
|
83
|
-
puts ' Emerald Stats Before Altercation'
|
84
|
-
puts " HP : #{monster.hp_cur}"
|
85
|
-
puts " DEF : #{monster.defense}"
|
86
|
-
puts " A_LO : #{monster.atk_lo}"
|
87
|
-
puts " A_HI : #{monster.atk_hi}"
|
88
|
-
end
|
89
|
-
|
90
|
-
monster.hp_cur -= (monster.hp_cur * 0.3).floor
|
91
|
-
monster.defense -= (monster.defense * 0.25).floor
|
92
|
-
monster.atk_lo -= (monster.atk_lo * 0.2).floor
|
93
|
-
monster.atk_hi -= (monster.atk_hi * 0.2).floor
|
94
|
-
|
95
|
-
if GameOptions.data['debug_mode']
|
96
|
-
puts
|
97
|
-
puts ' Emerald Stats After Altercation'
|
98
|
-
puts " HP : #{monster.hp_cur}"
|
99
|
-
puts " DEF : #{monster.defense}"
|
100
|
-
puts " A_LO : #{monster.atk_lo}"
|
101
|
-
puts " A_HI : #{monster.atk_hi}"
|
102
|
-
puts
|
103
|
-
end
|
104
|
-
|
105
|
-
puts ' Emerald has been wounded, but he is not done with this world yet. You approach him, wits about you, ready for battle.'
|
106
|
-
world.shifty_has_jeweled = true
|
107
|
-
end
|
108
|
-
elsif monster_strikes_first?(is_arena, is_ambush)
|
109
|
-
puts " #{monster.name} strikes first!".colorize(:yellow)
|
110
|
-
monster_attacks_player
|
111
|
-
end
|
112
|
-
|
113
|
-
# LV6:STONE_FACE modifier (chance to auto-win)
|
114
|
-
# Doesn't work against bosses, nor if battle is an event or in the arena
|
115
|
-
if player.special_abilities.include?(:stone_face) && !monster.is_boss && !is_ambush && !is_arena
|
116
|
-
level_diff = (player.level - monster.level) * 4
|
117
|
-
chance_range = 0..(30 + level_diff)
|
118
|
-
roll = rand(0..100)
|
119
|
-
|
120
|
-
if GameOptions.data['debug_mode']
|
121
|
-
puts
|
122
|
-
puts " (MOD) LV6: Stone Face"
|
123
|
-
puts " SUCCESS_RANGE: #{chance_range}"
|
124
|
-
puts " SUCCESS_ROLL : #{roll}"
|
125
|
-
end
|
126
|
-
|
127
|
-
if chance_range.include?(roll)
|
128
|
-
puts " You use your STONE FACE to tell the #{monster.name} you mean business, and #{monster.name} just gives up!".colorize(:green)
|
129
|
-
return monster_death
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
# main battle loop
|
134
|
-
loop do
|
135
|
-
skip_next_monster_attack = false
|
136
|
-
|
137
|
-
# 1. check for death to end battle
|
138
|
-
if monster_dead?
|
139
|
-
result = monster_death
|
140
|
-
return result
|
141
|
-
elsif player_dead?
|
142
|
-
player_death
|
143
|
-
return 'death'
|
144
|
-
end
|
145
|
-
|
146
|
-
# 2. print general info and display options prompt
|
147
|
-
print_near_death_info
|
148
|
-
print_combatants_health_info
|
149
|
-
print_battle_options_prompt
|
150
|
-
|
151
|
-
# 3. get player action
|
152
|
-
self.player_is_defending = false
|
153
|
-
player_action = STDIN.gets.chomp.downcase
|
154
|
-
|
155
|
-
# 4. parse player action
|
156
|
-
case player_action
|
157
|
-
when 'fight', 'f', 'attack', 'a'
|
158
|
-
can_attack = true
|
159
|
-
|
160
|
-
# gun exception
|
161
|
-
if player.has_weapon_equipped?
|
162
|
-
if player.inventory.weapon.name.eql?('gun')
|
163
|
-
if player.inventory.contains_item?('bullet')
|
164
|
-
player.inventory.remove_item('bullet')
|
165
|
-
else
|
166
|
-
puts ' Your gun is out of bullets! Running is your best option now.'.colorize(:red)
|
167
|
-
can_attack = false
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
if can_attack
|
173
|
-
puts " You attack #{monster.name}#{player.cur_weapon_name}!"
|
174
|
-
dmg = calculate_damage(player, monster)
|
175
|
-
if dmg > 0
|
176
|
-
Audio.play_synth(:battle_player_attack)
|
177
|
-
|
178
|
-
take_damage(monster, dmg)
|
179
|
-
if monster_dead?
|
180
|
-
result = monster_death
|
181
|
-
return result
|
182
|
-
end
|
183
|
-
else
|
184
|
-
Audio.play_synth(:battle_player_miss)
|
185
|
-
|
186
|
-
puts ' You do no damage!'.colorize(:yellow)
|
187
|
-
end
|
188
|
-
end
|
189
|
-
when 'defend', 'd'
|
190
|
-
puts ' You dig in and defend this round.'
|
191
|
-
self.player_is_defending = true
|
192
|
-
when 'item', 'i'
|
193
|
-
if player.inventory.is_empty?
|
194
|
-
puts ' You have no items!'
|
195
|
-
next
|
196
|
-
elsif player.inventory.contains_battle_item?
|
197
|
-
puts ' Which item do you want to use?'
|
198
|
-
puts
|
199
|
-
b_items = player.inventory.list_battle_items
|
200
|
-
count = 0
|
201
|
-
b_items.each do |bi|
|
202
|
-
puts " (#{count + 1}) #{bi.name}"
|
203
|
-
count += 1
|
204
|
-
end
|
205
|
-
puts ' (x) exit'
|
206
|
-
|
207
|
-
loop do
|
208
|
-
print_battle_prompt
|
209
|
-
answer = gets.chomp.downcase
|
210
|
-
|
211
|
-
case answer
|
212
|
-
when 'x', 'q'
|
213
|
-
skip_next_monster_attack = true
|
214
|
-
break
|
215
|
-
else
|
216
|
-
begin
|
217
|
-
item = b_items[answer.to_i - 1]
|
218
|
-
if item
|
219
|
-
result = item.use(world)
|
220
|
-
player.hp_cur += result[:data]
|
221
|
-
player.inventory.remove_item(item.name) if item.consumable
|
222
|
-
break
|
223
|
-
end
|
224
|
-
rescue
|
225
|
-
puts ' That is not a valid item choice.'
|
226
|
-
puts
|
227
|
-
next
|
228
|
-
end
|
229
|
-
end
|
230
|
-
end
|
231
|
-
else
|
232
|
-
puts ' You have no battle items!'
|
233
|
-
end
|
234
|
-
when 'look', 'l'
|
235
|
-
print " #{monster.name}".colorize(:white)
|
236
|
-
print " (#{monster.hp_cur}/#{monster.hp_max} HP): #{monster.description}\n"
|
237
|
-
puts " It has some distinguishing features, too: face is #{monster.face.colorize(:yellow)}, hands are #{monster.hands.colorize(:yellow)}, and mood, currently, is #{monster.mood.colorize(:yellow)}."
|
238
|
-
if GameOptions.data['debug_mode']
|
239
|
-
puts ' If defeated, will receive:'
|
240
|
-
puts " >> XP : #{monster.xp}"
|
241
|
-
puts " >> ROX : #{monster.rox}"
|
242
|
-
puts " >> ITEMS: #{monster.inventory.contents}"
|
243
|
-
next
|
244
|
-
end
|
245
|
-
when 'pass', 'p'
|
246
|
-
puts ' You decide to pass your turn for some reason. Brave!'
|
247
|
-
when 'run', 'r'
|
248
|
-
if player_escape?(is_arena)
|
249
|
-
monster.hp_cur = monster.hp_max
|
250
|
-
puts " You successfully elude #{monster.name}!".colorize(:green)
|
251
|
-
print_escape_text
|
252
|
-
print_battle_line
|
253
|
-
return
|
254
|
-
else
|
255
|
-
puts ' You were not able to run away! :-('.colorize(:yellow)
|
256
|
-
end
|
257
|
-
else
|
258
|
-
puts " #{ERROR_ATTACK_OPTION_INVALID}"
|
259
|
-
next
|
260
|
-
end
|
261
|
-
|
262
|
-
# 5. parse monster action
|
263
|
-
monster_attacks_player unless skip_next_monster_attack
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
private
|
268
|
-
|
269
|
-
# NEUTRAL
|
270
|
-
def calculate_damage(attacker, defender)
|
271
|
-
# things that modify attack success and damage
|
272
|
-
a_dex = attacker.dexterity
|
273
|
-
d_dex = defender.dexterity
|
274
|
-
d_def = defender.defense
|
275
|
-
|
276
|
-
# player attacking monster
|
277
|
-
if defender.eql?(monster)
|
278
|
-
# attack success defined by attacker's and defender's dexterity
|
279
|
-
attempt_success_lo = ATTEMPT_SUCCESS_LO_DEFAULT
|
280
|
-
attempt_success_hi = ATTEMPT_SUCCESS_HI_DEFAULT
|
281
|
-
|
282
|
-
# success range to hit based on dexterity
|
283
|
-
attempt_success_hi += rand((a_dex)..(a_dex+2))
|
284
|
-
attempt_success_hi -= rand((d_dex)..(d_dex+2))
|
285
|
-
|
286
|
-
# weapon can change dexterity
|
287
|
-
if attacker.has_weapon_equipped?
|
288
|
-
attempt_success_hi += attacker.inventory.weapon.dex_mod
|
289
|
-
end
|
290
|
-
|
291
|
-
# compute attempt success
|
292
|
-
attempt = rand(attempt_success_lo..attempt_success_hi)
|
293
|
-
miss_cap = MISS_CAP_DEFAULT
|
294
|
-
|
295
|
-
######################
|
296
|
-
# ACCURACY MODIFIERS #
|
297
|
-
# vvvvvvvvvvvvvvvvvv #
|
298
|
-
######################
|
299
|
-
|
300
|
-
### LV5:GRANITON modifier (more accuracy)
|
301
|
-
if player.special_abilities.include?(:graniton)
|
302
|
-
miss_cap -= rand(LV5_MOD_LO..LV5_MOD_HI)
|
303
|
-
end
|
304
|
-
|
305
|
-
######################
|
306
|
-
# ^^^^^^^^^^^^^^^^^^ #
|
307
|
-
# ACCURACY MODIFIERS #
|
308
|
-
######################
|
309
|
-
|
310
|
-
# number to beat for attack success
|
311
|
-
success_min = rand(miss_cap..miss_cap + 5)
|
312
|
-
|
313
|
-
if GameOptions.data['debug_mode']
|
314
|
-
puts
|
315
|
-
puts ' Player Roll for Attack:'.colorize(:green)
|
316
|
-
puts " ATTEMPT_RANGE: #{attempt_success_lo}..#{attempt_success_hi}"
|
317
|
-
puts " MUST_BEAT : #{success_min}"
|
318
|
-
if attempt > success_min
|
319
|
-
puts " ATTEMPT_ROLL : #{attempt.to_s.colorize(:green)}"
|
320
|
-
else
|
321
|
-
puts " ATTEMPT_ROLL : #{attempt.to_s.colorize(:red)}"
|
322
|
-
end
|
323
|
-
end
|
324
|
-
|
325
|
-
# no miss, so attack
|
326
|
-
if attempt > success_min
|
327
|
-
# base player damage range
|
328
|
-
base_atk_lo = player.atk_lo
|
329
|
-
base_atk_hi = player.atk_hi
|
330
|
-
|
331
|
-
# weapon increases damage range
|
332
|
-
if player.has_weapon_equipped?
|
333
|
-
base_atk_lo += player.inventory.weapon.atk_lo
|
334
|
-
base_atk_hi += player.inventory.weapon.atk_hi
|
335
|
-
end
|
336
|
-
|
337
|
-
# non-modified damage range
|
338
|
-
dmg_range = base_atk_lo..base_atk_hi
|
339
|
-
|
340
|
-
####################
|
341
|
-
# DAMAGE MODIFIERS #
|
342
|
-
# vvvvvvvvvvvvvvvv #
|
343
|
-
####################
|
344
|
-
|
345
|
-
### DEBUG:BEAST_MODE modifier (ludicrously higher damage range)
|
346
|
-
if GameOptions.data['beast_mode']
|
347
|
-
dmg_range = BEAST_MODE_ATTACK_LO..BEAST_MODE_ATTACK_HI
|
348
|
-
### LV4:ROCK_SLIDE modifier (increased damage range)
|
349
|
-
elsif player.special_abilities.include?(:rock_slide)
|
350
|
-
lo_boost = rand(0..9)
|
351
|
-
hi_boost = lo_boost + rand(5..10)
|
352
|
-
|
353
|
-
if GameOptions.data['debug_mode']
|
354
|
-
puts
|
355
|
-
puts ' (MOD) LV4: Rock Slide'
|
356
|
-
puts " Rock Slide lo_boost: #{lo_boost}"
|
357
|
-
puts " Rock Slide hi_boost: #{hi_boost}"
|
358
|
-
end
|
359
|
-
|
360
|
-
if lo_boost >= LV4_ROCK_SLIDE_MOD_LO
|
361
|
-
puts " You use Rock Slide for added damage!"
|
362
|
-
dmg_range = (player.atk_lo + lo_boost)..(player.atk_hi + hi_boost)
|
363
|
-
else
|
364
|
-
puts " Rock Slide failed :(" if GameOptions.data['debug_mode']
|
365
|
-
end
|
366
|
-
end
|
367
|
-
|
368
|
-
####################
|
369
|
-
# ^^^^^^^^^^^^^^^^ #
|
370
|
-
# DAMAGE MODIFIERS #
|
371
|
-
####################
|
372
|
-
|
373
|
-
# get base damage to apply
|
374
|
-
base_dmg = rand(dmg_range)
|
375
|
-
|
376
|
-
# lower value due to defender's defense
|
377
|
-
dmg = base_dmg - d_def
|
378
|
-
|
379
|
-
# 50% chance of doing 1 point of damage even if you were going to do no damage
|
380
|
-
hail_mary = [dmg, 1].sample
|
381
|
-
dmg = hail_mary if dmg <= 0
|
382
|
-
|
383
|
-
if GameOptions.data['debug_mode']
|
384
|
-
puts
|
385
|
-
puts ' Player Roll for Damage:'.colorize(:green)
|
386
|
-
puts " DMG_RANGE : #{dmg_range}"
|
387
|
-
puts " DMG_ROLL : #{base_dmg}"
|
388
|
-
puts " MONSTER_DEF: #{d_def}"
|
389
|
-
puts " HAIL_MARY : #{hail_mary}"
|
390
|
-
if dmg > 0
|
391
|
-
puts " NET_DMG : #{dmg.to_s.colorize(:green)}"
|
392
|
-
else
|
393
|
-
puts " NET_DMG : #{dmg.to_s.colorize(:red)}"
|
394
|
-
end
|
395
|
-
end
|
396
|
-
|
397
|
-
return dmg
|
398
|
-
# missed? no damage
|
399
|
-
else
|
400
|
-
return 0
|
401
|
-
end
|
402
|
-
# monster attacking player
|
403
|
-
else
|
404
|
-
# attack success defined by attacker's and defender's dexterity
|
405
|
-
attempt_success_lo = ATTEMPT_SUCCESS_LO_DEFAULT
|
406
|
-
attempt_success_hi = ATTEMPT_SUCCESS_HI_DEFAULT
|
407
|
-
|
408
|
-
# success range to hit based on dexterity
|
409
|
-
attempt_success_hi += rand((a_dex)..(a_dex+2))
|
410
|
-
attempt_success_hi -= rand((d_dex)..(d_dex+2))
|
411
|
-
|
412
|
-
# compute attempt success
|
413
|
-
attempt = rand(attempt_success_lo..attempt_success_hi)
|
414
|
-
miss_cap = MISS_CAP_DEFAULT
|
415
|
-
|
416
|
-
# number to beat for attack success
|
417
|
-
success_min = rand(miss_cap..miss_cap + 5)
|
418
|
-
|
419
|
-
if GameOptions.data['debug_mode']
|
420
|
-
puts
|
421
|
-
puts ' Monster Roll for Attack:'.colorize(:red)
|
422
|
-
puts " ATTEMPT_RANGE: #{attempt_success_lo}..#{attempt_success_hi}"
|
423
|
-
puts " MUST_BEAT : #{success_min}"
|
424
|
-
if attempt > success_min
|
425
|
-
puts " ATTEMPT_ROLL : #{attempt.to_s.colorize(:green)}"
|
426
|
-
else
|
427
|
-
puts " ATTEMPT_ROLL : #{attempt.to_s.colorize(:red)}"
|
428
|
-
end
|
429
|
-
end
|
430
|
-
|
431
|
-
# no miss, so attack
|
432
|
-
if attempt > success_min
|
433
|
-
dmg_range = attacker.atk_lo..attacker.atk_lo
|
434
|
-
|
435
|
-
# get base damage to apply
|
436
|
-
base_dmg = rand(dmg_range)
|
437
|
-
|
438
|
-
# lower value for defender's defense (and further if actively defending)
|
439
|
-
if player_is_defending
|
440
|
-
dmg = base_dmg - [(d_def * 1.5).floor, (d_def * 1.5).ceil].sample
|
441
|
-
else
|
442
|
-
dmg = base_dmg - d_def
|
443
|
-
end
|
444
|
-
|
445
|
-
# 25% chance of doing 1 point of damage even if monster was going to do no damage
|
446
|
-
hail_mary = [dmg, dmg, dmg, 1].sample
|
447
|
-
dmg = hail_mary if dmg <= 0
|
448
|
-
|
449
|
-
if GameOptions.data['debug_mode']
|
450
|
-
puts
|
451
|
-
puts ' Monster Roll for Damage:'.colorize(:red)
|
452
|
-
puts " DMG_RANGE : #{dmg_range}"
|
453
|
-
puts " DMG_ROLL : #{base_dmg}"
|
454
|
-
puts " PLAYER_DEF : #{d_def}"
|
455
|
-
puts " HAIL_MARY : #{hail_mary}"
|
456
|
-
if dmg > 0
|
457
|
-
puts " NET_DMG : #{dmg.to_s.colorize(:green)}"
|
458
|
-
else
|
459
|
-
puts " NET_DMG : #{dmg.to_s.colorize(:red)}"
|
460
|
-
end
|
461
|
-
end
|
462
|
-
|
463
|
-
return dmg
|
464
|
-
# missed? no damage
|
465
|
-
else
|
466
|
-
return 0
|
467
|
-
end
|
468
|
-
end
|
469
|
-
end
|
470
|
-
|
471
|
-
def take_damage(entity, dmg)
|
472
|
-
entity.hp_cur = entity.hp_cur.to_i - dmg.to_i
|
473
|
-
who_gets_wounded_start = ''
|
474
|
-
|
475
|
-
if entity.eql?(monster)
|
476
|
-
who_gets_wounded_start = " > You wound #{monster.name} for ".colorize(:green)
|
477
|
-
who_gets_wounded_end = " point(s)!\n".colorize(:green)
|
478
|
-
else
|
479
|
-
who_gets_wounded_start = ' > You are wounded for '.colorize(:red)
|
480
|
-
who_gets_wounded_end = " point(s)!\n".colorize(:red)
|
481
|
-
end
|
482
|
-
|
483
|
-
print who_gets_wounded_start
|
484
|
-
Animation.run(phrase: dmg.to_s, speed: :slow, oneline: true, alpha: false, random: false)
|
485
|
-
print who_gets_wounded_end
|
486
|
-
end
|
487
|
-
|
488
|
-
# MONSTER
|
489
|
-
def monster_strikes_first?(arena_battle = false, event_battle = false)
|
490
|
-
if event_battle || arena_battle
|
491
|
-
return false
|
492
|
-
elsif player.special_abilities.include?(:stone_face)
|
493
|
-
return false
|
494
|
-
elsif (monster.dexterity > player.dexterity)
|
495
|
-
if GameOptions.data['debug_mode']
|
496
|
-
puts
|
497
|
-
puts " MONSTER_DEX: #{monster.dexterity}, PLAYER_DEX: #{player.dexterity}"
|
498
|
-
end
|
499
|
-
|
500
|
-
return true
|
501
|
-
else
|
502
|
-
dex_diff = player.dexterity - monster.dexterity
|
503
|
-
rand_dex = rand(0..dex_diff)
|
504
|
-
|
505
|
-
if GameOptions.data['debug_mode']
|
506
|
-
puts
|
507
|
-
puts " DEX_DIFF : #{dex_diff}"
|
508
|
-
puts " RAND_DEX : #{rand_dex}"
|
509
|
-
puts " RAND_DEX % 2: #{rand_dex % 2}"
|
510
|
-
end
|
511
|
-
|
512
|
-
if rand_dex % 2 > 0
|
513
|
-
return true
|
514
|
-
else
|
515
|
-
return false
|
516
|
-
end
|
517
|
-
end
|
518
|
-
end
|
519
|
-
|
520
|
-
def monster_attacks_player
|
521
|
-
puts " #{monster.name} attacks you!".colorize(:yellow)
|
522
|
-
|
523
|
-
dmg = calculate_damage(monster, player)
|
524
|
-
if dmg > 0
|
525
|
-
Audio.play_synth(:battle_monster_attack)
|
526
|
-
|
527
|
-
take_damage(player, dmg)
|
528
|
-
else
|
529
|
-
Audio.play_synth(:battle_monster_miss)
|
530
|
-
|
531
|
-
puts " #{monster.name} does no damage!".colorize(:yellow)
|
532
|
-
end
|
533
|
-
end
|
534
|
-
|
535
|
-
def monster_near_death?
|
536
|
-
((monster.hp_cur.to_f / monster.hp_max.to_f) < 0.10)
|
537
|
-
end
|
538
|
-
|
539
|
-
def monster_dead?
|
540
|
-
monster.hp_cur <= 0
|
541
|
-
end
|
542
|
-
|
543
|
-
def monster_death
|
544
|
-
puts " YOU HAVE DEFEATED #{monster.name.upcase}!\n".colorize(:green)
|
545
|
-
|
546
|
-
if monster.is_boss
|
547
|
-
# stats
|
548
|
-
world.player.bosses_killed += 1
|
549
|
-
|
550
|
-
if monster.name.eql?('emerald')
|
551
|
-
puts ' You just beat THE FINAL BOSS! Unbelievable!'
|
552
|
-
puts
|
553
|
-
|
554
|
-
reward_player(monster)
|
555
|
-
|
556
|
-
# game ending: initiate
|
557
|
-
return monster.initiate_ending(world)
|
558
|
-
elsif monster.name.eql?('jaspern')
|
559
|
-
Audio.play_synth(:battle_win_boss)
|
560
|
-
|
561
|
-
puts ' You just beat a MINIBOSS! Fantastic!'
|
562
|
-
puts
|
563
|
-
|
564
|
-
reward_player(monster)
|
565
|
-
|
566
|
-
# river bridge boss: initiate
|
567
|
-
return monster.river_bridge_success(world)
|
568
|
-
else
|
569
|
-
Audio.play_synth(:battle_win_boss)
|
570
|
-
|
571
|
-
reward_player(monster)
|
572
|
-
|
573
|
-
world.location_by_coords(player.cur_coords).remove_monster(monster.name)
|
574
|
-
end
|
575
|
-
else
|
576
|
-
Audio.play_synth(:battle_win_monster)
|
577
|
-
|
578
|
-
# stats
|
579
|
-
world.player.monsters_killed += 1
|
580
|
-
|
581
|
-
reward_player(monster)
|
582
|
-
|
583
|
-
world.location_by_coords(player.cur_coords).remove_monster(monster.name)
|
584
|
-
end
|
585
|
-
end
|
586
|
-
|
587
|
-
def reward_player(monster)
|
588
|
-
puts ' You get the following spoils of war:'
|
589
|
-
puts " XP : #{monster.xp}".colorize(:green)
|
590
|
-
puts " ROX : #{monster.rox}".colorize(:green)
|
591
|
-
unless monster.inventory.nil?
|
592
|
-
puts " ITEMS: #{monster.inventory.contents}".colorize(:green) unless monster.inventory.items.empty?
|
593
|
-
end
|
594
|
-
print_battle_line
|
595
|
-
world.player.update_stats(reason: :monster, value: monster)
|
596
|
-
puts
|
597
|
-
end
|
598
|
-
|
599
|
-
# PLAYER
|
600
|
-
def player_near_death?
|
601
|
-
((player.hp_cur.to_f / player.hp_max.to_f) < 0.10 && !GameOptions.data['god_mode'])
|
602
|
-
end
|
603
|
-
|
604
|
-
def player_dead?
|
605
|
-
(player.hp_cur <= 0 && !GameOptions.data['god_mode'])
|
606
|
-
end
|
607
|
-
|
608
|
-
def player_death
|
609
|
-
puts " You are dead, slain by the #{monster.name}!".colorize(:red)
|
610
|
-
print_battle_line
|
611
|
-
end
|
612
|
-
|
613
|
-
def player_escape?(is_arena)
|
614
|
-
unless is_arena
|
615
|
-
if player.dexterity > monster.dexterity
|
616
|
-
return true
|
617
|
-
else
|
618
|
-
dex_diff = monster.dexterity - player.dexterity
|
619
|
-
rand_dex = rand(0..dex_diff)
|
620
|
-
rand_dex += rand(0..1) # slight extra chance modifier
|
621
|
-
|
622
|
-
if rand_dex % 2 > 0
|
623
|
-
return true
|
624
|
-
else
|
625
|
-
return false
|
626
|
-
end
|
627
|
-
end
|
628
|
-
end
|
629
|
-
false
|
630
|
-
end
|
631
|
-
|
632
|
-
# STATUS TEXT
|
633
|
-
def print_near_death_info
|
634
|
-
puts " You are almost dead!\n".colorize(:yellow) if player_near_death?
|
635
|
-
puts " #{monster.name} is almost dead!\n".colorize(:yellow) if monster_near_death?
|
636
|
-
puts
|
637
|
-
end
|
638
|
-
|
639
|
-
def print_combatants_health_info
|
640
|
-
print " #{player.name.upcase.ljust(12).colorize(:green)} :: #{player.hp_cur.to_s.rjust(3)} HP"
|
641
|
-
print " (LVL: #{player.level})" if GameOptions.data['debug_mode']
|
642
|
-
print "\n"
|
643
|
-
|
644
|
-
print " #{monster.name.upcase.ljust(12).colorize(:red)} :: "
|
645
|
-
if GameOptions.data['debug_mode'] || player.special_abilities.include?(:rocking_vision)
|
646
|
-
print "#{monster.hp_cur.to_s.rjust(3)}"
|
647
|
-
else
|
648
|
-
print '???'
|
649
|
-
end
|
650
|
-
print ' HP'
|
651
|
-
print " (LVL: #{monster.level})" if GameOptions.data['debug_mode']
|
652
|
-
print "\n"
|
653
|
-
puts
|
654
|
-
end
|
655
|
-
|
656
|
-
def print_escape_text
|
657
|
-
print ' '
|
658
|
-
Animation.run(phrase: ESCAPE_TEXT, oneline: false)
|
659
|
-
end
|
660
|
-
|
661
|
-
def print_battle_line
|
662
|
-
GameOptions.data['wrap_width'].times { print '*'.colorize(background: :red, color: :white) }
|
663
|
-
print "\n"
|
664
|
-
end
|
665
|
-
|
666
|
-
def print_battle_options_prompt
|
667
|
-
puts ' What do you do?'
|
668
|
-
print ' '
|
669
|
-
print "#{'['.colorize(:yellow)}"
|
670
|
-
print "#{'F'.colorize(:green)}#{'ight]['.colorize(:yellow)}"
|
671
|
-
print "#{'D'.colorize(:green)}#{'efend]['.colorize(:yellow)}"
|
672
|
-
print "#{'L'.colorize(:green)}#{'ook]['.colorize(:yellow)}"
|
673
|
-
print "#{'I'.colorize(:green)}#{'tem]['.colorize(:yellow)}"
|
674
|
-
print "#{'P'.colorize(:green)}#{'ass]['.colorize(:yellow)}"
|
675
|
-
print "#{'R'.colorize(:green)}#{'un]'.colorize(:yellow)}"
|
676
|
-
print "\n"
|
677
|
-
print_battle_prompt
|
678
|
-
end
|
679
|
-
|
680
|
-
def print_battle_prompt
|
681
|
-
print ' [BATTLE]> '
|
682
|
-
end
|
683
|
-
|
684
|
-
def print_battle_header
|
685
|
-
print_battle_line
|
686
|
-
puts ' BATTLE BEGINS!'.ljust(GameOptions.data['wrap_width']).colorize(background: :red, color: :white)
|
687
|
-
print_battle_line
|
688
|
-
end
|
689
|
-
end
|
690
|
-
end
|
1
|
+
# lib/gemwarrior/battle.rb
|
2
|
+
# Monster battle
|
3
|
+
|
4
|
+
require_relative 'game_options'
|
5
|
+
|
6
|
+
module Gemwarrior
|
7
|
+
class Battle
|
8
|
+
# CONSTANTS
|
9
|
+
ERROR_ATTACK_OPTION_INVALID = 'That will not do anything against the monster.'
|
10
|
+
BEAST_MODE_ATTACK_LO = 100
|
11
|
+
BEAST_MODE_ATTACK_HI = 200
|
12
|
+
ATTEMPT_SUCCESS_LO_DEFAULT = 0
|
13
|
+
ATTEMPT_SUCCESS_HI_DEFAULT = 100
|
14
|
+
MISS_CAP_DEFAULT = 20
|
15
|
+
LV4_ROCK_SLIDE_MOD_LO = 6
|
16
|
+
LV5_MOD_LO = 7
|
17
|
+
LV5_MOD_HI = 14
|
18
|
+
ESCAPE_TEXT = '** POOF **'
|
19
|
+
|
20
|
+
attr_accessor :world,
|
21
|
+
:player,
|
22
|
+
:monster,
|
23
|
+
:player_is_defending
|
24
|
+
|
25
|
+
def initialize(options)
|
26
|
+
self.world = options.fetch(:world)
|
27
|
+
self.player = options.fetch(:player)
|
28
|
+
self.monster = options.fetch(:monster)
|
29
|
+
self.player_is_defending = false
|
30
|
+
end
|
31
|
+
|
32
|
+
def start(is_arena = false, is_ambush = false)
|
33
|
+
# begin battle!
|
34
|
+
Audio.play_synth(:battle_start)
|
35
|
+
print_battle_header unless is_arena
|
36
|
+
|
37
|
+
# print opponent announcement, depending on reason for battle
|
38
|
+
if is_arena
|
39
|
+
print ' Your opponent is now...'
|
40
|
+
Animation.run(phrase: "#{monster.name.upcase}", speed: :slow, oneline: true)
|
41
|
+
print "!\n"
|
42
|
+
elsif is_ambush
|
43
|
+
puts " You are ambushed by #{monster.name}!".colorize(:yellow)
|
44
|
+
else
|
45
|
+
puts " You decide to attack #{monster.name}!"
|
46
|
+
end
|
47
|
+
|
48
|
+
puts " #{monster.name} cries out: \"#{monster.battlecry}\""
|
49
|
+
|
50
|
+
# first strike! (unless arena or emerald)
|
51
|
+
# shifty woman time, if emerald
|
52
|
+
if monster.name.eql?('emerald')
|
53
|
+
if world.shifty_to_jewel && !world.shifty_has_jeweled
|
54
|
+
puts
|
55
|
+
puts ' Suddenly, the Shifty Woman appears out of nowhere, and stands between you and Emerald!'
|
56
|
+
STDIN.getc
|
57
|
+
print ' '
|
58
|
+
Person.new.speak('Hey, friend. Now it is time to finally get my revenge on ol\' Emer!')
|
59
|
+
STDIN.getc
|
60
|
+
puts ' She brandishes a now-sharpened-to-a-fine-point-and-glowing version of the Sand Jewel you gave to her and plunges it deep into Emerald\'s side, causing him to scream in agony.'
|
61
|
+
puts
|
62
|
+
print ' '
|
63
|
+
Person.new.speak('THAT was for murdering my parents!')
|
64
|
+
STDIN.getc
|
65
|
+
puts ' She uses the hand not gripping the shiny blade to conjure up a small bolt of lightning, which she then flings directly at Emerald\'s chest.'
|
66
|
+
puts
|
67
|
+
print ' '
|
68
|
+
Person.new.speak('And THAT was for stealing the ShinyThing(tm)!')
|
69
|
+
STDIN.getc
|
70
|
+
puts ' Emerald growls mightily, looking quite put off at the turn of events.'
|
71
|
+
STDIN.getc
|
72
|
+
puts ' He is not without strength, however, and pulls the weapon from his body with one hand while conjuring a fireball with his other, sending it right back at the Shifty Woman. Her glee at delivering a seemingly crushing blow is thwarted as she crumples to the floor.'
|
73
|
+
STDIN.getc
|
74
|
+
puts ' Both combatants are breathing heavily and groaning through their injuries. The Shifty Woman looks pretty hurt, and begins eyeing an exit from this mess.'
|
75
|
+
STDIN.getc
|
76
|
+
print ' '
|
77
|
+
Person.new.speak('I may not have been able to finish you off, but my friend here will succeed where I and the land of Jool have failed. Goodbye and good luck!')
|
78
|
+
puts ' The Shifty Woman regains her compsure just enough to limp off to a back door, disappearing.'
|
79
|
+
STDIN.getc
|
80
|
+
|
81
|
+
if GameOptions.data['debug_mode']
|
82
|
+
puts
|
83
|
+
puts ' Emerald Stats Before Altercation'
|
84
|
+
puts " HP : #{monster.hp_cur}"
|
85
|
+
puts " DEF : #{monster.defense}"
|
86
|
+
puts " A_LO : #{monster.atk_lo}"
|
87
|
+
puts " A_HI : #{monster.atk_hi}"
|
88
|
+
end
|
89
|
+
|
90
|
+
monster.hp_cur -= (monster.hp_cur * 0.3).floor
|
91
|
+
monster.defense -= (monster.defense * 0.25).floor
|
92
|
+
monster.atk_lo -= (monster.atk_lo * 0.2).floor
|
93
|
+
monster.atk_hi -= (monster.atk_hi * 0.2).floor
|
94
|
+
|
95
|
+
if GameOptions.data['debug_mode']
|
96
|
+
puts
|
97
|
+
puts ' Emerald Stats After Altercation'
|
98
|
+
puts " HP : #{monster.hp_cur}"
|
99
|
+
puts " DEF : #{monster.defense}"
|
100
|
+
puts " A_LO : #{monster.atk_lo}"
|
101
|
+
puts " A_HI : #{monster.atk_hi}"
|
102
|
+
puts
|
103
|
+
end
|
104
|
+
|
105
|
+
puts ' Emerald has been wounded, but he is not done with this world yet. You approach him, wits about you, ready for battle.'
|
106
|
+
world.shifty_has_jeweled = true
|
107
|
+
end
|
108
|
+
elsif monster_strikes_first?(is_arena, is_ambush)
|
109
|
+
puts " #{monster.name} strikes first!".colorize(:yellow)
|
110
|
+
monster_attacks_player
|
111
|
+
end
|
112
|
+
|
113
|
+
# LV6:STONE_FACE modifier (chance to auto-win)
|
114
|
+
# Doesn't work against bosses, nor if battle is an event or in the arena
|
115
|
+
if player.special_abilities.include?(:stone_face) && !monster.is_boss && !is_ambush && !is_arena
|
116
|
+
level_diff = (player.level - monster.level) * 4
|
117
|
+
chance_range = 0..(30 + level_diff)
|
118
|
+
roll = rand(0..100)
|
119
|
+
|
120
|
+
if GameOptions.data['debug_mode']
|
121
|
+
puts
|
122
|
+
puts " (MOD) LV6: Stone Face"
|
123
|
+
puts " SUCCESS_RANGE: #{chance_range}"
|
124
|
+
puts " SUCCESS_ROLL : #{roll}"
|
125
|
+
end
|
126
|
+
|
127
|
+
if chance_range.include?(roll)
|
128
|
+
puts " You use your STONE FACE to tell the #{monster.name} you mean business, and #{monster.name} just gives up!".colorize(:green)
|
129
|
+
return monster_death
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# main battle loop
|
134
|
+
loop do
|
135
|
+
skip_next_monster_attack = false
|
136
|
+
|
137
|
+
# 1. check for death to end battle
|
138
|
+
if monster_dead?
|
139
|
+
result = monster_death
|
140
|
+
return result
|
141
|
+
elsif player_dead?
|
142
|
+
player_death
|
143
|
+
return 'death'
|
144
|
+
end
|
145
|
+
|
146
|
+
# 2. print general info and display options prompt
|
147
|
+
print_near_death_info
|
148
|
+
print_combatants_health_info
|
149
|
+
print_battle_options_prompt
|
150
|
+
|
151
|
+
# 3. get player action
|
152
|
+
self.player_is_defending = false
|
153
|
+
player_action = STDIN.gets.chomp.downcase
|
154
|
+
|
155
|
+
# 4. parse player action
|
156
|
+
case player_action
|
157
|
+
when 'fight', 'f', 'attack', 'a'
|
158
|
+
can_attack = true
|
159
|
+
|
160
|
+
# gun exception
|
161
|
+
if player.has_weapon_equipped?
|
162
|
+
if player.inventory.weapon.name.eql?('gun')
|
163
|
+
if player.inventory.contains_item?('bullet')
|
164
|
+
player.inventory.remove_item('bullet')
|
165
|
+
else
|
166
|
+
puts ' Your gun is out of bullets! Running is your best option now.'.colorize(:red)
|
167
|
+
can_attack = false
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
if can_attack
|
173
|
+
puts " You attack #{monster.name}#{player.cur_weapon_name}!"
|
174
|
+
dmg = calculate_damage(player, monster)
|
175
|
+
if dmg > 0
|
176
|
+
Audio.play_synth(:battle_player_attack)
|
177
|
+
|
178
|
+
take_damage(monster, dmg)
|
179
|
+
if monster_dead?
|
180
|
+
result = monster_death
|
181
|
+
return result
|
182
|
+
end
|
183
|
+
else
|
184
|
+
Audio.play_synth(:battle_player_miss)
|
185
|
+
|
186
|
+
puts ' You do no damage!'.colorize(:yellow)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
when 'defend', 'd'
|
190
|
+
puts ' You dig in and defend this round.'
|
191
|
+
self.player_is_defending = true
|
192
|
+
when 'item', 'i'
|
193
|
+
if player.inventory.is_empty?
|
194
|
+
puts ' You have no items!'
|
195
|
+
next
|
196
|
+
elsif player.inventory.contains_battle_item?
|
197
|
+
puts ' Which item do you want to use?'
|
198
|
+
puts
|
199
|
+
b_items = player.inventory.list_battle_items
|
200
|
+
count = 0
|
201
|
+
b_items.each do |bi|
|
202
|
+
puts " (#{count + 1}) #{bi.name}"
|
203
|
+
count += 1
|
204
|
+
end
|
205
|
+
puts ' (x) exit'
|
206
|
+
|
207
|
+
loop do
|
208
|
+
print_battle_prompt
|
209
|
+
answer = gets.chomp.downcase
|
210
|
+
|
211
|
+
case answer
|
212
|
+
when 'x', 'q'
|
213
|
+
skip_next_monster_attack = true
|
214
|
+
break
|
215
|
+
else
|
216
|
+
begin
|
217
|
+
item = b_items[answer.to_i - 1]
|
218
|
+
if item
|
219
|
+
result = item.use(world)
|
220
|
+
player.hp_cur += result[:data]
|
221
|
+
player.inventory.remove_item(item.name) if item.consumable
|
222
|
+
break
|
223
|
+
end
|
224
|
+
rescue
|
225
|
+
puts ' That is not a valid item choice.'
|
226
|
+
puts
|
227
|
+
next
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
else
|
232
|
+
puts ' You have no battle items!'
|
233
|
+
end
|
234
|
+
when 'look', 'l'
|
235
|
+
print " #{monster.name}".colorize(:white)
|
236
|
+
print " (#{monster.hp_cur}/#{monster.hp_max} HP): #{monster.description}\n"
|
237
|
+
puts " It has some distinguishing features, too: face is #{monster.face.colorize(:yellow)}, hands are #{monster.hands.colorize(:yellow)}, and mood, currently, is #{monster.mood.colorize(:yellow)}."
|
238
|
+
if GameOptions.data['debug_mode']
|
239
|
+
puts ' If defeated, will receive:'
|
240
|
+
puts " >> XP : #{monster.xp}"
|
241
|
+
puts " >> ROX : #{monster.rox}"
|
242
|
+
puts " >> ITEMS: #{monster.inventory.contents}"
|
243
|
+
next
|
244
|
+
end
|
245
|
+
when 'pass', 'p'
|
246
|
+
puts ' You decide to pass your turn for some reason. Brave!'
|
247
|
+
when 'run', 'r'
|
248
|
+
if player_escape?(is_arena)
|
249
|
+
monster.hp_cur = monster.hp_max
|
250
|
+
puts " You successfully elude #{monster.name}!".colorize(:green)
|
251
|
+
print_escape_text
|
252
|
+
print_battle_line
|
253
|
+
return
|
254
|
+
else
|
255
|
+
puts ' You were not able to run away! :-('.colorize(:yellow)
|
256
|
+
end
|
257
|
+
else
|
258
|
+
puts " #{ERROR_ATTACK_OPTION_INVALID}"
|
259
|
+
next
|
260
|
+
end
|
261
|
+
|
262
|
+
# 5. parse monster action
|
263
|
+
monster_attacks_player unless skip_next_monster_attack
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
# NEUTRAL
|
270
|
+
def calculate_damage(attacker, defender)
|
271
|
+
# things that modify attack success and damage
|
272
|
+
a_dex = attacker.dexterity
|
273
|
+
d_dex = defender.dexterity
|
274
|
+
d_def = defender.defense
|
275
|
+
|
276
|
+
# player attacking monster
|
277
|
+
if defender.eql?(monster)
|
278
|
+
# attack success defined by attacker's and defender's dexterity
|
279
|
+
attempt_success_lo = ATTEMPT_SUCCESS_LO_DEFAULT
|
280
|
+
attempt_success_hi = ATTEMPT_SUCCESS_HI_DEFAULT
|
281
|
+
|
282
|
+
# success range to hit based on dexterity
|
283
|
+
attempt_success_hi += rand((a_dex)..(a_dex+2))
|
284
|
+
attempt_success_hi -= rand((d_dex)..(d_dex+2))
|
285
|
+
|
286
|
+
# weapon can change dexterity
|
287
|
+
if attacker.has_weapon_equipped?
|
288
|
+
attempt_success_hi += attacker.inventory.weapon.dex_mod
|
289
|
+
end
|
290
|
+
|
291
|
+
# compute attempt success
|
292
|
+
attempt = rand(attempt_success_lo..attempt_success_hi)
|
293
|
+
miss_cap = MISS_CAP_DEFAULT
|
294
|
+
|
295
|
+
######################
|
296
|
+
# ACCURACY MODIFIERS #
|
297
|
+
# vvvvvvvvvvvvvvvvvv #
|
298
|
+
######################
|
299
|
+
|
300
|
+
### LV5:GRANITON modifier (more accuracy)
|
301
|
+
if player.special_abilities.include?(:graniton)
|
302
|
+
miss_cap -= rand(LV5_MOD_LO..LV5_MOD_HI)
|
303
|
+
end
|
304
|
+
|
305
|
+
######################
|
306
|
+
# ^^^^^^^^^^^^^^^^^^ #
|
307
|
+
# ACCURACY MODIFIERS #
|
308
|
+
######################
|
309
|
+
|
310
|
+
# number to beat for attack success
|
311
|
+
success_min = rand(miss_cap..miss_cap + 5)
|
312
|
+
|
313
|
+
if GameOptions.data['debug_mode']
|
314
|
+
puts
|
315
|
+
puts ' Player Roll for Attack:'.colorize(:green)
|
316
|
+
puts " ATTEMPT_RANGE: #{attempt_success_lo}..#{attempt_success_hi}"
|
317
|
+
puts " MUST_BEAT : #{success_min}"
|
318
|
+
if attempt > success_min
|
319
|
+
puts " ATTEMPT_ROLL : #{attempt.to_s.colorize(:green)}"
|
320
|
+
else
|
321
|
+
puts " ATTEMPT_ROLL : #{attempt.to_s.colorize(:red)}"
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# no miss, so attack
|
326
|
+
if attempt > success_min
|
327
|
+
# base player damage range
|
328
|
+
base_atk_lo = player.atk_lo
|
329
|
+
base_atk_hi = player.atk_hi
|
330
|
+
|
331
|
+
# weapon increases damage range
|
332
|
+
if player.has_weapon_equipped?
|
333
|
+
base_atk_lo += player.inventory.weapon.atk_lo
|
334
|
+
base_atk_hi += player.inventory.weapon.atk_hi
|
335
|
+
end
|
336
|
+
|
337
|
+
# non-modified damage range
|
338
|
+
dmg_range = base_atk_lo..base_atk_hi
|
339
|
+
|
340
|
+
####################
|
341
|
+
# DAMAGE MODIFIERS #
|
342
|
+
# vvvvvvvvvvvvvvvv #
|
343
|
+
####################
|
344
|
+
|
345
|
+
### DEBUG:BEAST_MODE modifier (ludicrously higher damage range)
|
346
|
+
if GameOptions.data['beast_mode']
|
347
|
+
dmg_range = BEAST_MODE_ATTACK_LO..BEAST_MODE_ATTACK_HI
|
348
|
+
### LV4:ROCK_SLIDE modifier (increased damage range)
|
349
|
+
elsif player.special_abilities.include?(:rock_slide)
|
350
|
+
lo_boost = rand(0..9)
|
351
|
+
hi_boost = lo_boost + rand(5..10)
|
352
|
+
|
353
|
+
if GameOptions.data['debug_mode']
|
354
|
+
puts
|
355
|
+
puts ' (MOD) LV4: Rock Slide'
|
356
|
+
puts " Rock Slide lo_boost: #{lo_boost}"
|
357
|
+
puts " Rock Slide hi_boost: #{hi_boost}"
|
358
|
+
end
|
359
|
+
|
360
|
+
if lo_boost >= LV4_ROCK_SLIDE_MOD_LO
|
361
|
+
puts " You use Rock Slide for added damage!"
|
362
|
+
dmg_range = (player.atk_lo + lo_boost)..(player.atk_hi + hi_boost)
|
363
|
+
else
|
364
|
+
puts " Rock Slide failed :(" if GameOptions.data['debug_mode']
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
####################
|
369
|
+
# ^^^^^^^^^^^^^^^^ #
|
370
|
+
# DAMAGE MODIFIERS #
|
371
|
+
####################
|
372
|
+
|
373
|
+
# get base damage to apply
|
374
|
+
base_dmg = rand(dmg_range)
|
375
|
+
|
376
|
+
# lower value due to defender's defense
|
377
|
+
dmg = base_dmg - d_def
|
378
|
+
|
379
|
+
# 50% chance of doing 1 point of damage even if you were going to do no damage
|
380
|
+
hail_mary = [dmg, 1].sample
|
381
|
+
dmg = hail_mary if dmg <= 0
|
382
|
+
|
383
|
+
if GameOptions.data['debug_mode']
|
384
|
+
puts
|
385
|
+
puts ' Player Roll for Damage:'.colorize(:green)
|
386
|
+
puts " DMG_RANGE : #{dmg_range}"
|
387
|
+
puts " DMG_ROLL : #{base_dmg}"
|
388
|
+
puts " MONSTER_DEF: #{d_def}"
|
389
|
+
puts " HAIL_MARY : #{hail_mary}"
|
390
|
+
if dmg > 0
|
391
|
+
puts " NET_DMG : #{dmg.to_s.colorize(:green)}"
|
392
|
+
else
|
393
|
+
puts " NET_DMG : #{dmg.to_s.colorize(:red)}"
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
return dmg
|
398
|
+
# missed? no damage
|
399
|
+
else
|
400
|
+
return 0
|
401
|
+
end
|
402
|
+
# monster attacking player
|
403
|
+
else
|
404
|
+
# attack success defined by attacker's and defender's dexterity
|
405
|
+
attempt_success_lo = ATTEMPT_SUCCESS_LO_DEFAULT
|
406
|
+
attempt_success_hi = ATTEMPT_SUCCESS_HI_DEFAULT
|
407
|
+
|
408
|
+
# success range to hit based on dexterity
|
409
|
+
attempt_success_hi += rand((a_dex)..(a_dex+2))
|
410
|
+
attempt_success_hi -= rand((d_dex)..(d_dex+2))
|
411
|
+
|
412
|
+
# compute attempt success
|
413
|
+
attempt = rand(attempt_success_lo..attempt_success_hi)
|
414
|
+
miss_cap = MISS_CAP_DEFAULT
|
415
|
+
|
416
|
+
# number to beat for attack success
|
417
|
+
success_min = rand(miss_cap..miss_cap + 5)
|
418
|
+
|
419
|
+
if GameOptions.data['debug_mode']
|
420
|
+
puts
|
421
|
+
puts ' Monster Roll for Attack:'.colorize(:red)
|
422
|
+
puts " ATTEMPT_RANGE: #{attempt_success_lo}..#{attempt_success_hi}"
|
423
|
+
puts " MUST_BEAT : #{success_min}"
|
424
|
+
if attempt > success_min
|
425
|
+
puts " ATTEMPT_ROLL : #{attempt.to_s.colorize(:green)}"
|
426
|
+
else
|
427
|
+
puts " ATTEMPT_ROLL : #{attempt.to_s.colorize(:red)}"
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
# no miss, so attack
|
432
|
+
if attempt > success_min
|
433
|
+
dmg_range = attacker.atk_lo..attacker.atk_lo
|
434
|
+
|
435
|
+
# get base damage to apply
|
436
|
+
base_dmg = rand(dmg_range)
|
437
|
+
|
438
|
+
# lower value for defender's defense (and further if actively defending)
|
439
|
+
if player_is_defending
|
440
|
+
dmg = base_dmg - [(d_def * 1.5).floor, (d_def * 1.5).ceil].sample
|
441
|
+
else
|
442
|
+
dmg = base_dmg - d_def
|
443
|
+
end
|
444
|
+
|
445
|
+
# 25% chance of doing 1 point of damage even if monster was going to do no damage
|
446
|
+
hail_mary = [dmg, dmg, dmg, 1].sample
|
447
|
+
dmg = hail_mary if dmg <= 0
|
448
|
+
|
449
|
+
if GameOptions.data['debug_mode']
|
450
|
+
puts
|
451
|
+
puts ' Monster Roll for Damage:'.colorize(:red)
|
452
|
+
puts " DMG_RANGE : #{dmg_range}"
|
453
|
+
puts " DMG_ROLL : #{base_dmg}"
|
454
|
+
puts " PLAYER_DEF : #{d_def}"
|
455
|
+
puts " HAIL_MARY : #{hail_mary}"
|
456
|
+
if dmg > 0
|
457
|
+
puts " NET_DMG : #{dmg.to_s.colorize(:green)}"
|
458
|
+
else
|
459
|
+
puts " NET_DMG : #{dmg.to_s.colorize(:red)}"
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
return dmg
|
464
|
+
# missed? no damage
|
465
|
+
else
|
466
|
+
return 0
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
def take_damage(entity, dmg)
|
472
|
+
entity.hp_cur = entity.hp_cur.to_i - dmg.to_i
|
473
|
+
who_gets_wounded_start = ''
|
474
|
+
|
475
|
+
if entity.eql?(monster)
|
476
|
+
who_gets_wounded_start = " > You wound #{monster.name} for ".colorize(:green)
|
477
|
+
who_gets_wounded_end = " point(s)!\n".colorize(:green)
|
478
|
+
else
|
479
|
+
who_gets_wounded_start = ' > You are wounded for '.colorize(:red)
|
480
|
+
who_gets_wounded_end = " point(s)!\n".colorize(:red)
|
481
|
+
end
|
482
|
+
|
483
|
+
print who_gets_wounded_start
|
484
|
+
Animation.run(phrase: dmg.to_s, speed: :slow, oneline: true, alpha: false, random: false)
|
485
|
+
print who_gets_wounded_end
|
486
|
+
end
|
487
|
+
|
488
|
+
# MONSTER
|
489
|
+
def monster_strikes_first?(arena_battle = false, event_battle = false)
|
490
|
+
if event_battle || arena_battle
|
491
|
+
return false
|
492
|
+
elsif player.special_abilities.include?(:stone_face)
|
493
|
+
return false
|
494
|
+
elsif (monster.dexterity > player.dexterity)
|
495
|
+
if GameOptions.data['debug_mode']
|
496
|
+
puts
|
497
|
+
puts " MONSTER_DEX: #{monster.dexterity}, PLAYER_DEX: #{player.dexterity}"
|
498
|
+
end
|
499
|
+
|
500
|
+
return true
|
501
|
+
else
|
502
|
+
dex_diff = player.dexterity - monster.dexterity
|
503
|
+
rand_dex = rand(0..dex_diff)
|
504
|
+
|
505
|
+
if GameOptions.data['debug_mode']
|
506
|
+
puts
|
507
|
+
puts " DEX_DIFF : #{dex_diff}"
|
508
|
+
puts " RAND_DEX : #{rand_dex}"
|
509
|
+
puts " RAND_DEX % 2: #{rand_dex % 2}"
|
510
|
+
end
|
511
|
+
|
512
|
+
if rand_dex % 2 > 0
|
513
|
+
return true
|
514
|
+
else
|
515
|
+
return false
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
def monster_attacks_player
|
521
|
+
puts " #{monster.name} attacks you!".colorize(:yellow)
|
522
|
+
|
523
|
+
dmg = calculate_damage(monster, player)
|
524
|
+
if dmg > 0
|
525
|
+
Audio.play_synth(:battle_monster_attack)
|
526
|
+
|
527
|
+
take_damage(player, dmg)
|
528
|
+
else
|
529
|
+
Audio.play_synth(:battle_monster_miss)
|
530
|
+
|
531
|
+
puts " #{monster.name} does no damage!".colorize(:yellow)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
def monster_near_death?
|
536
|
+
((monster.hp_cur.to_f / monster.hp_max.to_f) < 0.10)
|
537
|
+
end
|
538
|
+
|
539
|
+
def monster_dead?
|
540
|
+
monster.hp_cur <= 0
|
541
|
+
end
|
542
|
+
|
543
|
+
def monster_death
|
544
|
+
puts " YOU HAVE DEFEATED #{monster.name.upcase}!\n".colorize(:green)
|
545
|
+
|
546
|
+
if monster.is_boss
|
547
|
+
# stats
|
548
|
+
world.player.bosses_killed += 1
|
549
|
+
|
550
|
+
if monster.name.eql?('emerald')
|
551
|
+
puts ' You just beat THE FINAL BOSS! Unbelievable!'
|
552
|
+
puts
|
553
|
+
|
554
|
+
reward_player(monster)
|
555
|
+
|
556
|
+
# game ending: initiate
|
557
|
+
return monster.initiate_ending(world)
|
558
|
+
elsif monster.name.eql?('jaspern')
|
559
|
+
Audio.play_synth(:battle_win_boss)
|
560
|
+
|
561
|
+
puts ' You just beat a MINIBOSS! Fantastic!'
|
562
|
+
puts
|
563
|
+
|
564
|
+
reward_player(monster)
|
565
|
+
|
566
|
+
# river bridge boss: initiate
|
567
|
+
return monster.river_bridge_success(world)
|
568
|
+
else
|
569
|
+
Audio.play_synth(:battle_win_boss)
|
570
|
+
|
571
|
+
reward_player(monster)
|
572
|
+
|
573
|
+
world.location_by_coords(player.cur_coords).remove_monster(monster.name)
|
574
|
+
end
|
575
|
+
else
|
576
|
+
Audio.play_synth(:battle_win_monster)
|
577
|
+
|
578
|
+
# stats
|
579
|
+
world.player.monsters_killed += 1
|
580
|
+
|
581
|
+
reward_player(monster)
|
582
|
+
|
583
|
+
world.location_by_coords(player.cur_coords).remove_monster(monster.name)
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
def reward_player(monster)
|
588
|
+
puts ' You get the following spoils of war:'
|
589
|
+
puts " XP : #{monster.xp}".colorize(:green)
|
590
|
+
puts " ROX : #{monster.rox}".colorize(:green)
|
591
|
+
unless monster.inventory.nil?
|
592
|
+
puts " ITEMS: #{monster.inventory.contents}".colorize(:green) unless monster.inventory.items.empty?
|
593
|
+
end
|
594
|
+
print_battle_line
|
595
|
+
world.player.update_stats(reason: :monster, value: monster)
|
596
|
+
puts
|
597
|
+
end
|
598
|
+
|
599
|
+
# PLAYER
|
600
|
+
def player_near_death?
|
601
|
+
((player.hp_cur.to_f / player.hp_max.to_f) < 0.10 && !GameOptions.data['god_mode'])
|
602
|
+
end
|
603
|
+
|
604
|
+
def player_dead?
|
605
|
+
(player.hp_cur <= 0 && !GameOptions.data['god_mode'])
|
606
|
+
end
|
607
|
+
|
608
|
+
def player_death
|
609
|
+
puts " You are dead, slain by the #{monster.name}!".colorize(:red)
|
610
|
+
print_battle_line
|
611
|
+
end
|
612
|
+
|
613
|
+
def player_escape?(is_arena)
|
614
|
+
unless is_arena
|
615
|
+
if player.dexterity > monster.dexterity
|
616
|
+
return true
|
617
|
+
else
|
618
|
+
dex_diff = monster.dexterity - player.dexterity
|
619
|
+
rand_dex = rand(0..dex_diff)
|
620
|
+
rand_dex += rand(0..1) # slight extra chance modifier
|
621
|
+
|
622
|
+
if rand_dex % 2 > 0
|
623
|
+
return true
|
624
|
+
else
|
625
|
+
return false
|
626
|
+
end
|
627
|
+
end
|
628
|
+
end
|
629
|
+
false
|
630
|
+
end
|
631
|
+
|
632
|
+
# STATUS TEXT
|
633
|
+
def print_near_death_info
|
634
|
+
puts " You are almost dead!\n".colorize(:yellow) if player_near_death?
|
635
|
+
puts " #{monster.name} is almost dead!\n".colorize(:yellow) if monster_near_death?
|
636
|
+
puts
|
637
|
+
end
|
638
|
+
|
639
|
+
def print_combatants_health_info
|
640
|
+
print " #{player.name.upcase.ljust(12).colorize(:green)} :: #{player.hp_cur.to_s.rjust(3)} HP"
|
641
|
+
print " (LVL: #{player.level})" if GameOptions.data['debug_mode']
|
642
|
+
print "\n"
|
643
|
+
|
644
|
+
print " #{monster.name.upcase.ljust(12).colorize(:red)} :: "
|
645
|
+
if GameOptions.data['debug_mode'] || player.special_abilities.include?(:rocking_vision)
|
646
|
+
print "#{monster.hp_cur.to_s.rjust(3)}"
|
647
|
+
else
|
648
|
+
print '???'
|
649
|
+
end
|
650
|
+
print ' HP'
|
651
|
+
print " (LVL: #{monster.level})" if GameOptions.data['debug_mode']
|
652
|
+
print "\n"
|
653
|
+
puts
|
654
|
+
end
|
655
|
+
|
656
|
+
def print_escape_text
|
657
|
+
print ' '
|
658
|
+
Animation.run(phrase: ESCAPE_TEXT, oneline: false)
|
659
|
+
end
|
660
|
+
|
661
|
+
def print_battle_line
|
662
|
+
GameOptions.data['wrap_width'].times { print '*'.colorize(background: :red, color: :white) }
|
663
|
+
print "\n"
|
664
|
+
end
|
665
|
+
|
666
|
+
def print_battle_options_prompt
|
667
|
+
puts ' What do you do?'
|
668
|
+
print ' '
|
669
|
+
print "#{'['.colorize(:yellow)}"
|
670
|
+
print "#{'F'.colorize(:green)}#{'ight]['.colorize(:yellow)}"
|
671
|
+
print "#{'D'.colorize(:green)}#{'efend]['.colorize(:yellow)}"
|
672
|
+
print "#{'L'.colorize(:green)}#{'ook]['.colorize(:yellow)}"
|
673
|
+
print "#{'I'.colorize(:green)}#{'tem]['.colorize(:yellow)}"
|
674
|
+
print "#{'P'.colorize(:green)}#{'ass]['.colorize(:yellow)}"
|
675
|
+
print "#{'R'.colorize(:green)}#{'un]'.colorize(:yellow)}"
|
676
|
+
print "\n"
|
677
|
+
print_battle_prompt
|
678
|
+
end
|
679
|
+
|
680
|
+
def print_battle_prompt
|
681
|
+
print ' [BATTLE]> '
|
682
|
+
end
|
683
|
+
|
684
|
+
def print_battle_header
|
685
|
+
print_battle_line
|
686
|
+
puts ' BATTLE BEGINS!'.ljust(GameOptions.data['wrap_width']).colorize(background: :red, color: :white)
|
687
|
+
print_battle_line
|
688
|
+
end
|
689
|
+
end
|
690
|
+
end
|