Module: DisplayController

Includes:
Remedy
Defined in:
lib/terminal_hero/modules/display_controller.rb

Overview

Controls the display of output to the user

Class Method Summary collapse

Class Method Details

.calc_view_distance(size: Console.size) ⇒ Object

Set the map render distance to fit within a given console size



131
132
133
134
135
# File 'lib/terminal_hero/modules/display_controller.rb', line 131

def self.calc_view_distance(size: Console.size)
  horizontal = Utils.collar(2, size.cols / 4 - 2, GameData::MAX_H_VIEW_DIST)
  vertical = Utils.collar(2, size.rows / 2 - 5, GameData::MAX_V_VIEW_DIST)
  return [horizontal, vertical]
end

.calculate_padding(header, content, size) ⇒ Object

Calculate the amount of padding required to center a map view



160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/terminal_hero/modules/display_controller.rb', line 160

def self.calculate_padding(header, content, size)
  # Top padding is half of the console height minus the view height
  top_pad = (size.rows - (header.lines.length + content.lines.length)) / 2
  # Get length of the longest line of the view (to determine view width)
  view_width = [
    header.lines.map(&:uncolorize).max_by(&:length).length,
    content.lines.map(&:uncolorize).max_by(&:length).length
  ].max
  # Left padding is half of the console width minus the view width.
  left_pad = (size.cols - view_width) / 2
  return top_pad, left_pad
end

.cancel_resize_hookObject

Cancel the console resize hook (eg. when leaving the map view)



202
203
204
# File 'lib/terminal_hero/modules/display_controller.rb', line 202

def self.cancel_resize_hook
  Console::Resize.default_console_resized_hook!
end

.center_view!(header, content, size) ⇒ Object

Given a header, content and console size, pad the header and content to center them in the console.



174
175
176
177
178
179
# File 'lib/terminal_hero/modules/display_controller.rb', line 174

def self.center_view!(header, content, size)
  top_pad, left_pad = calculate_padding(header, content, size)
  top_pad.times { header.lines.unshift(" ") }
  content.lines.map! { |line| "#{' ' * left_pad}#{line}" }
  header.lines.map! { |line| "#{' ' * left_pad}#{line}" }
end

.clearObject

Clear the visible terminal display (without clearing terminal history)



236
237
238
# File 'lib/terminal_hero/modules/display_controller.rb', line 236

def self.clear
  ANSI::Screen.safe_reset!
end

.display_ascii(msg) ⇒ Object

Displays a message in an ASCII art font. Return the prompt object for use with subsequent prompts.



28
29
30
31
32
33
34
# File 'lib/terminal_hero/modules/display_controller.rb', line 28

def self.display_ascii(msg)
  clear
  font = TTY::Font.new(:standard)
  prompt = TTY::Prompt.new
  prompt.say(msg.call(font, Console.size))
  return prompt
end

.display_messages(msgs, pause: true) ⇒ Object

Display a series of messages, waiting for keypress input to advance



43
44
45
46
47
48
49
50
51
# File 'lib/terminal_hero/modules/display_controller.rb', line 43

def self.display_messages(msgs, pause: true)
  prompt = TTY::Prompt.new(quiet: true)
  print "\n"
  msgs.each do |msg|
    puts msg
    print "\n"
    prompt.keypress("Press any key...") if pause
  end
end

.display_stat_menu(stats, points, line_no, header, footer) ⇒ Object

Display the stat point allocation menu to the user



104
105
106
107
108
109
110
111
112
# File 'lib/terminal_hero/modules/display_controller.rb', line 104

def self.display_stat_menu(stats, points, line_no, header, footer)
  screen = Viewport.new
  menu = Content.new
  lines = stats.values.map { |stat| "#{stat[:name]}: #{stat[:value]}" }
  lines[line_no] = lines[line_no].colorize(:light_blue)
  menu.lines.push " ", "Stat points remaining: #{points}", " "
  lines.each { |line| menu << line }
  screen.draw(menu, [0, 0], header, footer)
end

.draw_map(map, player, size: Console.size, view_dist: calc_view_distance(size: size)) ⇒ Object

Draws one frame of the visible portion of the map



182
183
184
185
186
187
188
189
190
191
# File 'lib/terminal_hero/modules/display_controller.rb', line 182

def self.draw_map(map, player, size: Console.size, view_dist: calc_view_distance(size: size))
  screen, header, map_display = setup_map_view(player)
  filter_visible(map.grid, player.coords).each do |row|
    map_display << row.join(" ")
  end
  # Pushing additional row prevents truncation in smaller terminal sizes
  map_display << " " * (view_dist[0] * 2)
  center_view!(header, map_display, size)
  screen.draw(map_display, Size.new(0, 0), header)
end

.filter_visible(grid, camera_coords, size: Console.size, view_dist: calc_view_distance(size: size)) ⇒ Object

Given a grid, camera co-ordinates and view distances, return a grid containing only squares within the camera’s field of view



148
149
150
151
152
153
154
155
156
157
# File 'lib/terminal_hero/modules/display_controller.rb', line 148

def self.filter_visible(grid, camera_coords, size: Console.size, view_dist: calc_view_distance(size: size))
  h_view_dist, v_view_dist = view_dist
  # Filter rows outside view distance
  field_of_view = grid.map do |row|
    row.reject.with_index { |_cell, x_index| (camera_coords[:x] - x_index).abs > h_view_dist }
  end
  # Filter columns outside view distance
  field_of_view.reject!.with_index { |_row, y_index| (camera_coords[:y] - y_index).abs > v_view_dist }
  return field_of_view
end

.level_up(player, levels) ⇒ Object

When the player levels up, display the number of levels gained



