ruby_armor 0.0.4alpha → 0.0.5alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -64,8 +64,10 @@
64
64
  :rail_width: 14
65
65
  :rail_color: ?light_background
66
66
  :handle_color: ?handle
67
+
67
68
 
68
69
  :ScrollWindow: # < Composite
69
70
  :scroll_bar_thickness: 12
70
71
  :border_color: ?light_border
72
+ :background_color: ?dark_background
71
73
 
@@ -11,6 +11,8 @@ include Chingu
11
11
 
12
12
  $LOAD_PATH.unshift File.expand_path("..", __FILE__)
13
13
 
14
+ require "ruby_armor/version"
15
+
14
16
  require "ruby_armor/ruby_warrior_ext/position"
15
17
  require "ruby_armor/ruby_warrior_ext/ui"
16
18
  require "ruby_armor/ruby_warrior_ext/units/base"
@@ -20,6 +22,7 @@ require "ruby_armor/floating_text"
20
22
  require "ruby_armor/sprite_sheet"
21
23
  require "ruby_armor/states/choose_profile"
22
24
  require "ruby_armor/states/play"
25
+ require "ruby_armor/states/review_code"
23
26
  require "ruby_armor/window"
24
27
  require "ruby_armor/warrior_config"
25
28
 
@@ -5,7 +5,8 @@ module RubyWarrior
5
5
  def perform
6
6
  original = @unit.health
7
7
  original_perform
8
- $window.current_game_state.unit_health_changed(@unit, @unit.health - original) if @unit.health > original
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
9
10
  end
10
11
  end
11
12
  end
@@ -3,8 +3,9 @@ module RubyWarrior
3
3
  class Base
4
4
  alias_method :original_take_damage, :take_damage
5
5
  def take_damage(amount)
6
- $window.current_game_state.unit_health_changed self, -amount
7
- original_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
8
9
  end
9
10
  end
10
11
  end
@@ -10,19 +10,23 @@ module RubyArmor
10
10
 
11
11
  warrior_sprites = SpriteSheet.new "warriors.png", Play::SPRITE_WIDTH, Play::SPRITE_HEIGHT, 4
12
12
 
13
- vertical align_h: :center, spacing: 50 do
14
- label "RubyArmor", align: :center, font_height: 120, padding_top: 50
13
+ vertical align_h: :center, spacing: 40 do
14
+ vertical align: :center, padding: 0 do
15
+ label "RubyArmor", align: :center, font_height: 80, padding_top: 50
16
+ label "by Spooner", align: :center, font_height: 12
17
+ end
15
18
 
16
19
  button_options = { width: 400, align: :center, justify: :center }
17
20
 
18
21
  # Use existing profile.
19
22
  vertical padding: 0, align_h: :center do
20
- scroll_window height: 200, width: 460 do
23
+ scroll_window height: 250, width: 460 do
21
24
  @game.profiles.each do |profile|
22
25
  config = WarriorConfig.new profile
23
26
 
24
- title = "#{profile.warrior_name.ljust(20)} #{profile.tower.name.rjust(12)}:#{profile.level_number} #{profile.score.to_s.rjust(5)}"
25
- tip = "Play as #{profile.warrior_name} the #{config.warrior_class.capitalize} - #{profile.tower.name} - level #{profile.level_number} - score #{profile.score}"
27
+ name_of_level = profile.epic? ? "EPIC" : profile.level_number.to_s
28
+ title = "#{profile.warrior_name.ljust(20)} #{profile.tower.name.rjust(12)}:#{name_of_level[0, 1]} #{profile.score.to_s.rjust(5)}"
29
+ tip = "Play as #{profile.warrior_name} the #{config.warrior_class.capitalize} - #{profile.tower.name} - level #{name_of_level} - score #{profile.score}"
26
30
 
27
31
  # Can be disabled because of a bug in RubyWarrior paths.
