Module: GameController

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

Overview

Handles game loops and interactions between main objects

Class Method Summary collapse

Class Method Details

.character_creationObject

Get user input to create a new character by choosing a name and allocating stats.



84
85
86
87
88
89
90
91
# File 'lib/terminal_hero/modules/game_controller.rb', line 84

def self.character_creation
  # Prompt, then reprompt unless and until save name is not already taken or user confirms overwrite
  name = DisplayController.prompt_character_name
  name = DisplayController.prompt_character_name until confirm_save(name)
  stats = DisplayController.prompt_stat_allocation
  player, map = init_player_and_map(player_data: { name: name, stats: stats }).values_at(:player, :map)
  return [:world_map, [map, player]]
end

.check_combat_outcome(player, enemy, map, escaped: false) ⇒ Object

Return the outcome of a combat encounter along with parameters to pass to the next game state, or return false if combat has not ended



181
182
183
184
185
186
187
# File 'lib/terminal_hero/modules/game_controller.rb', line 181

def self.check_combat_outcome(player, enemy, map, escaped: false)
  return [:post_combat, [player, enemy, map, :defeat]] if player.dead?
  return [:post_combat, [player, enemy, map, :victory]] if enemy.dead?
  return [:post_combat, [player, enemy, map, :escaped]] if escaped

  return false
end

.combat_loop(player, map, tile, enemy = tile.entity) ⇒ Object

Manages a combat encounter by calling methods to get and process participant actions each round, determine when combat has ended, and return the outcome



236
237
238
239
240
241
242
243
244
245
246
# File 'lib/terminal_hero/modules/game_controller.rb', line 236

def self.combat_loop(player, map, tile, enemy = tile.entity)
  DisplayController.clear
  DisplayController.display_messages(GameData::MESSAGES[:enter_combat].call(enemy))
  actor = :player
  loop do
    combat_outcome = process_combat_turn(actor, player, enemy, map)
    return combat_outcome unless combat_outcome == false

    actor = actor == :enemy ? :player : :enemy
  end
end

.confirm_save(name) ⇒ Object

If the user attempts to create a character with the same name as an existing save file, confirm whether they want to override it



95
96
97
98
99
100
101
102
# File 'lib/terminal_hero/modules/game_controller.rb', line 95

def self.confirm_save(name)
  path = File.join(File.dirname(__FILE__), "../saves")
  if File.exist?(File.join(path, "#{name.downcase}.json"))
    return DisplayController.prompt_yes_no(GameData::PROMPTS[:overwrite_save].call(name), default_no: true)
  end

  return true
end

.enemy_act(player, enemy) ⇒ Object

Process one round of action by an enemy in combat.



173
174
175
176
177
# File 'lib/terminal_hero/modules/game_controller.rb', line 173

def self.enemy_act(player, enemy)
  action = :enemy_attack
  outcome = GameData::COMBAT_ACTIONS[action].call(player, enemy)
  return { action: action, outcome: outcome }
end

.enter(game_state, params = nil) ⇒ Object

Given a symbol corresponding to a key in the GAME_STATES hash (and optionally an array of parameters), calls a lambda triggering the method for that game state (which then returns the next game state + parameters).



31
32
33
# File 'lib/terminal_hero/modules/game_controller.rb', line 31

def self.enter(game_state, params = nil)
  GameData::GAME_STATES[game_state].call(self, params)
end

.exit_gameObject

Display an exit message and exit the application



36
37
38
39
# File 'lib/terminal_hero/modules/game_controller.rb', line 36

def self.exit_game
  DisplayController.display_messages(GameData::MESSAGES[:exit_game])
  exit
end

.fled_combat?(action_outcome) ⇒ Boolean

Returns true if passed the return value of a player_act call where the player attempted to flee and succeeded

Returns:

  • (Boolean)


217
218
219
220
221
222
# File 'lib/terminal_hero/modules/game_controller.rb', line 217

def self.fled_combat?(action_outcome)
  return action_outcome == {
    action: :player_flee,
    outcome: true
  }
end

.get_map_input(map, player) ⇒ Object

Get player input and call methods to process player and monster movement on the map



121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/terminal_hero/modules/game_controller.rb', line 121

def self.get_map_input(map, player)
  Interaction.new.loop do |key|
    prompt_quit(map, player) if GameData::EXIT_KEYS.include?(key.name.to_sym)
    next unless GameData::MOVE_KEYS.keys.include?(key.name.to_sym)

    tile = process_monster_movement(map, player)
    return [tile.event, [player, map, tile]] unless tile.nil? || tile.event.nil?

    tile = process_player_movement(map, player, key)
    return [tile.event, [player, map, tile]] unless tile.nil? || tile.event.nil?
  end
