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
-
.character_creation ⇒ Object
Get user input to create a new character by choosing a name and allocating stats.
-
.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.
-
.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.
-
.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.
-
.enemy_act(player, enemy) ⇒ Object
Process one round of action by an enemy in combat.
-
.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).
-
.exit_game ⇒ Object
Display an exit message and exit the application.
-
.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.
-
.get_map_input(map, player) ⇒ Object
Get player input and call methods to process player and monster movement on the map.
-
.init_player_and_map(player_data: {}, map_data: {}) ⇒ Object
Initialise Player or Map instances using given hashes of paramaters (or if none, default values).
-
.level_up(player) ⇒ Object
Level up the player, display level up message, and enter stat allocation menu.
-
.load_game(character_name = nil) ⇒ Object
Prompt the user for a character name, and attempt to load a savegame file with that name.
-
.map_loop(map, player) ⇒ Object
Calls methods to display map, listen for user input, and update map accordingly.
-
.player_act(player, enemy) ⇒ Object
Get player input and process their chosen action for a single combat round.
-
.post_combat(player, enemy, map, outcome) ⇒ Object
Display appropriate messages and take other required actions based on the outcome of a combat encounters.
-
.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.
-
.process_monster_movement(map, player) ⇒ Object
Process monster movements and render the map.
-
.process_player_movement(map, player, key) ⇒ Object
Process player movement and render the map.
- .prompt_quit(map, player) ⇒ Object
-
.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.
-
.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.
-
.tutorial ⇒ Object
Ask the player if they want to view the tutorial, and if so, display it.
Class Method Details
.character_creation ⇒ Object
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.(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_game ⇒ Object
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.(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
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.(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.(GameData::MESSAGES[:general_error].call("Loading", e, Utils.log_error(e))) DisplayController.(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.(GameData::MESSAGES[:parse_error]) # Don't display error msg directly as it contains the full JSON file content DisplayController.(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.(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.([e.]) 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.(GameData::MESSAGES[:combat_status].call(player, enemy), pause: false) DisplayController.(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.(GameData::MESSAGES[:general_error].call("Autosave", e, Utils.log_error(e))) DisplayController.(GameData::MESSAGES[:save_permission_error]) rescue StandardError => e DisplayController.(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. # 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.([e.]) 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 |
.tutorial ⇒ Object
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.(GameData::MESSAGES[:tutorial].call) show_tutorial = DisplayController.prompt_tutorial(repeat: true) end return :character_creation end |