28
32
  button title, button_options.merge(tip: tip, enabled: File.directory?(profile.tower_path),
@@ -36,7 +40,7 @@ module RubyArmor
36
40
  # Option to create a new profile.
37
41
  vertical padding: 0, align: :center do
38
42
  horizontal align: :center, padding: 0 do
39
- @new_name = text_area width: 300, height: 35, font_height: 24 do |_, text|
43
+ @new_name = text_area width: 300, height: 30, font_height: 20 do |_, text|
40
44
  duplicate = @game.profiles.any? {|p| p.warrior_name.downcase == text.downcase }
41
45
  @new_profile_button.enabled = !(text.empty? or duplicate)
42
46
  end
@@ -65,6 +69,11 @@ module RubyArmor
65
69
  end
66
70
  end
67
71
 
72
+ def update
73
+ super
74
+ @new_name.focus self unless @new_name.focused?
75
+ end
76
+
68
77
  def finalize
69
78
  super
70
79
  container.clear
@@ -4,9 +4,16 @@ module RubyArmor
4
4
 
5
5
  TILE_WIDTH, TILE_HEIGHT = 8, 12
6
6
  SPRITE_WIDTH, SPRITE_HEIGHT = 8, 8
7
- SPRITE_OFFSET_X, SPRITE_OFFSET_Y = 64, 64
8
7
  SPRITE_SCALE = 5
9
8
 
9
+ MAX_TURN_DELAY = 1
10
+ MIN_TURN_DELAY = 0
11
+ TURN_DELAY_STEP = 0.1
12
+
13
+ MAX_TURNS = 100
14
+
15
+ SHORTCUT_COLOR = Color.rgb(175, 255, 100)
16
+
10
17
  # Sprites to show based on player facing.
11
18
  FACINGS = {
12
19
  :east => 0,
@@ -39,6 +46,15 @@ module RubyArmor
39
46
 
40
47
  trait :timer
41
48
 
49
+ attr_reader :turn
50
+ def turn=(turn)
51
+ @turn = turn
52
+ @take_next_turn_at = Time.now + @config.turn_delay
53
+ @log_contents["current turn"].text = replace_log @turn_logs[turn]
54
+
55
+ turn
56
+ end
57
+
42
58
  def initialize(game, config)
43
59
  @game, @config = game, config
44
60
  super()
@@ -52,129 +68,259 @@ module RubyArmor
52
68
  @tiles = SpriteSheet.new "tiles.png", TILE_WIDTH, TILE_HEIGHT, 8
53
69
  @warrior_sprites = SpriteSheet.new "warriors.png", SPRITE_WIDTH, SPRITE_HEIGHT, 4
54
70
  @mob_sprites = SpriteSheet.new "mobs.png", SPRITE_WIDTH, SPRITE_HEIGHT, 4
55
- @max_turns = 100 # Just to recognise a stalemate ;)
56
71
 
57
72
  on_input(:escape) { pop_game_state }
58
73
 
59
- vertical spacing: 0, padding: 10 do
60
- horizontal padding: 0, height: $window.height * 0.5, width: 780 do
74
+ on_input(:right_arrow) do
75
+ if @turn_slider.enabled? and @turn_slider.value < turn
76
+ @turn_slider.value += 1
77
+ end
78
+ end
79
+ on_input(:left_arrow) do
80
+ if @turn_slider.enabled? and @turn_slider.value > 0
81
+ @turn_slider.value -= 1
82
+ end
83
+ end
84
+
85
+ vertical spacing: 10, padding: 10 do
86
+ horizontal padding: 0, height: 260, width: 780, spacing: 10 do
61
87
  # Space for the game graphics.
62
- vertical padding: 0, height: $window.height * 0.5, align_h: :fill
63
-
64
- vertical padding: 0, height: $window.height * 0.5, width: 100 do
65
- # Labels at top-right.
66
- @tower_label = label "", tip: "Each tower has a different difficulty level"
67
- @level_label = label "Level:", tip: "Each tower contains 9 levels"
68
- @turn_label = label "Turn:", tip: "Current turn; starvation at #{@max_turns} to avoid endless games"
69
- @health_label = label "Health:", tip: "The warrior's remaining health; death occurs at 0"
70
-
71
- # Buttons underneath them.
72
- button_options = {
73
- :width => 70,
74
- :justify => :center,
75
- shortcut: :auto,
76
- shortcut_color: Color.rgb(150, 255, 0),
77
- border_thickness: 0,
78
- }
79
- @start_button = button "Start", button_options.merge(tip: "Start running player.rb in this level") do
80
- start_level
81
- end
88
+ @game_window = vertical padding: 0, width: 670, height: 260
82
89
 
83
- @reset_button = button "Reset", button_options.merge(tip: "Restart the level") do
84
- prepare_level
85
- end
90
+ create_ui_bar
91
+ end
86
92
 
87
- @hint_button = button "Hint", button_options.merge(tip: "Get hint on how best to approach the level") do
88
- message replace_syntax(level.tip)
89
- end
93
+ @turn_slider = slider width: 774, range: 0..MAX_TURNS, value: 0, enabled: false, tip: "Turn" do |_, turn|
94
+ @log_contents["current turn"].text = replace_log @turn_logs[turn]
95
+ refresh_labels
96
+ end
90
97
 
91
- @continue_button = button "Continue", button_options.merge(tip: "Climb up the stairs to the next level") do
92
- @game.prepare_next_level
93
- prepare_level
94
- end
98
+ # Text areas at the bottom.
99
+ horizontal padding: 0, spacing: 10 do
100
+ create_file_tabs
101
+ create_log_tabs
102
+ end
103
+ end
95
104
 
96
- # Choose turn-duration with a slider.
97
- horizontal padding: 0, spacing: 0 do
98
- @turn_duration_label = label "", font_height: 12
99
- @turn_duration_slider = slider width: 55, range: 0..1000,
100
- tip: "Turn duration (ms)" do |_, value|
101
- @config.turn_delay = value * 0.001
102
- @turn_duration_label.text = "%4dms" % value.to_s
103
- end
105
+ # Return to normal mode if extra levels have been added.
106
+ if profile.epic?
107
+ if profile.level_after_epic?
108
+ # TODO: do something with log.
109
+ log = record_log do
110
+ @game.go_back_to_normal_mode
111
+ end
112
+ else
113
+ # TODO: do something with log.
114
+ log = record_log do
115
+ @game.play_epic_mode
116
+ end
117
+ end
118
+ end
104
119
 
105
- @turn_duration_slider.value = (@config.turn_delay * 1000).round
106
- end
120
+ prepare_level
121
+ end
122
+
123
+ def create_ui_bar
124
+ vertical padding: 0, height: 260, width: 100, spacing: 6 do
125
+ # Labels at top-right.
126
+ @tower_label = label "", tip: "Each tower has a different difficulty level"
127
+ @level_label = label "Level:"
128
+ @turn_label = label "Turn:", tip: "Current turn; starvation at #{MAX_TURNS} to avoid endless games"
129
+ @health_label = label "Health:", tip: "The warrior's remaining health; death occurs at 0"
130
+
131
+ # Buttons underneath them.
132
+ button_options = {
133
+ :width => 70,
134
+ :justify => :center,
135
+ shortcut: :auto,
136
+ shortcut_color: SHORTCUT_COLOR,
137
+ border_thickness: 0,
138
+ }
139
+ @start_button = button "Start", button_options.merge(tip: "Start running player.rb in this level") do
140
+ start_level
141
+ end
142
+
143
+ @reset_button = button "Reset", button_options.merge(tip: "Restart the level") do
144
+ profile.level_number = 0 if profile.epic?
145
+ prepare_level
146
+ end
147
+
148
+ @hint_button = button "Hint", button_options.merge(tip: "Get hint on how best to approach the level") do
149
+ message replace_syntax(level.tip)
150
+ end
151
+
152
+ @continue_button = button "Continue", button_options.merge(tip: "Climb up the stairs to the next level") do
153
+ save_player_code
154
+
155
+ # Move to next level.
156
+ if @game.next_level.exists?
157
+ @game.prepare_next_level
158
+ else
159
+ @game.prepare_epic_mode
107
160
  end
161
+
162
+ prepare_level
108
163
  end
109
164
 
110
- # Text areas at the bottom.
111
- horizontal padding: 0, spacing: 10 do
112
- # Tabs to contain README and player code to the left.
113
- vertical padding: 0, spacing: 0 do
114
- @tabs_group = group do
115
- @tab_buttons = horizontal padding: 0, spacing: 4 do
116
- %w[README player.rb].each do |name|
117
- radio_button(name.to_s, name, border_thickness: 0, tip: "View #{name}")
118
- end
119
-
120
- horizontal padding: 0, padding_left: 70 do
121
- # Default editor for Windows.
122
- ENV['EDITOR'] = "notepad" if Gem.win_platform? and ENV['EDITOR'].nil?
123
-
124
- tip = ENV['EDITOR'] ? "Edit file in #{ENV['EDITOR']} (set ENV['EDITOR'] to use a different editor)" : "ENV['EDITOR'] not set"
125
- button "edit", tip: tip, enabled: ENV['EDITOR'], font_height: 12, border_thickness: 0 do
126
- command = %<"#{ENV['EDITOR']}" "#{File.join(level.player_path, @tabs_group.value)}">
127
- $stdout.puts "SYSTEM: #{command}"
128
- Thread.new { system command }
129
- end
130
- end
131
- end
165
+ horizontal padding: 0, spacing: 21 do
166
+ options = { padding: 4, border_thickness: 0, shortcut: :auto, shortcut_color: SHORTCUT_COLOR }
167
+ @turn_slower_button = button "-", options.merge(tip: "Make turns run slower") do
168
+ @config.turn_delay = [@config.turn_delay + TURN_DELAY_STEP, MAX_TURN_DELAY].min if @config.turn_delay < MAX_TURN_DELAY
169
+ update_turn_delay
170
+ end
132
171
 
133
- subscribe :changed do |_, value|
134
- current = @tab_buttons.find {|elem| elem.value == value }
135
- @tab_buttons.each {|t| t.enabled = (t != current) }
136
- current.color, current.background_color = current.background_color, current.color
172
+ @turn_duration_label = label "", align: :center
137
173
 
138
- @tab_contents.clear
139
- @tab_contents.add @tab_windows[value]
174
+ @turn_faster_button = button "+", options.merge(tip: "Make turns run faster") do
175
+ @config.turn_delay = [@config.turn_delay - TURN_DELAY_STEP, MIN_TURN_DELAY].max if @config.turn_delay > MIN_TURN_DELAY
176
+ update_turn_delay
177
+ end
178
+
179
+ update_turn_delay
180
+ end
181
+
182
+ # Review old level code.
183
+ @review_button = button "Review", button_options.merge(tip: "Review code used for each level",
184
+ enabled: false, border_thickness: 0, shortcut: :v) do
185
+ ReviewCode.new(profile).show
186
+ end
187
+ end
188
+ end
189
+
190
+ def save_player_code
191
+ # Save the code used to complete the level for posterity.
192
+ File.open File.join(profile.player_path, "ruby_armor/player_#{profile.epic? ? "EPIC" : level.number.to_s.rjust(3, '0')}.rb"), "w" do |file|
193
+ file.puts @loaded_code
194
+
195
+ file.puts
196
+ file.puts
197
+ file.puts "#" * 40
198
+ file.puts "=begin"
199
+ file.puts
200
+ file.puts record_log { level.tally_points }
201
+ file.puts
202
+
203
+ if profile.epic? and @game.final_report
204
+ file.puts @game.final_report
205
+ else
206
+ file.puts "Completed in #{turn} turns."
207
+ end
208
+
209
+ file.puts
210
+ file.puts "=end"
211
+ file.puts "#" * 40
212
+ end
213
+ end
214
+
215
+ def create_log_tabs
216
+ vertical padding: 0, spacing: 0 do
217
+ @log_tabs_group = group do
218
+ @log_tab_buttons = horizontal padding: 0, spacing: 4 do
219
+ ["current turn", "full log"].each do |name|
220
+ radio_button(name.capitalize, name, border_thickness: 0, tip: "View #{name}")
221
+ end
222
+
223
+ horizontal padding_left: 50, padding: 0 do
224
+ button "copy", tip: "Copy displayed log to clipboard", font_height: 12, border_thickness: 0, padding: 4 do
225
+ Clipboard.copy @log_contents[@log_tabs_group.value].stripped_text
140
226
  end
141
227
  end
228
+ end
142
229
 
143
- # Contents of those tabs.
144
- @tab_contents = vertical padding: 0, width: 380, spacing: 10, height: $window.height * 0.45
230
+ subscribe :changed do |_, value|
231
+ current = @log_tab_buttons.find {|elem| elem.value == value }
232
+ @log_tab_buttons.each {|t| t.enabled = (t != current) }
233
+ current.color, current.background_color = current.background_color, current.color
145
234
 
146
- create_tab_windows
147
- @tabs_group.value = "README"
235
+ @log_tab_contents.clear
236
+ @log_tab_contents.add @log_tab_windows[value]
148
237
  end
238
+ end
239
+
240
+ # Contents of those tabs.
241
+ @log_tab_contents = vertical padding: 0, width: 380, height: $window.height * 0.5
242
+
243
+ create_log_tab_windows
244
+ @log_tabs_group.value = "current turn"
245
+ end
246
+ end
247
+
248
+
249
+ def create_file_tabs
250
+ # Tabs to contain README and player code to the left.
251
+ vertical padding: 0, spacing: 0 do
252
+ @file_tabs_group = group do
253
+ @file_tab_buttons = horizontal padding: 0, spacing: 4 do
254
+ %w[README player.rb].each do |name|
255
+ radio_button(name.to_s, name, border_thickness: 0, tip: "View #{File.join profile.player_path, name}")
256
+ end
257
+
258
+ horizontal padding_left: 50, padding: 0 do
259
+ button "copy", tip: "Copy displayed file to clipboard", font_height: 12, border_thickness: 0, padding: 4 do
260
+ Clipboard.copy @file_contents[@file_tabs_group.value].stripped_text
261
+ end
262
+
263
+ # Default editor for Windows.
264
+ ENV['EDITOR'] = "notepad" if Gem.win_platform? and ENV['EDITOR'].nil?
149
265
 
150
- # Log on the right
151
- vertical padding: 0, width: 380, height: 278 do
152
- @log_window = scroll_window width: 380, height: 278 do
153
- @log_display = text_area width: 368, editable: false
266
+ tip = ENV['EDITOR'] ? "Edit file in #{ENV['EDITOR']} (set EDITOR environment variable to use a different editor)" : "ENV['EDITOR'] not set"
267
+ button "edit", tip: tip, enabled: ENV['EDITOR'], font_height: 12, border_thickness: 0, padding: 4 do
268
+ command = %<#{ENV['EDITOR']} "#{File.join(level.player_path, @file_tabs_group.value)}">
269
+ $stdout.puts "SYSTEM: #{command}"
270
+ Thread.new { system command }
271
+ end
154
272
  end
155
- @log_window.background_color = @log_display.background_color
273
+ end
274
+
275
+ subscribe :changed do |_, value|
276
+ current = @file_tab_buttons.find {|elem| elem.value == value }
277
+ @file_tab_buttons.each {|t| t.enabled = (t != current) }
278
+ current.color, current.background_color = current.background_color, current.color
279
+
280
+ @file_tab_contents.clear
281
+ @file_tab_contents.add @file_tab_windows[value]
156
282
  end
157
283
  end
284
+
285
+ # Contents of those tabs.
286
+ @file_tab_contents = vertical padding: 0, width: 380, height: $window.height * 0.5
287
+
288
+ create_file_tab_windows
289
+ @file_tabs_group.value = "README"
158
290
  end
291
+ end
159
292
 
160
- prepare_level
293
+ def update_turn_delay
294
+ @turn_duration_label.text = "%2d" % [(MAX_TURN_DELAY / TURN_DELAY_STEP) + 1 - (@config.turn_delay / TURN_DELAY_STEP)]
295
+ @turn_slower_button.enabled = @config.turn_delay < MAX_TURN_DELAY
296
+ @turn_faster_button.enabled = @config.turn_delay > MIN_TURN_DELAY
297
+ @turn_duration_label.tip = "Speed of turns (high is faster; currently turns take #{(@config.turn_delay * 1000).to_i}ms)"
161
298
  end
162
299
 
163
- def create_tab_windows
164
- @tab_windows = {}
165
- @tab_windows["README"] = Fidgit::ScrollWindow.new width: 380, height: 250 do
166
- @readme_display = text_area width: 368, editable: false
300
+ def create_log_tab_windows
301
+ @log_tab_windows = {}
302
+ @log_contents = {}
303
+ ["current turn", "full log"].each do |log|
304
+ @log_tab_windows[log] = Fidgit::ScrollWindow.new width: 380, height: 250 do
305
+ @log_contents[log] = text_area width: 368, editable: false
306
+ end
167
307
  end
168
- @tab_windows["README"].background_color = @readme_display.background_color
308
+ end
169
309
 
170
- @tab_windows["player.rb"] = Fidgit::ScrollWindow.new width: 380, height: 250 do
171
- @code_display = text_area width: 368, editable: false
310
+ def create_file_tab_windows
311
+ @file_tab_windows = {}
312
+ @file_contents = {}
313
+ ["README", "player.rb"].each do |file|
314
+ @file_tab_windows[file] = Fidgit::ScrollWindow.new width: 380, height: 250 do
315
+ @file_contents[file] = text_area width: 368, editable: false
316
+ end
172
317
  end
173
- @tab_windows["player.rb"].background_color = @code_display.background_color
174
318
  end
175
319
 
176
320
  def prepare_level
177
- @log_display.text = ""
321
+ @recorded_log = nil # Not initially logging.
322
+
323
+ @log_contents["full log"].text = "" #unless profile.epic? # TODO: Might need to avoid this, since it could get REALLY long.
178
324
  @continue_button.enabled = false
179
325
  @hint_button.enabled = false
180
326
  @reset_button.enabled = false
@@ -182,9 +328,68 @@ module RubyArmor
182
328
 
183
329
  @exception = nil
184
330
 
185
- @game.prepare_next_level unless profile.current_level.number > 0
331
+ if profile.current_level.number.zero?
332
+ if profile.epic?
333
+ @game.prepare_epic_mode
334
+ profile.level_number += 1
335
+ profile.current_epic_score = 0
336
+ profile.current_epic_grades = {}
337
+ else
338
+ @game.prepare_next_level
339
+ end
340
+ end
341
+
342
+ create_sync_timer
343
+
344
+ @level = profile.current_level # Need to store this because it gets forgotten by the profile/game :(
345
+ @playing = false
346
+ level.load_level
347
+
348
+ # List of log entries, unit drawings and health made in each turn.
349
+ @turn_logs = Hash.new {|h, k| h[k] = "" }
350
+ @units_record = Array.new
351
+ @health = [level.warrior.health]
186
352
 
187
- # Continually poll the player code file to see when it is edited.
353
+ @review_button.enabled = ReviewCode.saved_levels? profile # Can't review code unless some has been saved.
354
+
355
+ self.turn = 0
356
+
357
+ @file_contents["README"].text = replace_syntax File.read(File.join(level.player_path, "README"))
358
+
359
+ # Initial log entry.
360
+ self.puts "- turn 0 -"
361
+ self.print floor.character
362
+ self.print "#{profile.warrior_name} climbs up to level #{level.number}\n"
363
+ @log_contents["full log"].text += @log_contents["current turn"].text
364
+
365
+ @tile_set = %w[beginner intermediate].index(profile.tower.name) || 2 # We don't know what the last tower will be called.
366
+
367
+ warrior = floor.units.find {|u| u.is_a? RubyWarrior::Units::Warrior }
368
+ @entry_x, @entry_y = warrior.position.x, warrior.position.y
369
+
370
+ # Reset the time-line slider.
371
+ @turn_slider.enabled = false
372
+ @turn_slider.value = 0
373
+
374
+ refresh_labels
375
+
376
+ # Work out how to offset the level graphics based on how large it is (should be centered in the level area.
377
+ level_width = floor.width * SPRITE_SCALE * SPRITE_WIDTH
378
+ level_height = floor.height * SPRITE_SCALE * SPRITE_HEIGHT
379
+ @level_offset_x = (@game_window.width - level_width) / 2
380
+ @level_offset_y = (@game_window.height - level_height) / 2
381
+
382
+ # Load the player's own code, which might explode!
383
+ begin
384
+ level.load_player
385
+ rescue SyntaxError, StandardError => ex
386
+ handle_exception ex
387
+ return
388
+ end
389
+ end
390
+
391
+ # Continually poll the player code file to see when it is edited.
392
+ def create_sync_timer
188
393
  stop_timer :refresh_code
189
394
  friendly_line_endings = false
190
395
  every(100, :name => :refresh_code) do
@@ -208,7 +413,7 @@ module RubyArmor
208
413
  end
209
414
  friendly_line_endings = true # Either will have or don't need to.
210
415
 
211
- @code_display.text = stripped_code
416
+ @file_contents["player.rb"].text = stripped_code
212
417
  @loaded_code = stripped_code
213
418
  prepare_level
214
419
  end
@@ -216,43 +421,25 @@ module RubyArmor
216
421
  # This can happen if the file is busy.
217
422
  end
218
423
  end
424
+ end
219
425
 
220
- @level = profile.current_level # Need to store this because it gets forgotten by the profile/game :(
221
- @turn = 0
222
- @playing = false
223
- level.load_level
224
-
225
- @readme_display.text = replace_syntax File.read(File.join(level.player_path, "README"))
226
-
227
- print "#{profile.warrior_name} climbs up to level #{level.number}\n"
228
- @tile_set = %w[beginner intermediate].index(profile.tower.name) || 2 # We don't know what the last tower will be called.
229
-
230
- warrior = floor.units.find {|u| u.is_a? RubyWarrior::Units::Warrior }
231
- @entry_x, @entry_y = warrior.position.x, warrior.position.y
232
-
233
- refresh_labels
234
-
235
- # Load the player's own code, which might explode!
236
- begin
237
- level.load_player
238
- rescue SyntaxError, StandardError => ex
239
- handle_exception ex
240
- return
241
- end
426
+ def effective_turn
427
+ @turn_slider.enabled? ? @turn_slider.value : @turn
242
428
  end
243
429
 
244
430
  def refresh_labels
245
431
  @tower_label.text = profile.tower.name.capitalize
246
- @level_label.text = "Level: #{level.number}"
247
- @turn_label.text = "Turn: #{(@turn + 1).to_s.rjust(2)}"
248
- @health_label.text = "Health: #{level.warrior.health.to_s.rjust(2)}"
432
+ @level_label.text = "Level: #{profile.epic? ? "E" : " "}#{level.number}"
433
+ @level_label.tip = profile.epic? ? "Playing in EPIC mode" : "Playing in normal mode"
434
+ @turn_label.text = "Turn: #{effective_turn.to_s.rjust(2)}"
435
+ @health_label.text = "Health: #{@health[effective_turn].to_s.rjust(2)}"
249
436
  end
250
437
 
251
438
  def start_level
252
439
  @reset_button.enabled = true
253
440
  @start_button.enabled = false
254
441
  @playing = true
255
- @take_next_turn_at = Time.now + @config.turn_delay
442
+ self.turn = 0
256
443
  refresh_labels
257
444
  end
258
445
 
@@ -289,40 +476,97 @@ module RubyArmor
289
476
  def level; @level; end
290
477
  def floor; level.floor; end
291
478
 
479
+ def recording_log?; not @recorded_log.nil?; end
480
+ def record_log
481
+ raise "block required" unless block_given?
482
+ @recorded_log = ""
483
+ record = ""
484
+ begin
485
+ yield
486
+ ensure
487
+ record = @recorded_log
488
+ @recorded_log = nil
489
+ end
490
+ record
491
+ end
492
+
292
493
  def play_turn
293
- self.puts "- turn #{@turn+1} -"
294
- self.print floor.character
494
+ self.turn += 1
495
+ self.puts "- turn #{turn.to_s.rjust(3)} -"
295
496
 
296
497
  begin
297
- floor.units.each(&:prepare_turn)
298
- floor.units.each(&:perform_turn)
498
+ actions = record_log do
499
+ floor.units.each(&:prepare_turn)
500
+ floor.units.each(&:perform_turn)
501
+ end
502
+
503
+ self.print floor.character # State after
504
+ self.print actions
299
505
  rescue => ex
300
506
  handle_exception ex
301
507
  return
302
508
  end
303
509
 
304
- @turn += 1
305
- level.time_bonus -= 1 if level.time_bonus > 0
510
+ @health[turn] = level.warrior.health # Record health for later playback.
306
511
 
307
- @take_next_turn_at = Time.now + @config.turn_delay
512
+ level.time_bonus -= 1 if level.time_bonus > 0
308
513
 
309
514
  refresh_labels
310
515
 
311
516
  if level.passed?
312
- if @game.next_level.exists?
313
- @continue_button.enabled = true
517
+ @reset_button.enabled = false if profile.epic? # Continue will save performance; reset won't.
518
+ @continue_button.enabled = true
519
+
520
+ if profile.next_level.exists?
314
521
  self.puts "Success! You have found the stairs."
522
+ level.tally_points
523
+
524
+ if profile.epic?
525
+ # Start the next level immediately.
526
+ self.puts "\n#{"-" * 25}\n"
527
+
528
+ # Rush onto the next level immediately!
529
+ profile.level_number += 1
530
+ prepare_level
531
+ start_level
532
+ end
315
533
  else
316
534
  self.puts "CONGRATULATIONS! You have climbed to the top of the tower and rescued the fair maiden Ruby."
535
+ level.tally_points
536
+
537
+ if profile.epic?
538
+ self.puts @game.final_report if @game.final_report
539
+ profile.save
540
+ end
317
541
  end
318
- level.tally_points
542
+
543
+ level_ended
544
+
319
545
  elsif level.failed?
320
- @hint_button.enabled = true
546
+ level_ended
321
547
  self.puts "Sorry, you failed level #{level.number}. Change your script and try again."
548
+
322
549
  elsif out_of_time?
323
- @hint_button.enabled = true
550
+ level_ended
324
551
  self.puts "Sorry, you starved to death on level #{level.number}. Change your script and try again."
552
+
325
553
  end
554
+
555
+ # Add the full turn's text into the main log at once, to save on re-calculations.
556
+ @log_contents["full log"].text += @log_contents["current turn"].text
557
+ @log_tab_windows["full log"].offset_y = Float::INFINITY
558
+
559
+ self.puts
560
+ end
561
+
562
+ # Not necessarily complete; just finished.
563
+ def level_ended
564
+ return if profile.epic?
565
+
566
+ @hint_button.enabled = true
567
+ @turn_slider.enabled = true
568
+ @turn_slider.instance_variable_set :@range, 0..turn
569
+ @turn_slider.value = turn
326
570
  end
327
571
 
328
572
  def handle_exception(exception)
@@ -336,9 +580,9 @@ module RubyArmor
336
580
  # TODO: Make this work without it raising exceptions in Fidgit :(
337
581
  #exception.message =~ /:(\d+):/
338
582
  #exception_line = $1.to_i - 1
339
- #code_lines = @code_display.text.split "\n"
583
+ #code_lines = @file_contents["player.rb"].text.split "\n"
340
584
  #code_lines[exception_line] = "<c=ff0000>{code_lines[exception_line]}</c>"
341
- #@code_display.text = code_lines.join "\n"
585
+ #@file_contents["player.rb"].text = code_lines.join "\n"
342
586
 
343
587
  @start_button.enabled = false
344
588
 
@@ -346,7 +590,7 @@ module RubyArmor
346
590
  end
347
591
 
348
592
  def out_of_time?
349
- @turn > @max_turns
593
+ turn >= MAX_TURNS
350
594
  end
351
595
 
352
596
  def puts(message = "")
@@ -354,80 +598,101 @@ module RubyArmor
354
598
  end
355
599
 
356
600
  def print(message)
357
- #$stdout.puts message
358
- @log_display.text += replace_log message
359
- @log_window.offset_y = Float::INFINITY
601
+ if recording_log?
602
+ @recorded_log << message
603
+ else
604
+ @turn_logs[turn] << message
605
+ @log_contents["current turn"].text = replace_log @turn_logs[turn]
606
+ end
360
607
  end
361
608
 
362
609
  def draw
363
610
  super
364
611
 
365
- $window.translate SPRITE_OFFSET_X, SPRITE_OFFSET_Y do
612
+ $window.translate @level_offset_x, @level_offset_y do
366
613
  $window.scale SPRITE_SCALE do
367
- # Draw horizontal walls.
368
- floor.width.times do |x|
369
- light = x % 2
370
- light = 2 if light == 1 and (Gosu::milliseconds / 500) % 2 == 0
371
- @tiles[light + 3, @tile_set].draw x * SPRITE_WIDTH, -SPRITE_HEIGHT, 0
372
- @tiles[3, @tile_set].draw x * SPRITE_WIDTH, floor.height * SPRITE_HEIGHT, floor.height
373
- end
374
- # Draw vertical walls.
375
- (-1..floor.height).each do |y|
376
- @tiles[3, @tile_set].draw -SPRITE_WIDTH, y * SPRITE_HEIGHT, y
377
- @tiles[3, @tile_set].draw floor.width * SPRITE_WIDTH, y * SPRITE_HEIGHT, y
378
- end
379
-
380
- # Draw floor
381
- floor.width.times do |x|
382
- floor.height.times do |y|
383
- @tiles[(x + y + 1) % 2, @tile_set].draw x * SPRITE_WIDTH, y * SPRITE_HEIGHT, 0, 1, 1, FLOOR_COLOR
384
- end
385
- end
614
+ draw_map
386
615
 
387
616
  # Draw stairs (exit)
388
- @tiles[2, @tile_set].draw floor.stairs_location[0] * SPRITE_WIDTH, floor.stairs_location[1] * SPRITE_HEIGHT, 0
617
+ if floor.stairs_location[0] == 0
618
+ # flip when on the left hand side.
619
+ @tiles[2, @tile_set].draw (floor.stairs_location[0] + 1) * SPRITE_WIDTH, floor.stairs_location[1] * SPRITE_HEIGHT, 0, -1
620
+ else
621
+ @tiles[2, @tile_set].draw floor.stairs_location[0] * SPRITE_WIDTH, floor.stairs_location[1] * SPRITE_HEIGHT, 0
622
+ end
389
623
 
390
624
  # Draw trapdoor (entrance)
391
625
  @tiles[6, @tile_set].draw @entry_x * SPRITE_WIDTH, @entry_y * SPRITE_HEIGHT, 0
392
626
 
393
- # Draw units.
394
- floor.units.each do |unit|
395
- sprite = case unit
396
- when RubyWarrior::Units::Warrior
397
- @warrior_sprites[FACINGS[unit.position.direction], WARRIORS[@config.warrior_class]]
398
- when RubyWarrior::Units::Wizard
399
- @mob_sprites[0, 1]
400
- when RubyWarrior::Units::ThickSludge
401
- @mob_sprites[2, 1]
402
- when RubyWarrior::Units::Sludge
403
- @mob_sprites[1, 1]
404
- when RubyWarrior::Units::Archer
405
- @mob_sprites[3, 1]
406
- when RubyWarrior::Units::Captive
407
- @mob_sprites[0, 2]
408
- when RubyWarrior::Units::Golem
409
- @mob_sprites[1, 2]
410
- else
411
- raise "unknown unit: #{unit.class}"
412
- end
413
-
414
- sprite.draw unit.position.x * SPRITE_WIDTH, unit.position.y * SPRITE_HEIGHT, unit.position.y
415
-
416
- if unit.bound?
417
- @mob_sprites[2, 2].draw unit.position.x * SPRITE_WIDTH, unit.position.y * SPRITE_HEIGHT, unit.position.y
418
- end
627
+ draw_units
628
+ end
629
+ end
630
+ end
631
+
632
+ def draw_map
633
+ # Draw horizontal walls.
634
+ floor.width.times do |x|
635
+ light = x % 2
636
+ light = 2 if light == 1 and (Gosu::milliseconds / 500) % 2 == 0
637
+ @tiles[light + 3, @tile_set].draw x * SPRITE_WIDTH, -SPRITE_HEIGHT, 0
638
+ @tiles[3, @tile_set].draw x * SPRITE_WIDTH, floor.height * SPRITE_HEIGHT, floor.height
639
+ end
640
+ # Draw vertical walls.
641
+ (-1..floor.height).each do |y|
642
+ @tiles[3, @tile_set].draw -SPRITE_WIDTH, y * SPRITE_HEIGHT, y
643
+ @tiles[3, @tile_set].draw floor.width * SPRITE_WIDTH, y * SPRITE_HEIGHT, y
644
+ end
645
+
646
+ # Draw floor
647
+ floor.width.times do |x|
648
+ floor.height.times do |y|
649
+ @tiles[(x + y + 1) % 2, @tile_set].draw x * SPRITE_WIDTH, y * SPRITE_HEIGHT, 0, 1, 1, FLOOR_COLOR
650
+ end
651
+ end
652
+ end
653
+
654
+ def draw_units
655
+ @units_record[effective_turn] ||= $window.record 1, 1 do
656
+ floor.units.sort_by {|u| u.position.y }.each do |unit|
657
+ sprite = case unit
658
+ when RubyWarrior::Units::Warrior
659
+ @warrior_sprites[FACINGS[unit.position.direction], WARRIORS[@config.warrior_class]]
660
+ when RubyWarrior::Units::Wizard
661
+ @mob_sprites[0, 1]
662
+ when RubyWarrior::Units::ThickSludge
663
+ @mob_sprites[2, 1]
664
+ when RubyWarrior::Units::Sludge
665
+ @mob_sprites[1, 1]
666
+ when RubyWarrior::Units::Archer
667
+ @mob_sprites[3, 1]
668
+ when RubyWarrior::Units::Captive
669
+ @mob_sprites[0, 2]
670
+ when RubyWarrior::Units::Golem
671
+ @mob_sprites[1, 2]
672
+ else
673
+ raise "unknown unit: #{unit.class}"
674
+ end
675
+
676
+ sprite.draw unit.position.x * SPRITE_WIDTH, unit.position.y * SPRITE_HEIGHT, unit.position.y
677
+
678
+ if unit.bound?
679
+ @mob_sprites[2, 2].draw unit.position.x * SPRITE_WIDTH, unit.position.y * SPRITE_HEIGHT, 0
419
680
  end
420
681
  end
421
682
  end
683
+
684
+ @units_record[effective_turn].draw 0, 0, 0
422
685
  end
423
686
 
424
687
  def unit_health_changed(unit, amount)
688
+ return unless @level_offset_x # Ignore changes out of order, such as between epic levels.
689
+
425
690
  color = (amount > 0) ? Color::GREEN : Color::RED
426
691
  y_offset = (amount > 0) ? -0.15 : +0.15
427
692
  FloatingText.create "#{amount > 0 ? "+" : ""}#{amount}",
428
693
  :color => color,
429
- :x => unit.position.x * SPRITE_SCALE * SPRITE_WIDTH + (SPRITE_SCALE * SPRITE_WIDTH / 2) + SPRITE_OFFSET_X,
430
- :y => (unit.position.y + y_offset) * SPRITE_SCALE * SPRITE_HEIGHT + SPRITE_OFFSET_Y
694
+ :x => unit.position.x * SPRITE_SCALE * SPRITE_WIDTH + (SPRITE_SCALE * SPRITE_WIDTH / 2) + @level_offset_x,
695
+ :y => (unit.position.y + y_offset) * SPRITE_SCALE * SPRITE_HEIGHT + @level_offset_y
431
696
  end
432
697
 
433
698
  def update
@@ -0,0 +1,73 @@
1
+ module RubyArmor
2
+ class ReviewCode < Fidgit::DialogState
3
+ LEVELS = (1..9).to_a + [:EPIC]
4
+
5
+ class << self
6
+ def path_for_level(profile, level)
7
+ File.join(profile.player_path, "ruby_armor/player_#{level.is_a?(Symbol) ? level : level.to_s.rjust(3, '0')}.rb")
8
+ end
9
+
10
+ # Check if there are levels saved that can be recalled.
11
+ def saved_levels?(profile)
12
+ LEVELS.any? {|level| File.exists? path_for_level(profile, level) }
13
+ end
14
+ end
15
+
16
+ def initialize(profile)
17
+ super(shadow_full: true)
18
+
19
+ @profile = profile
20
+
21
+ vertical spacing: 10, align: :center, background_color: Color::BLACK do
22
+ label "Reviewing code that completed levels in #{profile.tower.name} tower", font_height: 20
23
+
24
+ @tab_group = group do
25
+ @tab_buttons = horizontal padding: 0, spacing: 4 do
26
+ LEVELS.each do |level|
27
+ if File.exists?(path_for_level(level))
28
+ radio_button level.to_s, level, border_thickness: 0,
29
+ tip: "View code used to complete level #{level}"
30
+ else
31
+ button level.to_s, border_thickness: 0, enabled: false,
32
+ tip: "No code saved for level #{level}"
33
+ end
34
+ end
35
+
36
+ horizontal padding_left: 50, padding: 0 do
37
+ button "copy", tip: "Copy displayed code to clipboard", font_height: 12, border_thickness: 0, padding: 4 do
38
+ Clipboard.copy @code.stripped_text
39
+ end
40
+ end
41
+ end
42
+
43
+ subscribe :changed do |_, value|
44
+ buttons = @tab_buttons.each.grep Fidgit::RadioButton
45
+ current = buttons.find {|b| b.value == value }
46
+ buttons.each {|b| b.enabled = (b != current) }
47
+ current.color, current.background_color = current.background_color, current.color
48
+
49
+ @code.text = File.read path_for_level(value)
50
+ end
51
+ end
52
+
53
+ # Contents of those tabs.
54
+ vertical padding: 0 do
55
+ scroll_window width: 700, height: 430 do
56
+ @code = text_area width: 680
57
+ end
58
+ end
59
+
60
+ button "Close", shortcut: :escape, align_h: :center, border_thickness: 0 do
61
+ hide
62
+ end
63
+
64
+ # Pick the last level we have completed (and saved the code).
65
+ @tab_group.value = LEVELS.to_a.reverse.find {|level| File.exists? path_for_level(level) }
66
+ end
67
+ end
68
+
69
+ def path_for_level(level)
70
+ self.class.path_for_level @profile, level
71
+ end
72
+ end
73
+ end
@@ -1,3 +1,3 @@
1
1
  module RubyArmor
2
- VERSION = "0.0.4alpha"
2
+ VERSION = "0.0.5alpha"
3
3
  end
@@ -1,12 +1,23 @@
1
+ require 'fileutils'
2
+
1
3
  require_relative "base_user_data"
2
4
 
3
5
  module RubyArmor
4
6
  class WarriorConfig < BaseUserData
5
7
  DEFAULT_CONFIG = File.expand_path "../../../config/default_config.yml", __FILE__
6
- CONFIG_FILE = "ruby_armour.yml"
8
+ OLD_CONFIG_FILE = "ruby_armour.yml"
9
+ CONFIG_FILE = "ruby_armor/config.yml"
7
10
 
8
11
  def initialize(profile)
9
- super File.join(profile.player_path, CONFIG_FILE), DEFAULT_CONFIG
12
+ # Originally, config file was just in the folder. Move into its own folder, so we can put more in there.
13
+ old_config_file = File.join(profile.player_path, OLD_CONFIG_FILE)
14
+ config_file = File.join(profile.player_path, CONFIG_FILE)
15
+ if File.exists? old_config_file
16
+ FileUtils.mkdir_p File.dirname(config_file)
17
+ FileUtils.mv old_config_file, config_file
18
+ end
19
+
20
+ super config_file, DEFAULT_CONFIG
10
21
  end
11
22
 
12
23
  def turn_delay; data[:turn_delay]; end
@@ -5,7 +5,7 @@ module RubyArmor
5
5
 
6
6
  Gosu::enable_undocumented_retrofication
7
7
 
8
- self.caption = "RubyArmor for RubyWarrior"
8
+ self.caption = "RubyArmor v#{RubyArmor::VERSION} (for RubyWarrior)"
9
9
  push_game_state ChooseProfile
10
10
  end
11
11
  end
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_armor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4alpha
4
+ version: 0.0.5alpha
5
5
  prerelease: 5
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-26 00:00:00.000000000 Z
12
+ date: 2012-02-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rubywarrior
16
- requirement: &25413276 !ruby/object:Gem::Requirement
16
+ requirement: &21159480 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: 0.1.2
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *25413276
24
+ version_requirements: *21159480
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: gosu
27
- requirement: &25412196 !ruby/object:Gem::Requirement
27
+ requirement: &21157404 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: 0.7.41
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *25412196
35
+ version_requirements: *21157404
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: chingu
38
- requirement: &25411440 !ruby/object:Gem::Requirement
38
+ requirement: &21155796 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,21 +43,21 @@ dependencies:
43
43
  version: 0.9rc7
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *25411440
46
+ version_requirements: *21155796
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: fidgit
49
- requirement: &25409844 !ruby/object:Gem::Requirement
49
+ requirement: &21154332 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
53
53
  - !ruby/object:Gem::Version
54
- version: 0.2.2
54
+ version: 0.2.4
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *25409844
57
+ version_requirements: *21154332
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: releasy
60
- requirement: &25406592 !ruby/object:Gem::Requirement
60
+ requirement: &21153432 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,7 +65,7 @@ dependencies:
65
65
  version: 0.2.2
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *25406592
68
+ version_requirements: *21153432
69
69
  description:
70
70
  email:
71
71
  - bil.bagpuss@gmail.com
@@ -85,6 +85,7 @@ files:
85
85
  - lib/ruby_armor/sprite_sheet.rb
86
86
  - lib/ruby_armor/states/choose_profile.rb
87
87
  - lib/ruby_armor/states/play.rb
88
+ - lib/ruby_armor/states/review_code.rb
88
89
  - lib/ruby_armor/version.rb
89
90
  - lib/ruby_armor/warrior_config.rb
90
91
  - lib/ruby_armor/window.rb
@@ -106,12 +107,9 @@ require_paths:
106
107
  required_ruby_version: !ruby/object:Gem::Requirement
107
108
  none: false
108
109
  requirements:
109
- - - ! '>='
110
+ - - ~>
110
111
  - !ruby/object:Gem::Version
111
- version: '0'
112
- segments:
113
- - 0
114
- hash: -574849791
112
+ version: 1.9.2
115
113
  required_rubygems_version: !ruby/object:Gem::Requirement
116
114
  none: false
117
115
  requirements: