gemwarrior 0.15.10 → 0.15.11
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 +4 -4
- data/bin/gemwarrior +146 -146
- data/gemwarrior.gemspec +3 -1
- 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 +6 -6
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
|