ruby_armor 0.0.4alpha → 0.0.5alpha

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