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.
- checksums.yaml +7 -0
- data/.gitignore +54 -0
- data/Gemfile +3 -0
- data/README.md +57 -0
- data/Rakefile +15 -0
- data/bin/ruby_armor +7 -0
- data/bin/ruby_armor.rbw +7 -0
- data/config/default_config.yml +5 -0
- data/config/gui/schema.yml +73 -0
- data/lib/ruby_armor.rb +42 -0
- data/lib/ruby_armor/base_user_data.rb +38 -0
- data/lib/ruby_armor/dungeon_view.rb +234 -0
- data/lib/ruby_armor/floating_text.rb +22 -0
- data/lib/ruby_armor/ruby_warrior_ext/abilities/rest.rb +13 -0
- data/lib/ruby_armor/ruby_warrior_ext/player_generator.rb +17 -0
- data/lib/ruby_armor/ruby_warrior_ext/position.rb +5 -0
- data/lib/ruby_armor/ruby_warrior_ext/ui.rb +44 -0
- data/lib/ruby_armor/ruby_warrior_ext/units/base.rb +12 -0
- data/lib/ruby_armor/sprite_sheet.rb +25 -0
- data/lib/ruby_armor/states/choose_profile.rb +61 -0
- data/lib/ruby_armor/states/create_profile.rb +79 -0
- data/lib/ruby_armor/states/play.rb +606 -0
- data/lib/ruby_armor/states/review_code.rb +75 -0
- data/lib/ruby_armor/version.rb +3 -0
- data/lib/ruby_armor/warrior_config.rb +35 -0
- data/lib/ruby_armor/window.rb +12 -0
- data/media/fonts/Licence.txt +7 -0
- data/media/fonts/ProggyCleanSZ.ttf +0 -0
- data/media/images/mobs.png +0 -0
- data/media/images/tiles.png +0 -0
- data/media/images/warriors.png +0 -0
- data/rake/gemspec.rake +48 -0
- data/rake/releasy.rake +43 -0
- data/ruby_armor.gemspec +28 -0
- metadata +172 -0
@@ -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,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
|