terminal_hero 0.1.0

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.
@@ -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