colstrom-ruby_armor 0.0.6

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.
@@ -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