end

.init_player_and_map(player_data: {}, map_data: {}) ⇒ Object

Initialise Player or Map instances using given hashes of paramaters (or if none, default values). Return a hash containing those instances.



76
77
78
79
80
# File 'lib/terminal_hero/modules/game_controller.rb', line 76

def self.init_player_and_map(player_data: {}, map_data: {})
  player = Player.new(**player_data)
  map = Map.new(player: player, **map_data)
  { player: player, map: map }
end

.level_up(player) ⇒ Object

Level up the player, display level up message, and enter stat allocation menu



190
191
192
193
194
195
196
197
198
199
# File 'lib/terminal_hero/modules/game_controller.rb', line 190

def self.level_up(player)
  levels = player.level_up
  DisplayController.level_up(player, levels)
  player.allocate_stats(
    DisplayController.prompt_stat_allocation(
      starting_stats: player.stats,
      starting_points: GameData::STAT_POINTS_PER_LEVEL * levels
    )
  )
end

.load_game(character_name = nil) ⇒ Object

Prompt the user for a character name, and attempt to load a savegame file with that name



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/terminal_hero/modules/game_controller.rb', line 268

def self.load_game(character_name = nil)
  begin
    unless InputHandler.character_name_valid?(character_name)
      character_name = DisplayController.prompt_save_name(character_name)
    end
    # character_name will be false if input failed validation and user chose not to retry
    return :start_game if character_name == false
    path = File.join(File.dirname(__FILE__), "../saves")
    save_data = JSON.parse(File.read(File.join(path, "#{character_name.downcase}.json")), symbolize_names: true)
    player, map = init_player_and_map(
      **{ player_data: save_data[:player_data], map_data: save_data[:map_data] }
    ).values_at(:player, :map)
  # If load fails, let user choose to retry. When they choose not to, return to title menu.
  rescue Errno::ENOENT => e
    DisplayController.display_messages(GameData::MESSAGES[:no_save_file_error])
    return :start_game unless DisplayController.prompt_yes_no(GameData::PROMPTS[:re_load])

    character_name = nil
    retry
  rescue Errno::EACCES => e
    DisplayController.display_messages(GameData::MESSAGES[:general_error].call("Loading", e, Utils.log_error(e)))
    DisplayController.display_messages(GameData::MESSAGES[:load_permission_error])
    return :start_game unless DisplayController.prompt_yes_no(GameData::PROMPTS[:re_load])

    character_name = nil
    retry
  rescue JSON::ParserError => e
    DisplayController.display_messages(GameData::MESSAGES[:parse_error])
    # Don't display error msg directly as it contains the full JSON file content
    DisplayController.display_messages(GameData::MESSAGES[:error_hide_msg].call(Utils.log_error(e)))
    return :start_game unless DisplayController.prompt_yes_no(GameData::PROMPTS[:re_load])

    character_name = nil
    retry
  rescue StandardError => e
    DisplayController.display_messages(GameData::MESSAGES[:general_error].call("Loading", e, Utils.log_error(e)))
    return :start_game unless DisplayController.prompt_yes_no(GameData::PROMPTS[:re_load])

    character_name = nil
    retry
  end
  map_loop(map, player)
end

.map_loop(map, player) ⇒ Object

Calls methods to display map, listen for user input, and update map accordingly



146
147
148
149
150
151
152
153
154
# File 'lib/terminal_hero/modules/game_controller.rb', line 146

def self.map_loop(map, player)
  # Autosave whenever entering the map
  save_game(player, map)
  DisplayController.set_resize_hook(map, player)
  DisplayController.draw_map(map, player)
  event_and_params = get_map_input(map, player)
  DisplayController.cancel_resize_hook
  return event_and_params
end

.player_act(player, enemy) ⇒ Object

Get player input and process their chosen action for a single combat round.



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

def self.player_act(player, enemy)
  begin
    action = DisplayController.prompt_combat_action(player, enemy)
    # Raise a custom error if selected option does not exist
    raise NoFeatureError unless GameData::COMBAT_ACTIONS.keys.include?(action)
  rescue NoFeatureError => e
    DisplayController.display_messages([e.message])
    retry
  end
  outcome = GameData::COMBAT_ACTIONS[action].call(player, enemy)
  return { action: action, outcome: outcome }
end

.post_combat(player, enemy, map, outcome) ⇒ Object

Display appropriate messages and take other required actions based on the outcome of a combat encounters



203
204
205
206
207
208
209
210
211
212
213
# File 'lib/terminal_hero/modules/game_controller.rb', line 203

