terminal_hero 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,311 @@
1
+ begin
2
+ require "remedy"
3
+ require "json"
4
+ rescue LoadError => e
5
+ # Display load errors using puts (not calling external methods which may not have been loaded)
6
+ puts "It appears that a dependency was unable to be loaded: "
7
+ puts e.message
8
+ puts "Please try installing dependencies mannually by running the command "\
9
+ "\"bundle install\" from within the installation directory."
10
+ puts "If you installed this application as a gem, you could try reinstalling it by "\
11
+ "running \"gem uninstall terminal_hero\" followed by \"gem install terminal_hero\""
12
+ exit
13
+ end
14
+
15
+ require_relative "game_data"
16
+ require_relative "input_handler"
17
+ require_relative "display_controller"
18
+ require_relative "../classes/player"
19
+ require_relative "../classes/map"
20
+ require_relative "../classes/errors/no_feature_error"
21
+
22
+ # Handles game loops and interactions between main objects
23
+ module GameController
24
+ include Remedy
25
+
26
+ # MAIN GAME STATES
27
+
28
+ # Given a symbol corresponding to a key in the GAME_STATES hash (and optionally
29
+ # an array of parameters), calls a lambda triggering the method for that game
30
+ # state (which then returns the next game state + parameters).
31
+ def self.enter(game_state, params = nil)
32
+ GameData::GAME_STATES[game_state].call(self, params)
33
+ end
34
+
35
+ # Display an exit message and exit the application
36
+ def self.exit_game
37
+ DisplayController.display_messages(GameData::MESSAGES[:exit_game])
38
+ exit
39
+ end
40
+
41
+ # Display title menu, determine the next game state based on command line
42
+ # arguments or user input, and return a symbol representing the next game state
43
+ def self.start_game(command_line_args)
44
+ next_state = InputHandler.process_command_line_args(command_line_args)
45
+ if next_state == false
46
+ begin
47
+ next_state = DisplayController.prompt_title_menu
48
+ # If selected option has no associated game state, raise a custom error and
49
+ # re-prompt the user
50
+ raise NoFeatureError unless GameData::GAME_STATES.keys.include?(next_state)
51
+ rescue NoFeatureError => e
52
+ DisplayController.display_messages([e.message])
53
+ retry
54
+ end
55
+ else
56
+ # Console is cleared when displaying title menu. If menu is skipped with command line args, clear it here instead.
57
+ DisplayController.clear
58
+ end
59
+ return next_state
60
+ end
61
+
62
+ # Ask the player if they want to view the tutorial, and if so, display it.
63
+ # Give player the option to replay tutorial multiple times.
64
+ # Return a symbol representing the next game state (character creation).
65
+ def self.tutorial
66
+ show_tutorial = DisplayController.prompt_tutorial
67
+ while show_tutorial
68
+ DisplayController.display_messages(GameData::MESSAGES[:tutorial].call)
69
+ show_tutorial = DisplayController.prompt_tutorial(repeat: true)
70
+ end
71
+ return :character_creation
72
+ end
73
+
74
+ # Initialise Player or Map instances using given hashes of paramaters
75
+ # (or if none, default values). Return a hash containing those instances.
76
+ def self.init_player_and_map(player_data: {}, map_data: {})
77
+ player = Player.new(**player_data)
78
+ map = Map.new(player: player, **map_data)
79
+ { player: player, map: map }
80
+ end
81
+
82
+ # Get user input to create a new character by choosing a name and
83
+ # allocating stats.
84
+ def self.character_creation
85
+ # Prompt, then reprompt unless and until save name is not already taken or user confirms overwrite
86
+ name = DisplayController.prompt_character_name
87
+ name = DisplayController.prompt_character_name until confirm_save(name)
88
+ stats = DisplayController.prompt_stat_allocation
89
+ player, map = init_player_and_map(player_data: { name: name, stats: stats }).values_at(:player, :map)
90
+ return [:world_map, [map, player]]
91
+ end
92
+
93
+ # If the user attempts to create a character with the same name as an existing
94
+ # save file, confirm whether they want to override it
95
+ def self.confirm_save(name)
96
+ path = File.join(File.dirname(__FILE__), "../saves")
97
+ if File.exist?(File.join(path, "#{name.downcase}.json"))
98
+ return DisplayController.prompt_yes_no(GameData::PROMPTS[:overwrite_save].call(name), default_no: true)
99
+ end
100
+
101
+ return true
102
+ end
103
+
104
+ # MAP MOVEMENT
105
+
106
+ # Process monster movements and render the map
107
+ def self.process_monster_movement(map, player)
108
+ tile = map.move_monsters(player.coords)
109
+ DisplayController.draw_map(map, player)
110
+ return tile
111
+ end
112
+
113
+ # Process player movement and render the map
114
+ def self.process_player_movement(map, player, key)
115
+ tile = map.process_movement(player, player.calc_destination(key.name.to_sym))
116
+ DisplayController.draw_map(map, player)
117
+ return tile
118
+ end
119
+
120
+ # Get player input and call methods to process player and monster movement on the map
121
+ def self.get_map_input(map, player)
122
+ Interaction.new.loop do |key|
123
+ prompt_quit(map, player) if GameData::EXIT_KEYS.include?(key.name.to_sym)
124
+ next unless GameData::MOVE_KEYS.keys.include?(key.name.to_sym)
125
+
126
+ tile = process_monster_movement(map, player)
127
+ return [tile.event, [player, map, tile]] unless tile.nil? || tile.event.nil?
128
+
129
+ tile = process_player_movement(map, player, key)
130
+ return [tile.event, [player, map, tile]] unless tile.nil? || tile.event.nil?
131
+ end
132
+ end
133
+
134
+ def self.prompt_quit(map, player)
135
+ DisplayController.clear
136
+ quit = DisplayController.prompt_yes_no(GameData::PROMPTS[:save_and_exit])
137
+ if quit
138
+ save_game(player, map)
139
+ exit_game
140
+ else
141
+ DisplayController.draw_map(map, player)
142
+ end
143
+ end
144
+
145
+ # Calls methods to display map, listen for user input, and update map accordingly
146
+ def self.map_loop(map, player)
147
+ # Autosave whenever entering the map
148
+ save_game(player, map)
149
+ DisplayController.set_resize_hook(map, player)
150
+ DisplayController.draw_map(map, player)
151
+ event_and_params = get_map_input(map, player)
152
+ DisplayController.cancel_resize_hook
153
+ return event_and_params
154
+ end
155
+
156
+ # COMBAT
157
+
158
+ # Get player input and process their chosen action for a single combat round.
159
+ def self.player_act(player, enemy)
160
+ begin
161
+ action = DisplayController.prompt_combat_action(player, enemy)
162
+ # Raise a custom error if selected option does not exist
163
+ raise NoFeatureError unless GameData::COMBAT_ACTIONS.keys.include?(action)
164
+ rescue NoFeatureError => e
165
+ DisplayController.display_messages([e.message])
166
+ retry
167
+ end
168
+ outcome = GameData::COMBAT_ACTIONS[action].call(player, enemy)
169
+ return { action: action, outcome: outcome }
170
+ end
171
+
172
+ # Process one round of action by an enemy in combat.
173
+ def self.enemy_act(player, enemy)
174
+ action = :enemy_attack
175
+ outcome = GameData::COMBAT_ACTIONS[action].call(player, enemy)
176
+ return { action: action, outcome: outcome }
177
+ end
178
+
179
+ # Return the outcome of a combat encounter along with parameters to pass to the
180
+ # next game state, or return false if combat has not ended
181
+ def self.check_combat_outcome(player, enemy, map, escaped: false)
182
+ return [:post_combat, [player, enemy, map, :defeat]] if player.dead?
183
+ return [:post_combat, [player, enemy, map, :victory]] if enemy.dead?
184
+ return [:post_combat, [player, enemy, map, :escaped]] if escaped
185
+
186
+ return false
187
+ end
188
+
189
+ # Level up the player, display level up message, and enter stat allocation menu
190
+ def self.level_up(player)
191
+ levels = player.level_up
192
+ DisplayController.level_up(player, levels)
193
+ player.allocate_stats(
194
+ DisplayController.prompt_stat_allocation(
195
+ starting_stats: player.stats,
196
+ starting_points: GameData::STAT_POINTS_PER_LEVEL * levels
197
+ )
198
+ )
199
+ end
200
+
201
+ # Display appropriate messages and take other required actions based on
202
+ # the outcome of a combat encounters
203
+ def self.post_combat(player, enemy, map, outcome)
204
+ enemy.heal_hp(enemy.max_hp) if outcome == :defeat
205
+ map.post_combat(player, enemy, outcome)
206
+ xp = player.post_combat(outcome, enemy)
207
+ DisplayController.post_combat(outcome, player, xp)
208
+ # If player leveled up, apply and display the level gain and prompt user to allocate stat points
209
+ level_up(player) if player.leveled_up?
210
+ DisplayController.clear
211
+ # Game state returns to the world map after combat
212
+ return [:world_map, [map, player]]
213
+ end
214
+
215
+ # Returns true if passed the return value of a player_act call where
216
+ # the player attempted to flee and succeeded
217
+ def self.fled_combat?(action_outcome)
218
+ return action_outcome == {
219
+ action: :player_flee,
220
+ outcome: true
221
+ }
222
+ end
223
+
224
+ # Process a turn of combat for the participant whose turn it is, and check if
225
+ # combat has ended, returning the outcome if so
226
+ def self.process_combat_turn(actor, player, enemy, map)
227
+ action_outcome = actor == :player ? player_act(player, enemy) : enemy_act(player, enemy)
228
+ DisplayController.clear
229
+ DisplayController.display_messages(GameData::MESSAGES[:combat_status].call(player, enemy), pause: false)
230
+ DisplayController.display_messages(GameData::MESSAGES[action_outcome[:action]].call(action_outcome[:outcome]))
231
+ return check_combat_outcome(player, enemy, map, escaped: fled_combat?(action_outcome))
232
+ end
233
+
234
+ # Manages a combat encounter by calling methods to get and process participant
235
+ # actions each round, determine when combat has ended, and return the outcome
236
+ def self.combat_loop(player, map, tile, enemy = tile.entity)
237
+ DisplayController.clear
238
+ DisplayController.display_messages(GameData::MESSAGES[:enter_combat].call(enemy))
239
+ actor = :player
240
+ loop do
241
+ combat_outcome = process_combat_turn(actor, player, enemy, map)
242
+ return combat_outcome unless combat_outcome == false
243
+
244
+ actor = actor == :enemy ? :player : :enemy
245
+ end
246
+ end
247
+
248
+ # SAVING AND LOADING
249
+
250
+ # Save all data required to re-initialise the current game state to a file
251
+ # If save fails, display a message to the user but allow program to continue
252
+ def self.save_game(player, map)
253
+ save_data = { player_data: player.export, map_data: map.export }
254
+ begin
255
+ path = File.join(File.dirname(__FILE__), "../saves")
256
+ Dir.mkdir(path) unless Dir.exist?(path)
257
+ File.write(File.join(path, "#{player.name.downcase}.json"), JSON.dump(save_data))
258
+ # If save fails, log and display the error, but let the application continue.
259
+ rescue Errno::EACCES => e
260
+ DisplayController.display_messages(GameData::MESSAGES[:general_error].call("Autosave", e, Utils.log_error(e)))
261
+ DisplayController.display_messages(GameData::MESSAGES[:save_permission_error])
262
+ rescue StandardError => e
263
+ DisplayController.display_messages(GameData::MESSAGES[:general_error].call("Autosave", e, Utils.log_error(e)))
264
+ end
265
+ end
266
+
267
+ # Prompt the user for a character name, and attempt to load a savegame file with that name
268
+ def self.load_game(character_name = nil)
269
+ begin
270
+ unless InputHandler.character_name_valid?(character_name)
271
+ character_name = DisplayController.prompt_save_name(character_name)
272
+ end
273
+ # character_name will be false if input failed validation and user chose not to retry
274
+ return :start_game if character_name == false
275
+ path = File.join(File.dirname(__FILE__), "../saves")
276
+ save_data = JSON.parse(File.read(File.join(path, "#{character_name.downcase}.json")), symbolize_names: true)
277
+ player, map = init_player_and_map(
278
+ **{ player_data: save_data[:player_data], map_data: save_data[:map_data] }
279
+ ).values_at(:player, :map)
280
+ # If load fails, let user choose to retry. When they choose not to, return to title menu.
281
+ rescue Errno::ENOENT => e
282
+ DisplayController.display_messages(GameData::MESSAGES[:no_save_file_error])
283
+ return :start_game unless DisplayController.prompt_yes_no(GameData::PROMPTS[:re_load])
284
+
285
+ character_name = nil
286
+ retry
287
+ rescue Errno::EACCES => e
288
+ DisplayController.display_messages(GameData::MESSAGES[:general_error].call("Loading", e, Utils.log_error(e)))
289
+ DisplayController.display_messages(GameData::MESSAGES[:load_permission_error])
290
+ return :start_game unless DisplayController.prompt_yes_no(GameData::PROMPTS[:re_load])
291
+
292
+ character_name = nil
293
+ retry
294
+ rescue JSON::ParserError => e
295
+ DisplayController.display_messages(GameData::MESSAGES[:parse_error])
296
+ # Don't display error msg directly as it contains the full JSON file content
297
+ DisplayController.display_messages(GameData::MESSAGES[:error_hide_msg].call(Utils.log_error(e)))
298
+ return :start_game unless DisplayController.prompt_yes_no(GameData::PROMPTS[:re_load])
299
+
300
+ character_name = nil
301
+ retry
302
+ rescue StandardError => e
303
+ DisplayController.display_messages(GameData::MESSAGES[:general_error].call("Loading", e, Utils.log_error(e)))
304
+ return :start_game unless DisplayController.prompt_yes_no(GameData::PROMPTS[:re_load])
305
+
306
+ character_name = nil
307
+ retry
308
+ end
309
+ map_loop(map, player)
310
+ end
311
+ end
@@ -0,0 +1,331 @@
1
+ begin
2
+ require "colorize"
3
+ rescue LoadError => e
4
+ # Display load errors using puts (not calling external methods which may not have been loaded)
5
+ puts "It appears that a dependency was unable to be loaded: "
6
+ puts e.message
7
+ puts "Please try installing dependencies mannually by running the command "\
8
+ "\"bundle install\" from within the installation directory."
9
+ puts "If you installed this application as a gem, you could try reinstalling it by "\
10
+ "running \"gem uninstall terminal_hero\" followed by \"gem install terminal_hero\""
11
+ exit
12
+ end
13
+
14
+ # Stores game content and parameters as constants. Keeps data separate from
15
+ # logic, so that content or parameters can be added or adjusted without changing
16
+ # the substantive code.
17
+ module GameData
18
+ # World map dimensions
19
+ MAP_WIDTH = 50
20
+ MAP_HEIGHT = 50
21
+
22
+ # Maximum map render distance (field of view)
23
+ MAX_H_VIEW_DIST = 25
24
+ MAX_V_VIEW_DIST = 25
25
+
26
+ # Constants for calculating XP to next level
27
+ LEVELING_CONSTANT = 10
28
+ LEVELING_EXPONENT = 1.6
29
+
30
+ # Stat points awarded at character creation and on level up
31
+ STAT_POINTS_PER_LEVEL = 5
32
+
33
+ # Maximum variance of monster levels from player level
34
+ MONSTER_LEVEL_VARIANCE = 1
35
+
36
+ # Multiplier for XP lost (per player level) as compared to XP gained (when defeating a monster of the same level)
37
+ XP_LOSS_MULTIPLIER = 0.5
38
+
39
+ # Default stats for any creature
40
+ DEFAULT_STATS = {
41
+ atk: {
42
+ value: 5, name: "Attack"
43
+ },
44
+ dfc: {
45
+ value: 5, name: "Defence"
46
+ },
47
+ con: {
48
+ value: 5, name: "Constitution"
49
+ }
50
+ }.freeze
51
+
52
+ # Max HP of a creature is equal to its constitution times this multiplier
53
+ CON_TO_HP = 10
54
+
55
+ # Player default starting coords
56
+ DEFAULT_COORDS = { x: 2, y: 2 }.freeze
57
+
58
+ # World map tile data
59
+ MAP_TILES = {
60
+ player: {
61
+ symbol: "@",
62
+ blocking: true,
63
+ color: :blue
64
+ },
65
+ forest: {
66
+ symbol: "T",
67
+ color: :green
68
+ },
69
+ mountain: {
70
+ symbol: "M",
71
+ color: :light_black
72
+ },
73
+ plain: {
74
+ symbol: "P",
75
+ color: :light_yellow
76
+ },
77
+ edge: {
78
+ symbol: "|",
79
+ color: :default,
80
+ blocking: true
81
+ },
82
+ monster: {
83
+ symbol: "%",
84
+ color: :red,
85
+ blocking: true,
86
+ event: :combat
87
+ }
88
+ }.freeze
89
+
90
+ TILE_DESCRIPTIONS = {
91
+ player: "You, the player.",
92
+ forest: "A forest of towering trees.",
93
+ mountain: "Rugged, mountainous terrain.",
94
+ plain: "Vast, empty plains.",
95
+ edge: "An impassable wall.",
96
+ monster: "A terrifying monster. You should fight it!"
97
+ }.freeze
98
+
99
+ MAP_HEADER = lambda { |player|
100
+ [
101
+ player.name.upcase.colorize(:light_yellow),
102
+ "HEALTH: ".colorize(:light_white) +
103
+ "#{player.current_hp}/#{player.max_hp}".colorize(player.health_color),
104
+ "ATK: ".colorize(:light_white) +
105
+ player.stats[:atk][:value].to_s.colorize(:green) +
106
+ " DEF: ".colorize(:light_white) +
107
+ player.stats[:dfc][:value].to_s.colorize(:green) +
108
+ " CON: ".colorize(:light_white) +
109
+ player.stats[:con][:value].to_s.colorize(:green),
110
+ "LEVEL: ".colorize(:light_white) +
111
+ player.level.to_s.colorize(:green) +
112
+ " XP: ".colorize(:light_white) +
113
+ player.xp_progress.to_s.colorize(:green),
114
+ " "
115
+ ]
116
+ }
117
+
118
+ # Keypress inputs for movement, and their associated coord changes
119
+ MOVE_KEYS = {
120
+ left: { x: -1, y: 0 },
121
+ a: { x: -1, y: 0 },
122
+ right: { x: 1, y: 0 },
123
+ d: { x: 1, y: 0 },
124
+ up: { x: 0, y: -1 },
125
+ w: { x: 0, y: -1 },
126
+ down: { x: 0, y: 1 },
127
+ s: { x: 0, y: 1 },
128
+ }.freeze
129
+
130
+ # Keypress inputs that will attempt to save and then exit the game from the map screen
131
+ # control_left_square_bracket is equivalent to the escape key
132
+ EXIT_KEYS = [:q, :control_left_square_bracket].freeze
133
+
134
+ GAME_STATES = {
135
+ start_game: ->(game_controller, _params) { game_controller.start_game([]) },
136
+ new_game: ->(game_controller, _params) { game_controller.tutorial },
137
+ load_game: ->(game_controller, params) { game_controller.load_game(params) },
138
+ exit_game: ->(game_controller, _params) { game_controller.exit_game },
139
+ character_creation: ->(game_controller, _params) { game_controller.character_creation },
140
+ world_map: ->(game_controller, params) { game_controller.map_loop(*params) },
141
+ combat: ->(game_controller, params) { game_controller.combat_loop(*params) },
142
+ post_combat: ->(game_controller, params) { game_controller.post_combat(*params) }
143
+ }.freeze
144
+
145
+ # Actions that may be taken in combat, and their associated callbacks
146
+ COMBAT_ACTIONS = {
147
+ player_attack: ->(player, enemy) { enemy.receive_damage(player.calc_damage_dealt) },
148
+
149
+ player_flee: ->(player, enemy) { player.flee(enemy) },
150
+
151
+ enemy_attack: ->(player, enemy) { player.receive_damage(enemy.calc_damage_dealt) }
152
+ }.freeze
153
+
154
+ # Title menu options and their return values
155
+ # Strings used as keys to match tty-prompt requirements
156
+ TITLE_MENU_OPTIONS = {
157
+ "New Game" => :new_game,
158
+ "Load Game" => :load_game,
159
+ "Exit" => :exit_game
160
+ }.freeze
161
+
162
+ # Combat menu options and their return values
163
+ COMBAT_MENU_OPTIONS = {
164
+ "Attack" => :player_attack,
165
+ "Flee" => :player_flee
166
+ }.freeze
167
+
168
+ COMMAND_LINE_ARGUMENTS = {
169
+ new_game: ["-n", "--new"],
170
+ load_game: ["-l", "--load"]
171
+ }.freeze
172
+
173
+ # Validation requirements to display to the user
174
+ VALIDATION_REQUIREMENTS = {
175
+ character_name: "Names must contain only letters, numbers and underscores, be 3 to 15 characters in length"\
176
+ ", and not contain spaces."
177
+ }.freeze
178
+
179
+ PROMPTS = {
180
+ re_load: "Would you like to try loading again?",
181
+ overwrite_save: lambda { |name|
182
+ "It looks like a saved game already exists for that character name. Are you "\
183
+ "sure you want to overwrite the save file for #{name.upcase.colorize(:light_yellow)}?" },
184
+ save_and_exit: "Would you like to exit the game? Your progress will be saved."
185
+ }.freeze
186
+
187
+ ASCII_ART = {
188
+ title: lambda { |font, console_size|
189
+ # Pad based on console and text size. Ascii text is centered horizontally,
190
+ # and about ~1/5 of the way down the screen vertically (leaving room for content below).
191
+ top_pad = [(console_size.rows / 5), 0].max
192
+ left_pad = [(console_size.cols / 2) - 22, 0].max
193
+ return "#{"\n"*top_pad}#{font.write('Terminal'.rjust(left_pad))}\n"\
194
+ "#{font.write('Hero'.rjust(10 + left_pad))}\n".colorize(:light_yellow)
195
+ },
196
+ level_up: lambda { |font, console_size|
197
+ top_pad = [(console_size.rows / 5), 0].max
198
+ left_pad = [(console_size.cols / 2) - 12, 0].max
199
+ "#{"\n"*top_pad}#{font.write('Level'.rjust(left_pad))}\n#{font.write('Up'.rjust(8 + left_pad))}\n".colorize(:light_yellow)
200
+ }
201
+ }
202
+
203
+ # Arrays of strings to be displayed in turn by the display controller
204
+ # (or callbacks generating such arrays)
205
+ MESSAGES = {
206
+ not_implemented: ["Sorry, it looks like you're trying to access a feature that hasn't been implemented yet."\
207
+ "Try choosing something else!"],
208
+
209
+ tutorial: lambda {
210
+ msgs = []
211
+ msgs.push "Welcome to Terminal Hero! In a moment, you will be prompted to create a character, but first,"\
212
+ "let's go over how things work."
213
+ msgs.push "When you enter the game, you will be presented with a map made up of the following symbols:"
214
+ MAP_TILES.each do |key, tile|
215
+ msgs.push " #{tile[:symbol]} : #{TILE_DESCRIPTIONS[key]}".colorize(tile[:color])
216
+ end
217
+ msgs.push "You can move your character around the map using the arrow keys or \"WASD\" controls."
218
+ msgs.push "It's a good idea to expand your terminal to full-screen, so that you can see further on the map."
219
+ msgs.push "If you run into a monster, you will enter combat."
220
+ msgs.push "In combat, you and the enemy will take turns to act."\
221
+ "You will select your action each round from a list of options."
222
+ msgs.push "Combat continues until you or the enemy loses all their hit points (HP), or you flee the battle."
223
+ msgs.push "When you defeat an enemy, you will gain experience points (XP). When you lose, you will lose some XP"\
224
+ "(but you won't lose levels). You will then be revived with full HP."
225
+ msgs.push "When you gain enough XP, you will level up."
226
+ msgs.push "Leveling up awards stat points, which you can expend to increase your combat statistics. These are:"
227
+ msgs.push "#{'Attack'.colorize(:red)}: With higher attack, you will deal more damage in combat."
228
+ msgs.push "#{'Defence'.colorize(:blue)}: With higher defence, you will receive less damage in combat."
229
+ msgs.push "#{'Constitution'.colorize(:green)}: Determines your maximum HP."
230
+ msgs.push "You can see your current level, HP and stats above the map at any time."
231
+ msgs.push "The game will automatically save after every battle."
232
+ msgs.push "To load a saved game, select \"load\" from the title menu and enter the name of a character with "\
233
+ "an existing save file when prompted."
234
+ msgs.push "Alternatively, you can pass the arguments \"-l\" or \"--load\" followed by a saved character's name"\
235
+ "when running the game from the command line."
236
+ msgs.push "You can also pass the arguments \"-n\" or \"--new\" to skip the title menu and "\
237
+ "jump straight into a new game."
238
+ msgs.push "You can press \"escape\" or \"q\" on the map screen at any time to save and exit the game."
239
+ msgs.push "That's all there is to it. Have fun!"
240
+ return msgs
241
+ },
242
+
243
+ enter_combat: ->(enemy) { ["You encountered a level #{enemy.level} #{enemy.name}!"] },
244
+
245
+ combat_status: lambda { |player, enemy|
246
+ [
247
+ "Your health: #{"#{player.current_hp} / #{player.max_hp}".colorize(player.health_color)} | "\
248
+ "Enemy health: #{"#{enemy.current_hp} / #{enemy.max_hp}".colorize(enemy.health_color)}"
249
+ ]
250
+ },
251
+
252
+ player_attack: lambda { |damage|
253
+ ["You attacked the enemy, dealing #{damage} damage!\n"]
254
+ },
255
+
256
+ enemy_attack: lambda { |damage|
257
+ ["The enemy attacked you, dealing #{damage} damage!\n"]
258
+ },
259
+
260
+ player_flee: lambda { |success|
261
+ msgs = ["You attempt to flee..."]
262
+ msgs.push("You couldn't get away!") unless success
263
+ return msgs
264
+ },
265
+
266
+ combat_victory: lambda { |xp|
267
+ msgs = ["You defeated the enemy!"]
268
+ msgs.push xp == 1 ? "You received #{xp} experience point!" : "You received #{xp} experience points!"
269
+ return msgs
270
+ },
271
+
272
+ leveled_up: lambda { |player, levels|
273
+ msgs = levels == 1 ? ["You gained #{levels} level!"] : ["You gained #{levels} levels!"]
274
+ msgs[0] += " You are now level #{player.level}!"
275
+ return msgs
276
+ },
277
+
278
+ level_progress: ->(player) { ["XP to next level: #{player.xp_progress}"] },
279
+
280
+ combat_defeat: ->(xp) { ["You were defeated! You lost #{xp} XP."] },
281
+
282
+ combat_escaped: ["You got away!"],
283
+
284
+ exit_game: ["Thanks for playing! See you next time."],
285
+
286
+ general_error: ->(action, e, file_path, msg: "#{action} failed: an error occurred.") {
287
+ [
288
+ msg.colorize(:red),
289
+ " \"#{e.message}.\"".colorize(:yellow),
290
+ "Details of the error have been logged to \"#{file_path.colorize(:light_blue)}.\" "\
291
+ "If you would like to submit a bug report, please include a copy of this file."
292
+ ]
293
+ },
294
+
295
+ out_of_bounds_error: "You have attempted to move outside the game map. If you loaded the game,"\
296
+ "your save file may have been modified or corrupted.".colorize(:red),
297
+
298
+ error_hide_msg: ->(file_path) {
299
+ [
300
+ "Details of the error have been logged to \"#{file_path.colorize(:light_blue)}.\" "\
301
+ "If you would like to submit a bug report, please include a copy of this file."
302
+ ]
303
+ },
304
+
305
+ save_permission_error: [
306
+ "To enable saving, please ensure that the current user has "\
307
+ "write access to the directory where the game has been installed"\
308
+ "and to files in the \"termina_hero/saves\" subfolder."
309
+ ],
310
+
311
+ no_save_file_error: [
312
+ "No save was file found for that character. Input "\
313
+ "must match the character's name exactly (but is not case sensitive).".colorize(:red)
314
+ ],
315
+
316
+ load_permission_error: [
317
+ "To enable loading, please ensure that the current user has "\
318
+ "read access to files in the \"terminal_hero/saves\" subfolder."
319
+ ],
320
+
321
+ parse_error: [
322
+ "The save file you are trying to load could not be parsed. It may have been modified or corrupted.".colorize(:red)
323
+ ],
324
+
325
+ dependency_load_error: [
326
+ "It appears that a dependency was not able to be loaded.",
327
+ "Please try installing dependencies mannually by running the command \"bundle install\" from within the installation directory.",
328
+ "If you installed this application as a gem, you could try reinstalling it by running \"gem uninstall terminal_hero\" followed by \"gem install terminal_hero\""
329
+ ]
330
+ }.freeze
331
+ end