colstrom-ruby_armor 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ module RubyArmor
2
+ class FloatingText < GameObject
3
+ FONT_SIZE = 20
4
+
5
+ def initialize(text, options = {})
6
+ super(options)
7
+
8
+ @final_y = y - 60
9
+ @text = text
10
+ @font = Font["ProggyCleanSZ.ttf", FONT_SIZE]
11
+ end
12
+
13
+ def update
14
+ self.y -= 1 # TODO: scale this with FPS.
15
+ destroy if y < @final_y
16
+ end
17
+
18
+ def draw
19
+ @font.draw_rel @text, x, y, zorder, 0.5, 0.5, 1, 1, color
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ module RubyWarrior
2
+ module Abilities
3
+ class Rest < Base
4
+ alias_method :original_perform, :perform
5
+ def perform
6
+ original = @unit.health
7
+ original_perform
8
+ state = $window.game_state_manager.inside_state || $window.current_game_state
9
+ state.unit_health_changed(@unit, @unit.health - original) if @unit.health > original
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module RubyWarrior
2
+ class PlayerGenerator
3
+ def generate
4
+ if level.number == 1
5
+ FileUtils.mkdir_p(level.player_path)
6
+ # Read and write, so that line-endings are correct for the OS.
7
+ File.open(level.player_path + '/player.rb', "w") do |f|
8
+ f.write File.read(templates_path + '/player.rb')
9
+ end
10
+ end
11
+
12
+ File.open(level.player_path + '/README', 'w') do |f|
13
+ f.write read_template(templates_path + '/README.erb')
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ module RubyWarrior
2
+ class Position
3
+ attr_reader :x, :y
4
+ end
5
+ end
@@ -0,0 +1,44 @@
1
+ module RubyWarrior
2
+ class UI
3
+ class << self
4
+ attr_accessor :proxy
5
+
6
+ def puts(msg)
7
+ print msg + "\n"
8
+ end
9
+
10
+ def puts_with_delay(msg)
11
+ puts msg
12
+ end
13
+
14
+ def print(msg)
15
+ proxy.print msg
16
+ end
17
+
18
+ def gets
19
+ ""
20
+ end
21
+
22
+ def request(msg)
23
+ print msg
24
+ true
25
+ end
26
+
27
+ def ask(msg)
28
+ print msg
29
+ true
30
+ end
31
+
32
+ def choose(item, options)
33
+ response = options.first
34
+ if response.kind_of? Array
35
+ response.first
36
+ else
37
+ response
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+
44
+ end
@@ -0,0 +1,12 @@
1
+ module RubyWarrior
2
+ module Units
3
+ class Base
4
+ alias_method :original_take_damage, :take_damage
5
+ def take_damage(amount)
6
+ state = $window.game_state_manager.inside_state || $window.current_game_state
7
+ state.unit_health_changed self, -amount
8
+ original_take_damage amount
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ module RubyArmor
2
+ class SpriteSheet
3
+ extend Forwardable
4
+
5
+ def_delegators :@sprites, :map, :each
6
+
7
+ class << self
8
+ def [](file, width, height, tiles_wide = 0)
9
+ @cached_sheets ||= Hash.new do |h, k|
10
+ h[k] = new(*k)
11
+ end
12
+ @cached_sheets[[file, width, height, tiles_wide]]
13
+ end
14
+ end
15
+
16
+ def initialize(file, width, height, tiles_wide = 0)
17
+ @sprites = Image.load_tiles($window, File.expand_path(file, Image.autoload_dirs[0]), width, height, false)
18
+ @tiles_wide = tiles_wide
19
+ end
20
+
21
+ def [](x, y = 0)
22
+ @sprites[y * @tiles_wide + x]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,61 @@
1
+ module RubyArmor
2
+ class ChooseProfile < Fidgit::GuiState
3
+ def setup
4
+ super
5
+
6
+ on_input :escape, :hide
7
+
8
+ # Create the game.
9
+ @game = RubyWarrior::Game.new
10
+
11
+ warrior_sprites = SpriteSheet.new "warriors.png", DungeonView::SPRITE_WIDTH, DungeonView::SPRITE_HEIGHT, 4
12
+
13
+ vertical align_h: :center, spacing: 30 do
14
+ vertical align: :center, padding_top: 30, padding: 0 do
15
+ label "ryanb's RubyWarrior is wearing Spooner's", align: :center, font_height: 12
16
+ label "RubyArmor", align: :center, font_height: 80
17
+ end
18
+
19
+ # Use existing profile.
20
+ vertical padding: 0, align_h: :center do
21
+ scroll_window height: 250, width: 460 do
22
+ @game.profiles.each do |profile|
23
+ config = WarriorConfig.new profile
24
+
25
+ name_of_level = profile.epic? ? "EPIC" : profile.level_number.to_s
26
+ title = "#{profile.warrior_name.ljust(20)} #{profile.tower.name.rjust(12)}:#{name_of_level[0, 1]} #{profile.score.to_s.rjust(5)}"
27
+ tip = "Play as #{profile.warrior_name} the #{config.warrior_class.capitalize} - #{profile.tower.name} - level #{name_of_level} - score #{profile.score}"
28
+
29
+ # Can be disabled because of a bug in RubyWarrior paths.
30
+ button title, width: 400, tip: tip, enabled: File.directory?(profile.tower_path),
31
+ icon: warrior_sprites[0, DungeonView::WARRIORS[config.warrior_class]], icon_options: { factor: 2 } do
32
+ play profile, config
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ # Option to create a new profile.
39
+ horizontal align: :center do
40
+ button "Create new profile", shortcut: :auto, shortcut_color: Play::SHORTCUT_COLOR do
41
+ CreateProfile.new(@game, warrior_sprites).show
42
+ end
43
+
44
+ button "Exit", shortcut: :x, shortcut_color: Play::SHORTCUT_COLOR do
45
+ exit!
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def finalize
52
+ super
53
+ container.clear
54
+ end
55
+
56
+ def play(profile, config)
57
+ @game.instance_variable_set :@profile, profile
58
+ push_game_state Play.new(@game, config)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,79 @@
1
+ module RubyArmor
2
+ class CreateProfile < Fidgit::DialogState
3
+ DEFAULT_WARRIOR_CLASS = :valkyrie
4
+
5
+ def initialize(game, warrior_sprites)
6
+ @game = game
7
+
8
+ super shadow_full: true
9
+
10
+ on_input :escape, :hide
11
+ on_input [:return, :enter] do
12
+ @new_profile_button.activate if @new_profile_button.enabled?
13
+ end
14
+
15
+ # Option to create a new profile.
16
+ vertical align: :center, border_thickness: 4, background_color: Color::BLACK do
17
+ label "Create new profile", font_height: 20
18
+
19
+ @new_name = text_area width: 300, height: 30, font_height: 20 do |_, text|
20
+ duplicate = @game.profiles.any? {|p| p.warrior_name.downcase == text.downcase }
21
+ @new_profile_button.enabled = !(text.empty? or duplicate)
22
+ end
23
+
24
+ # Choose class; just cosmetic.
25
+ @warrior_class = group align_h: :center do
26
+ horizontal padding: 0, align_h: :center do
27
+ DungeonView::WARRIORS.each do |warrior, row|
28
+ radio_button "", warrior, tip: "Play as a #{warrior.capitalize} (The difference between classes is purely cosmetic!)",
29
+ :icon => warrior_sprites[0, row], :icon_options => { :factor => 4 }
30
+ end
31
+ end
32
+ end
33
+
34
+ horizontal align: :center do
35
+ button "Cancel" do
36
+ hide
37
+ end
38
+
39
+ @new_profile_button = button "Create", justify: :center, tip: "Create a new profile" do
40
+ play *new_profile(@new_name.text)
41
+ end
42
+ end
43
+
44
+ new_name = File.basename File.expand_path("~")
45
+ new_name = "Player" if new_name.empty?
46
+ @new_name.text = new_name
47
+
48
+ @warrior_class.value = DEFAULT_WARRIOR_CLASS
49
+ end
50
+ end
51
+
52
+ def update
53
+ super
54
+ @new_name.focus self unless @new_name.focused?
55
+ end
56
+
57
+ def finalize
58
+ super
59
+ container.clear
60
+ end
61
+
62
+ def play(profile, config)
63
+ @game.instance_variable_set :@profile, profile
64
+ hide
65
+ push_game_state Play.new(@game, config)
66
+ end
67
+
68
+ def new_profile(name)
69
+ new_profile = RubyWarrior::Profile.new
70
+ new_profile.tower_path = @game.towers[0].path
71
+ new_profile.warrior_name = name
72
+
73
+ config = WarriorConfig.new new_profile
74
+ config.warrior_class = @warrior_class.value
75
+
76
+ [new_profile, config]
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,606 @@
1
+ module RubyArmor
2
+ class Play < Fidgit::GuiState
3
+ MAX_TURN_DELAY = 1
4
+ MIN_TURN_DELAY = 0
5
+ TURN_DELAY_STEP = 0.1
6
+
7
+ MAX_TURNS = 100
8
+
9
+ SHORTCUT_COLOR = Color.rgb(175, 255, 100)
10
+
11
+ FILE_SYNC_DELAY = 0.5 # 2 polls per second.
12
+
13
+
14
+
15
+ ENEMY_TYPES = [
16
+ RubyWarrior::Units::Wizard,
17
+ RubyWarrior::Units::ThickSludge,
18
+ RubyWarrior::Units::Sludge,
19
+ RubyWarrior::Units::Archer,
20
+ ]
21
+ WARRIOR_TYPES = [
22
+ RubyWarrior::Units::Warrior,
23
+ RubyWarrior::Units::Golem,
24
+ ]
25
+ FRIEND_TYPES = [
26
+ RubyWarrior::Units::Captive,
27
+ ]
28
+
29
+ trait :timer
30
+
31
+ attr_reader :turn
32
+ def turn=(turn)
33
+ @turn = turn
34
+ @take_next_turn_at = Time.now + @config.turn_delay
35
+ @log_contents["current turn"].text = replace_log @turn_logs[turn]
36
+ @dungeon_view.turn = turn
37
+
38
+ turn
39
+ end
40
+
41
+ def initialize(game, config)
42
+ @game, @config = game, config
43
+ super()
44
+ end
45
+
46
+ def setup
47
+ super
48
+
49
+ RubyWarrior::UI.proxy = self
50
+
51
+
52
+ on_input(:escape) { pop_game_state }
53
+
54
+ on_input(:right_arrow) do
55
+ if !focus and @turn_slider.enabled? and @turn_slider.value < turn
56
+ @turn_slider.value += 1
57
+ end
58
+ end
59
+ on_input(:left_arrow) do
60
+ if !focus and @turn_slider.enabled? and @turn_slider.value > 0
61
+ @turn_slider.value -= 1
62
+ end
63
+ end
64
+
65
+ vertical spacing: 10, padding: 10 do
66
+ horizontal padding: 0, height: 260, width: 780, spacing: 10 do |packer|
67
+ # Space for the game graphics.
68
+ @dungeon_view = DungeonView.new packer, @config.warrior_class
69
+
70
+ create_ui_bar packer
71
+ end
72
+
73
+ @turn_slider = slider width: 774, range: 0..MAX_TURNS, value: 0, enabled: false, tip: "Turn" do |_, turn|
74
+ @log_contents["current turn"].text = replace_log @turn_logs[turn]
75
+ @dungeon_view.turn = turn
76
+ refresh_labels
77
+ end
78
+
79
+ # Text areas at the bottom.
80
+ horizontal padding: 0, spacing: 10 do
81
+ create_file_tabs
82
+ create_log_tabs
83
+ end
84
+ end
85
+
86
+ # Return to normal mode if extra levels have been added.
87
+ if profile.epic?
88
+ if profile.level_after_epic?
89
+ # TODO: do something with log.
90
+ log = record_log do
91
+ @game.go_back_to_normal_mode
92
+ end
93
+ else
94
+ # TODO: do something with log.
95
+ log = record_log do
96
+ @game.play_epic_mode
97
+ end
98
+ end
99
+ end
100
+
101
+ prepare_level
102
+ end
103
+
104
+ def create_ui_bar(packer)
105
+ vertical parent: packer, padding: 0, height: 260, width: 100, spacing: 6 do
106
+ # Labels at top-right.
107
+ @tower_label = label "", tip: "Each tower has a different difficulty level"
108
+ @level_label = label "Level:"
109
+ @turn_label = label "Turn:", tip: "Current turn; starvation at #{MAX_TURNS} to avoid endless games"
110
+ @health_label = label "Health:", tip: "The warrior's remaining health; death occurs at 0"
111
+
112
+ # Buttons underneath them.
113
+ button_options = {
114
+ :width => 70,
115
+ :justify => :center,
116
+ shortcut: :auto,
117
+ shortcut_color: SHORTCUT_COLOR,
118
+ border_thickness: 0,
119
+ }
120
+ @start_button = button "Start", button_options.merge(tip: "Start running player.rb in this level") do
121
+ start_level
122
+ end
123
+
124
+ @reset_button = button "Reset", button_options.merge(tip: "Restart the level") do
125
+ profile.level_number = 0 if profile.epic?
126
+ prepare_level
127
+ end
128
+
129
+ @hint_button = button "Hint", button_options.merge(tip: "Get hint on how best to approach the level") do
130
+ message replace_syntax(level.tip)
131
+ end
132
+
133
+ @continue_button = button "Continue", button_options.merge(tip: "Climb up the stairs to the next level") do
134
+ save_player_code
135
+
136
+ # Move to next level.
137
+ if @game.next_level.exists?
138
+ @game.prepare_next_level
139
+ else
140
+ @game.prepare_epic_mode
141
+ end
142
+
143
+ prepare_level
144
+ end
145
+
146
+ horizontal padding: 0, spacing: 21 do
147
+ options = { padding: 4, border_thickness: 0, shortcut: :auto, shortcut_color: SHORTCUT_COLOR }
148
+ @turn_slower_button = button "-", options.merge(tip: "Make turns run slower") do
149
+ @config.turn_delay = [@config.turn_delay + TURN_DELAY_STEP, MAX_TURN_DELAY].min if @config.turn_delay < MAX_TURN_DELAY
150
+ update_turn_delay
151
+ end
152
+
153
+ @turn_duration_label = label "", align: :center
154
+
155
+ @turn_faster_button = button "+", options.merge(tip: "Make turns run faster") do
156
+ @config.turn_delay = [@config.turn_delay - TURN_DELAY_STEP, MIN_TURN_DELAY].max if @config.turn_delay > MIN_TURN_DELAY
157
+ update_turn_delay
158
+ end
159
+
160
+ update_turn_delay
161
+ end
162
+
163
+ # Review old level code.
164
+ @review_button = button "Review", button_options.merge(tip: "Review code used for each level",
165
+ enabled: false, border_thickness: 0, shortcut: :v) do
166
+ ReviewCode.new(profile).show
167
+ end
168
+ end
169
+ end
170
+
171
+ def save_player_code
172
+ # Save the code used to complete the level for posterity.
173
+ File.open File.join(profile.player_path, "ruby_armor/player_#{profile.epic? ? "EPIC" : level.number.to_s.rjust(3, '0')}.rb"), "w" do |file|
174
+ file.puts @loaded_code
175
+
176
+ file.puts
177
+ file.puts
178
+ file.puts "#" * 40
179
+ file.puts "=begin"
180
+ file.puts
181
+ file.puts record_log { level.tally_points }
182
+ file.puts
183
+
184
+ if profile.epic? and @game.final_report
185
+ file.puts @game.final_report
186
+ else
187
+ file.puts "Completed in #{turn} turns."
188
+ end
189
+
190
+ file.puts
191
+ file.puts "=end"
192
+ file.puts "#" * 40
193
+ end
194
+ end
195
+
196
+ def create_log_tabs
197
+ vertical padding: 0, spacing: 0 do
198
+ @log_tabs_group = group do
199
+ @log_tab_buttons = horizontal padding: 0, spacing: 4 do
200
+ ["current turn", "full log"].each do |name|
201
+ radio_button(name.capitalize, name, border_thickness: 0, tip: "View #{name}")
202
+ end
203
+
204
+ horizontal padding_left: 50, padding: 0 do
205
+ button "copy", tip: "Copy displayed log to clipboard", font_height: 12, border_thickness: 0, padding: 4 do
206
+ Clipboard.copy @log_contents[@log_tabs_group.value].stripped_text
207
+ end
208
+ end
209
+ end
210
+
211
+ subscribe :changed do |_, value|
212
+ current = @log_tab_buttons.find {|elem| elem.value == value }
213
+ @log_tab_buttons.each {|t| t.enabled = (t != current) }
214
+ current.color, current.background_color = current.background_color, current.color
215
+
216
+ @log_tab_contents.clear
217
+ @log_tab_contents.add @log_tab_windows[value]
218
+ end
219
+ end
220
+
221
+ # Contents of those tabs.
222
+ @log_tab_contents = vertical padding: 0, width: 380, height: $window.height * 0.5
223
+
224
+ create_log_tab_windows
225
+ @log_tabs_group.value = "current turn"
226
+ end
227
+ end
228
+
229
+
230
+ def create_file_tabs
231
+ # Tabs to contain README and player code to the left.
232
+ vertical padding: 0, spacing: 0 do
233
+ @file_tabs_group = group do
234
+ @file_tab_buttons = horizontal padding: 0, spacing: 4 do
235
+ %w[README player.rb].each do |name|
236
+ radio_button(name.to_s, name, border_thickness: 0, tip: "View #{File.join profile.player_path, name}")
237
+ end
238
+
239
+ horizontal padding_left: 50, padding: 0 do
240
+ button "copy", tip: "Copy displayed file to clipboard", font_height: 12, border_thickness: 0, padding: 4 do
241
+ Clipboard.copy @file_contents[@file_tabs_group.value].stripped_text
242
+ end
243
+
244
+ # Default editor for Windows.
245
+ ENV['EDITOR'] = "notepad" if Gem.win_platform? and ENV['EDITOR'].nil?
246
+
247
+ tip = ENV['EDITOR'] ? "Edit file in #{ENV['EDITOR']} (set EDITOR environment variable to use a different editor)" : "ENV['EDITOR'] not set"
248
+ button "edit", tip: tip, enabled: ENV['EDITOR'], font_height: 12, border_thickness: 0, padding: 4 do
249
+ command = %<#{ENV['EDITOR']} "#{File.join(level.player_path, @file_tabs_group.value)}">
250
+ $stdout.puts "SYSTEM: #{command}"
251
+ Thread.new { system command }
252
+ end
253
+ end
254
+ end
255
+
256
+ subscribe :changed do |_, value|
257
+ current = @file_tab_buttons.find {|elem| elem.value == value }
258
+ @file_tab_buttons.each {|t| t.enabled = (t != current) }
259
+ current.color, current.background_color = current.background_color, current.color
260
+
261
+ @file_tab_contents.clear
262
+ @file_tab_contents.add @file_tab_windows[value]
263
+ end
264
+ end
265
+
266
+ # Contents of those tabs.
267
+ @file_tab_contents = vertical padding: 0, width: 380, height: $window.height * 0.5
268
+
269
+ create_file_tab_windows
270
+ @file_tabs_group.value = "README"
271
+ end
272
+ end
273
+
274
+ def update_turn_delay
275
+ @turn_duration_label.text = "%2d" % [(MAX_TURN_DELAY / TURN_DELAY_STEP) + 1 - (@config.turn_delay / TURN_DELAY_STEP)]
276
+ @turn_slower_button.enabled = @config.turn_delay < MAX_TURN_DELAY
277
+ @turn_faster_button.enabled = @config.turn_delay > MIN_TURN_DELAY
278
+ @turn_duration_label.tip = "Speed of turns (high is faster; currently turns take #{(@config.turn_delay * 1000).to_i}ms)"
279
+ end
280
+
281
+ def create_log_tab_windows
282
+ @log_tab_windows = {}
283
+ @log_contents = {}
284
+ ["current turn", "full log"].each do |log|
285
+ @log_tab_windows[log] = Fidgit::ScrollWindow.new width: 380, height: 250 do
286
+ @log_contents[log] = text_area width: 368, editable: false
287
+ end
288
+ end
289
+ end
290
+
291
+ def create_file_tab_windows
292
+ @file_tab_windows = {}
293
+ @file_contents = {}
294
+ ["README", "player.rb"].each do |file|
295
+ @file_tab_windows[file] = Fidgit::ScrollWindow.new width: 380, height: 250 do
296
+ @file_contents[file] = text_area width: 368, editable: false
297
+ end
298
+ end
299
+ end
300
+
301
+ def prepare_level
302
+ @recorded_log = nil # Not initially logging.
303
+
304
+ @log_contents["full log"].text = "" #unless profile.epic? # TODO: Might need to avoid this, since it could get REALLY long.
305
+ @continue_button.enabled = false
306
+ @hint_button.enabled = false
307
+ @reset_button.enabled = false
308
+ @start_button.enabled = true
309
+
310
+ @exception = nil
311
+
312
+ if profile.current_level.number.zero?
313
+ if profile.epic?
314
+ @game.prepare_epic_mode
315
+ profile.level_number += 1
316
+ profile.current_epic_score = 0
317
+ profile.current_epic_grades = {}
318
+ else
319
+ @game.prepare_next_level
320
+ end
321
+ end
322
+
323
+ create_sync_timer
324
+
325
+ @level = profile.current_level # Need to store this because it gets forgotten by the profile/game :(
326
+ @playing = false
327
+ level.load_level
328
+
329
+ @dungeon_view.floor = floor
330
+
331
+ # List of log entries, unit drawings and health made in each turn.
332
+ @turn_logs = Hash.new {|h, k| h[k] = "" }
333
+ @health = [level.warrior.health]
334
+
335
+ @review_button.enabled = ReviewCode.saved_levels? profile # Can't review code unless some has been saved.
336
+
337
+ self.turn = 0
338
+
339
+ generate_readme
340
+
341
+ # Initial log entry.
342
+ self.puts "- turn 0 -"
343
+ self.print "#{profile.warrior_name} climbs up to level #{level.number}\n"
344
+ @log_contents["full log"].text += @log_contents["current turn"].text
345
+
346
+ @dungeon_view.tile_set = %w[beginner intermediate].index(profile.tower.name) || 2 # We don't know what the last tower will be called.
347
+
348
+ # Reset the time-line slider.
349
+ @turn_slider.enabled = false
350
+ @turn_slider.value = 0
351
+
352
+ refresh_labels
353
+
354
+ # Load the player's own code, which might explode!
355
+ begin
356
+ level.load_player
357
+ rescue SyntaxError, StandardError => ex
358
+ handle_exception ex
359
+ return
360
+ end
361
+ end
362
+
363
+ def generate_readme
364
+ readme = <<END
365
+ #{level.description}
366
+
367
+ Tip: #{level.tip}
368
+
369
+ Warrior Abilities:
370
+ END
371
+ level.warrior.abilities.each do |name, ability|
372
+ readme << " warrior.#{name}\n"
373
+ readme << " #{ability.description}\n\n"
374
+ end
375
+
376
+ @file_contents["README"].text = replace_syntax readme
377
+ end
378
+
379
+ # Continually poll the player code file to see when it is edited.
380
+ def create_sync_timer
381
+ stop_timer :refresh_code
382
+
383
+ every(FILE_SYNC_DELAY * 1000, :name => :refresh_code) do
384
+ sync_player_file
385
+ end
386
+ end
387
+
388
+ def sync_player_file
389
+ begin
390
+ player_file = File.join level.player_path, "player.rb"
391
+ player_code = File.read player_file
392
+ stripped_code = player_code.strip
393
+ @loaded_code ||= ""
394
+ unless @loaded_code == stripped_code
395
+ $stdout.puts "Detected change in player.rb"
396
+
397
+ @file_contents["player.rb"].text = stripped_code
398
+ @loaded_code = stripped_code
399
+ prepare_level
400
+ end
401
+ rescue Errno::ENOENT
402
+ # This can happen if the file is busy.
403
+ end
404
+ end
405
+
406
+ def effective_turn
407
+ @turn_slider.enabled? ? @turn_slider.value : @turn
408
+ end
409
+
410
+ def refresh_labels
411
+ @tower_label.text = profile.tower.name.capitalize
412
+ @level_label.text = "Level: #{profile.epic? ? "E" : " "}#{level.number}"
413
+ @level_label.tip = profile.epic? ? "Playing in EPIC mode" : "Playing in normal mode"
414
+ @turn_label.text = "Turn: #{effective_turn.to_s.rjust(2)}"
415
+ @health_label.text = "Health: #{@health[effective_turn].to_s.rjust(2)}"
416
+ end
417
+
418
+ def start_level
419
+ @reset_button.enabled = true
420
+ @start_button.enabled = false
421
+ @playing = true
422
+ self.turn = 0
423
+ refresh_labels
424
+ end
425
+
426
+ def replace_syntax(string)
427
+ # Used in readme.
428
+ string.gsub!(/warrior\.[^! \n]+./) do |s|
429
+ if s[-1, 1] == '!'
430
+ "<c=eeee00>#{s}</c>" # Commands.
431
+ else
432
+ "<c=00ff00>#{s}</c>" # Queries.
433
+ end
434
+ end
435
+
436
+ replace_log string
437
+ end
438
+
439
+ def replace_log(string)
440
+ @enemy_pattern ||= /([asw])/i #Archer, sludge, thick sludge, wizard.
441
+ @friend_pattern ||= /([C])/
442
+ @warrior_pattern ||= /([@G])/ # Player and golem
443
+
444
+ # Used in log.
445
+ string.gsub(/\|.*\|/i) {|c|
446
+ c = c.gsub @enemy_pattern, '<c=ff0000>\1</c>'
447
+ c.gsub! @friend_pattern, '<c=00dd00>\1</c>'
448
+ c.gsub! @warrior_pattern, '<c=aaaaff>\1</c>'
449
+ c.gsub '|', '<c=777777>|</c>'
450
+ }
451
+ .gsub(/^(#{profile.warrior_name}.*)/, '<c=aaaaff>\1</c>') # Player doing stuff.
452
+ .gsub(/(\-{3,})/, '<c=777777>\1</c>') # Walls.
453
+ end
454
+
455
+ def profile; @game.profile; end
456
+ def level; @level; end
457
+ def floor; level.floor; end
458
+
459
+ def recording_log?; not @recorded_log.nil?; end
460
+ def record_log
461
+ raise "block required" unless block_given?
462
+ @recorded_log = ""
463
+ record = ""
464
+ begin
465
+ yield
466
+ ensure
467
+ record = @recorded_log
468
+ @recorded_log = nil
469
+ end
470
+ record
471
+ end
472
+
473
+ def play_turn
474
+ self.puts "- turn #{(turn + 1).to_s.rjust(3)} -"
475
+
476
+ begin
477
+ actions = record_log do
478
+ floor.units.each(&:prepare_turn)
479
+ floor.units.each(&:perform_turn)
480
+ end
481
+
482
+ self.print actions
483
+ rescue => ex
484
+ handle_exception ex
485
+ return
486
+ end
487
+
488
+ self.turn += 1
489
+
490
+ @health[turn] = level.warrior.health # Record health for later playback.
491
+
492
+ level.time_bonus -= 1 if level.time_bonus > 0
493
+
494
+ refresh_labels
495
+
496
+ if level.passed?
497
+ stop_timer :refresh_code # Don't sync after successful completion, unless reset.
498
+ @reset_button.enabled = false if profile.epic? # Continue will save performance; reset won't.
499
+ @continue_button.enabled = true
500
+
501
+ if profile.next_level.exists?
502
+ self.puts "Success! You have found the stairs."
503
+ level.tally_points
504
+
505
+ if profile.epic?
506
+ # Start the next level immediately.
507
+ self.puts "\n#{"-" * 25}\n"
508
+
509
+ # Rush onto the next level immediately!
510
+ profile.level_number += 1
511
+ prepare_level
512
+ start_level
513
+ end
514
+ else
515
+ self.puts "CONGRATULATIONS! You have climbed to the top of the tower and rescued the fair maiden Ruby."
516
+ level.tally_points
517
+
518
+ if profile.epic?
519
+ self.puts @game.final_report if @game.final_report
520
+ profile.save
521
+ end
522
+ end
523
+
524
+ level_ended
525
+
526
+ elsif level.failed?
527
+ level_ended
528
+ self.puts "Sorry, you failed level #{level.number}. Change your script and try again."
529
+
530
+ elsif out_of_time?
531
+ level_ended
532
+ self.puts "Sorry, you starved to death on level #{level.number}. Change your script and try again."
533
+
534
+ end
535
+
536
+ # Add the full turn's text into the main log at once, to save on re-calculations.
537
+ @log_contents["full log"].text += @log_contents["current turn"].text
538
+ @log_tab_windows["full log"].offset_y = Float::INFINITY
539
+
540
+ self.puts
541
+ end
542
+
543
+ # Not necessarily complete; just finished.
544
+ def level_ended
545
+ return if profile.epic?
546
+
547
+ @hint_button.enabled = true
548
+ @turn_slider.enabled = true
549
+ @turn_slider.instance_variable_set :@range, 0..turn
550
+ @turn_slider.value = turn
551
+ end
552
+
553
+ def handle_exception(exception)
554
+ return if @exception and exception.message == @exception.message
555
+
556
+ self.puts "\n#{profile.warrior_name} was eaten by a #{exception.class}!\n"
557
+ self.puts exception.message
558
+ self.puts
559
+ self.puts exception.backtrace.join("\n")
560
+
561
+ # TODO: Make this work without it raising exceptions in Fidgit :(
562
+ #exception.message =~ /:(\d+):/
563
+ #exception_line = $1.to_i - 1
564
+ #code_lines = @file_contents["player.rb"].text.split "\n"
565
+ #code_lines[exception_line] = "<c=ff0000>{code_lines[exception_line]}</c>"
566
+ #@file_contents["player.rb"].text = code_lines.join "\n"
567
+
568
+ @start_button.enabled = false
569
+
570
+ @exception = exception
571
+ end
572
+
573
+ def out_of_time?
574
+ turn >= MAX_TURNS
575
+ end
576
+
577
+ def puts(message = "")
578
+ print "#{message}\n"
579
+ end
580
+
581
+ def print(message)
582
+ if recording_log?
583
+ @recorded_log << message
584
+ else
585
+ @turn_logs[turn] << message
586
+ @log_contents["current turn"].text = replace_log @turn_logs[turn]
587
+ end
588
+ end
589
+
590
+ def draw
591
+ super
592
+ end
593
+
594
+ def unit_health_changed(unit, amount)
595
+ @dungeon_view.unit_health_changed unit, amount
596
+ end
597
+
598
+ def update
599
+ super
600
+
601
+ if @playing and Time.now >= @take_next_turn_at and not (level.passed? || level.failed? || out_of_time? || @exception)
602
+ play_turn
603
+ end
604
+ end
605
+ end
606
+ end