def self.post_combat(player, enemy, map, outcome)
  enemy.heal_hp(enemy.max_hp) if outcome == :defeat
  map.post_combat(player, enemy, outcome)
  xp = player.post_combat(outcome, enemy)
  DisplayController.post_combat(outcome, player, xp)
  # If player leveled up, apply and display the level gain and prompt user to allocate stat points
  level_up(player) if player.leveled_up?
  DisplayController.clear
  # Game state returns to the world map after combat
  return [:world_map, [map, player]]
end

.process_combat_turn(actor, player, enemy, map) ⇒ Object

Process a turn of combat for the participant whose turn it is, and check if combat has ended, returning the outcome if so



226
227
228
229
230
231
232
# File 'lib/terminal_hero/modules/game_controller.rb', line 226

def self.process_combat_turn(actor, player, enemy, map)
  action_outcome = actor == :player ? player_act(player, enemy) : enemy_act(player, enemy)
  DisplayController.clear
  DisplayController.display_messages(GameData::MESSAGES[:combat_status].call(player, enemy), pause: false)
  DisplayController.display_messages(GameData::MESSAGES[action_outcome[:action]].call(action_outcome[:outcome]))
  return check_combat_outcome(player, enemy, map, escaped: fled_combat?(action_outcome))
end

.process_monster_movement(map, player) ⇒ Object

Process monster movements and render the map



107
108
109
110
111
# File 'lib/terminal_hero/modules/game_controller.rb', line 107

def self.process_monster_movement(map, player)
  tile = map.move_monsters(player.coords)
  DisplayController.draw_map(map, player)
  return tile
end

.process_player_movement(map, player, key) ⇒ Object

Process player movement and render the map



114
115
116
117
118
# File 'lib/terminal_hero/modules/game_controller.rb', line 114

def self.process_player_movement(map, player, key)
  tile = map.process_movement(player, player.calc_destination(key.name.to_sym))
  DisplayController.draw_map(map, player)
  return tile
end

.prompt_quit(map, player) ⇒ Object



134
135
136
137
138
139
140
141
142
143
# File 'lib/terminal_hero/modules/game_controller.rb', line 134

def self.prompt_quit(map, player)
  DisplayController.clear
  quit = DisplayController.prompt_yes_no(GameData::PROMPTS[:save_and_exit])
  if quit
    save_game(player, map)
    exit_game
  else
    DisplayController.draw_map(map, player)
  end
end

.save_game(player, map) ⇒ Object

Save all data required to re-initialise the current game state to a file If save fails, display a message to the user but allow program to continue



252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/terminal_hero/modules/game_controller.rb', line 252

def self.save_game(player, map)
  save_data = { player_data: player.export, map_data: map.export }
  begin
    path = File.join(File.dirname(__FILE__), "../saves")
    Dir.mkdir(path) unless Dir.exist?(path)
    File.write(File.join(path, "#{player.name.downcase}.json"), JSON.dump(save_data))
  # If save fails, log and display the error, but let the application continue.
  rescue Errno::EACCES => e
    DisplayController.display_messages(GameData::MESSAGES[:general_error].call("Autosave", e, Utils.log_error(e)))
    DisplayController.display_messages(GameData::MESSAGES[:save_permission_error])
  rescue StandardError => e
    DisplayController.display_messages(GameData::MESSAGES[:general_error].call("Autosave", e, Utils.log_error(e)))
  end
end

.start_game(command_line_args) ⇒ Object

Display title menu, determine the next game state based on command line arguments or user input, and return a symbol representing the next game state



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/terminal_hero/modules/game_controller.rb', line 43

def self.start_game(command_line_args)
  next_state = InputHandler.process_command_line_args(command_line_args)
  if next_state == false
    begin
      next_state = DisplayController.prompt_title_menu
      # If selected option has no associated game state, raise a custom error and
      # re-prompt the user
      raise NoFeatureError unless GameData::GAME_STATES.keys.include?(next_state)
    rescue NoFeatureError => e
      DisplayController.display_messages([e.message])
      retry
    end
  else
    # Console is cleared when displaying title menu. If menu is skipped with command line args, clear it here instead.
    DisplayController.clear
  end
  return next_state
end

.tutorialObject

Ask the player if they want to view the tutorial, and if so, display it. Give player the option to replay tutorial multiple times. Return a symbol representing the next game state (character creation).



65
66
67
68
69
70
71
72
# File 'lib/terminal_hero/modules/game_controller.rb', line 65

def self.tutorial
  show_tutorial = DisplayController.prompt_tutorial
  while show_tutorial
    DisplayController.display_messages(GameData::MESSAGES[:tutorial].call)
    show_tutorial = DisplayController.prompt_tutorial(repeat: true)
  end
  return :character_creation
end