230
231
232
233
# File 'lib/terminal_hero/modules/display_controller.rb', line 230

def self.level_up(player, levels)
  display_ascii(GameData::ASCII_ART[:level_up])
  display_messages(GameData::MESSAGES[:leveled_up].call(player, levels))
end

.post_combat(outcome, player, xp_amount) ⇒ Object

Display relevant information to the user after the end of a combat encounter.



217
218
219
220
221
222
223
224
225
226
227
# File 'lib/terminal_hero/modules/display_controller.rb', line 217

def self.post_combat(outcome, player, xp_amount)
  case outcome
  when :victory
    display_messages(GameData::MESSAGES[:combat_victory].call(xp_amount))
  when :defeat
    display_messages(GameData::MESSAGES[:combat_defeat].call(xp_amount))
    display_messages(GameData::MESSAGES[:level_progress].call(player))
  when :escaped
    display_messages(GameData::MESSAGES[:combat_escaped])
  end
end

.prompt_character_nameObject

Prompt the user to enter a character name when creating a character



54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/terminal_hero/modules/display_controller.rb', line 54

def self.prompt_character_name
  begin
    prompt = display_ascii(GameData::ASCII_ART[:title])
    name = prompt.ask("Please enter a name for your character: ")
    unless InputHandler.character_name_valid?(name)
      raise InvalidInputError.new(requirements: GameData::VALIDATION_REQUIREMENTS[:character_name])
    end
  rescue InvalidInputError => e
    display_messages([e.message.colorize(:red), "Please try again.".colorize(:red)])
    retry
  end
  return name
end

.prompt_combat_action(player, enemy) ⇒ Object

Display the combat action selection menu and return user’s selection



207
208
209
210
211
212
213
214
# File 'lib/terminal_hero/modules/display_controller.rb', line 207

def self.prompt_combat_action(player, enemy)
  clear
  display_messages(GameData::MESSAGES[:combat_status].call(player, enemy), pause: false)
  prompt = TTY::Prompt.new
  answer = prompt.select("\nWhat would you like to do?", GameData::COMBAT_MENU_OPTIONS)
  print "\n"
  return answer
end

.prompt_save_name(name = nil) ⇒ Object

Prompt the user to enter the name of the character they want to attempt to load



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/terminal_hero/modules/display_controller.rb', line 86

def self.prompt_save_name(name = nil)
  display_ascii(GameData::ASCII_ART[:title])
  begin
    name = TTY::Prompt.new.ask("Please enter the name of the character you want to load: ") if name.nil?
    unless InputHandler.character_name_valid?(name)
      raise InvalidInputError.new(requirements: GameData::VALIDATION_REQUIREMENTS[:character_name])
    end
  rescue InvalidInputError => e
    display_messages([e.message.colorize(:red)])
    return false unless prompt_yes_no(GameData::PROMPTS[:re_load])

    name = nil
    retry
  end
  return name
end

.prompt_stat_allocation(starting_stats: GameData::DEFAULT_STATS, starting_points: GameData::STAT_POINTS_PER_LEVEL) ⇒ Object

Prompt the user and get their input to allocate stat points using a stat menu



115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/terminal_hero/modules/display_controller.rb', line 115

def self.prompt_stat_allocation(
  starting_stats: GameData::DEFAULT_STATS,
  starting_points: GameData::STAT_POINTS_PER_LEVEL
)
  stat_menu = StatMenu.new(starting_stats, starting_points)
  display_stat_menu(*stat_menu.get_display_parameters)
  input = Interaction.new
  input.loop do |key|
    finished, stats = stat_menu.process_input(key.name)
    return stats if finished

    display_stat_menu(*stat_menu.get_display_parameters)
  end
end

.prompt_title_menuObject

Displays the title menu



37
38
39
40
# File 'lib/terminal_hero/modules/display_controller.rb', line 37

def self.prompt_title_menu
  prompt = display_ascii(GameData::ASCII_ART[:title])
  return prompt.select("What would you like to do?", GameData::TITLE_MENU_OPTIONS)
end

.prompt_tutorial(repeat: false) ⇒ Object

Ask the user whether they would like to view the tutorial



78
79
80
81
82
83
# File 'lib/terminal_hero/modules/display_controller.rb', line 78

def self.prompt_tutorial(repeat: false)
  display_ascii(GameData::ASCII_ART[:title])
  verb = repeat ? "repeat" : "see"
  message = "Would you like to #{verb} the tutorial?"
  return prompt_yes_no(message, default_no: repeat)
end

.prompt_yes_no(msg, default_no: false) ⇒ Object

Prompt the user for whether to re-try a failed action



69
70
71
72
73
74
75
# File 'lib/terminal_hero/modules/display_controller.rb', line 69

def self.prompt_yes_no(msg, default_no: false)
  TTY::Prompt.new.select(msg) do |menu|
    menu.default default_no ? "No" : "Yes"
    menu.choice "Yes", true
    menu.choice "No", false
  end
end

.set_resize_hook(map, player) ⇒ Object

Sets a hook to draw the map (with adjusted view distance) when the console is resized



195
196
197
198
199
# File 'lib/terminal_hero/modules/display_controller.rb', line 195

def self.set_resize_hook(map, player)
  Console.set_console_resized_hook! do |size|
    draw_map(map, player, size: size)
  end
end

.setup_map_view(player) ⇒ Object

Initialise variables required for draw_map



138
139
140
141
142
143
144
# File 'lib/terminal_hero/modules/display_controller.rb', line 138

def self.setup_map_view(player)
  screen = Viewport.new
  header = Header.new
  map_display = Content.new
  GameData::MAP_HEADER.call(player).each { |line| header.lines.push(line) }
  return [screen, header, map_display]
end