zarta 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/zarta +3 -0
- data/lib/zarta/dungeon.rb +134 -0
- data/lib/zarta/enemy.rb +125 -0
- data/lib/zarta/enemy.yml +289 -0
- data/lib/zarta/main.rb +214 -0
- data/lib/zarta/player.rb +188 -0
- data/lib/zarta/rooms.yml +368 -0
- data/lib/zarta/weapon.rb +67 -0
- data/lib/zarta/weapons.yml +300 -0
- data/lib/zarta.rb +8 -0
- data/tests/test_zarta.rb +93 -0
- metadata +141 -0
data/lib/zarta/main.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'tty'
|
2
|
+
require 'terminal-table'
|
3
|
+
require 'pastel'
|
4
|
+
require 'artii'
|
5
|
+
|
6
|
+
BOSS_RARITY = 5
|
7
|
+
ENEMY_CHANCE_BASE = 40
|
8
|
+
ENEMY_CHANCE_MOD = 5
|
9
|
+
ENEMY_LEVEL_MAX_MOD = 3
|
10
|
+
ENEMY_LEVEL_MIN_MOD = 2
|
11
|
+
ENEMY_MAX_HEALTH_MOD = 2
|
12
|
+
FLEE_CHANCE = 0.5
|
13
|
+
HEALTH_INCREASE = 3
|
14
|
+
MAX_NEXT_ROOMS = 4
|
15
|
+
MIN_NEXT_ROOMS = 2
|
16
|
+
NEXT_LEVEL_XP = 10
|
17
|
+
SPAWN_CHANCE_MOD = 1.2
|
18
|
+
STAIRS_CHANCE = 5
|
19
|
+
WEAPON_CHANCE_BASE = 5
|
20
|
+
WEAPON_CHANCE_MOD = 5
|
21
|
+
WEAPON_MAX_MOD = 2
|
22
|
+
WEAPON_MIN_MOD = 0
|
23
|
+
|
24
|
+
# The catch-all module for ZARTA
|
25
|
+
module Zarta
|
26
|
+
# Runs the game
|
27
|
+
class Engine
|
28
|
+
def initialize
|
29
|
+
@dungeon = Zarta::Dungeon.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# Main game loop
|
33
|
+
def play
|
34
|
+
Zarta::StartScreen.new(@dungeon)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class StartScreen
|
39
|
+
def initialize(dungeon)
|
40
|
+
@dungeon = dungeon
|
41
|
+
@prompt = TTY::Prompt.new
|
42
|
+
@pastel = Pastel.new
|
43
|
+
show_splash
|
44
|
+
end
|
45
|
+
|
46
|
+
def show_splash
|
47
|
+
loop do
|
48
|
+
system 'clear'
|
49
|
+
a = Artii::Base.new font: 'isometric3'
|
50
|
+
title = a.asciify('Zarta')
|
51
|
+
puts @pastel.red.bold(title)
|
52
|
+
splash_option = @prompt.select('') do |menu|
|
53
|
+
menu.choice 'New Game'
|
54
|
+
menu.choice 'Load Game'
|
55
|
+
menu.choice 'Leaderboards'
|
56
|
+
menu.choice 'Quit'
|
57
|
+
end
|
58
|
+
|
59
|
+
new_game if splash_option == 'New Game'
|
60
|
+
load_game if splash_option == 'Load Game'
|
61
|
+
leaderboard if splash_option == 'Leaderboards'
|
62
|
+
exit[0] if splash_option == 'Quit' && @prompt.yes?('Are you sure?')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def new_game
|
67
|
+
player_name = @prompt.ask("What's your name, adventurer?", required: true)
|
68
|
+
@dungeon.player.name = player_name
|
69
|
+
Zarta::Screen.new(@dungeon)
|
70
|
+
end
|
71
|
+
|
72
|
+
def load_game
|
73
|
+
puts 'Load Game not implemented yet...'
|
74
|
+
gets
|
75
|
+
end
|
76
|
+
|
77
|
+
def leaderboard
|
78
|
+
puts 'Leaderboard not implemented yet...'
|
79
|
+
gets
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Writes the current game screen. No idea why I didn't just do all this stuff
|
84
|
+
# in the Engine class. Look how empty that thing is. You probably didn't even
|
85
|
+
# see it up there.
|
86
|
+
class Screen
|
87
|
+
def initialize(dungeon)
|
88
|
+
@dungeon = dungeon
|
89
|
+
@player = @dungeon.player
|
90
|
+
@room = @dungeon.room
|
91
|
+
@prompt = TTY::Prompt.new
|
92
|
+
|
93
|
+
refresh
|
94
|
+
end
|
95
|
+
|
96
|
+
# When the player moves into a new room, this function checks if anything
|
97
|
+
# has spawned in it and calls the appropriate functions. Looking at it now,
|
98
|
+
# it seems that all of this fucnctionality could be off-handed to the room
|
99
|
+
# class. I'm just creating HUD objects in any function where I need to
|
100
|
+
# refresh it anyway. Feels redundant.
|
101
|
+
def refresh
|
102
|
+
loop do
|
103
|
+
Zarta::HUD.new(@dungeon)
|
104
|
+
@player.handle_enemy if @dungeon.room.enemy.is_a?(Zarta::Enemy)
|
105
|
+
@player.handle_weapon if @dungeon.room.weapon.is_a?(Zarta::Weapon)
|
106
|
+
handle_stairs if @dungeon.room.stairs
|
107
|
+
|
108
|
+
@dungeon.room = next_rooms_prompt
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Another function that could be handed off to the Room class. Like that
|
113
|
+
# whale of a beast needs more methods. Maybe it would make more sense to
|
114
|
+
# move some functionality into the Engine class.
|
115
|
+
def handle_stairs
|
116
|
+
Zarta::HUD.new(@dungeon)
|
117
|
+
puts 'You see stairs leading down here.'
|
118
|
+
return unless @prompt.yes?('Go down?')
|
119
|
+
@dungeon.level += 1
|
120
|
+
@dungeon.room = Zarta::Room.new(@dungeon)
|
121
|
+
refresh
|
122
|
+
end
|
123
|
+
|
124
|
+
# Not one to be left out, this guy could also piss off somewhere else. Seems
|
125
|
+
# like this class is doomed. When I can get round to it, that is. So maybe
|
126
|
+
# it will be ok...
|
127
|
+
def next_rooms_prompt
|
128
|
+
next_rooms_options = []
|
129
|
+
@dungeon.room.new_rooms
|
130
|
+
@dungeon.room.next_rooms.each do |room|
|
131
|
+
next_rooms_options << room.description
|
132
|
+
end
|
133
|
+
|
134
|
+
Zarta::HUD.new(@dungeon)
|
135
|
+
puts 'You see these rooms ahead of you,'
|
136
|
+
next_room_choice = @prompt.select('Choose one:', next_rooms_options)
|
137
|
+
|
138
|
+
@dungeon.room.next_rooms.each do |room|
|
139
|
+
return room if next_room_choice == room.description
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Resfreshes the top of the screen when I need it to. I spawn instances of
|
145
|
+
# this guy a lot.
|
146
|
+
class HUD
|
147
|
+
def initialize(dungeon)
|
148
|
+
@dungeon = dungeon
|
149
|
+
@player = @dungeon.player
|
150
|
+
@pastel = Pastel.new
|
151
|
+
|
152
|
+
hud_table
|
153
|
+
end
|
154
|
+
|
155
|
+
# Lays out the top of the screen in a nice way for me. It would be great if
|
156
|
+
# I could get the whole game to display in a bordered table, but alas, the
|
157
|
+
# gem I'm using can't do it, and I appear to lack the technichal knowhow.
|
158
|
+
def hud_table
|
159
|
+
table = Terminal::Table.new
|
160
|
+
table.title = @pastel.bright_red(@dungeon.name)
|
161
|
+
table.style = { width: 80, padding_left: 3, border_x: '=' }
|
162
|
+
table.rows = build_table_rows
|
163
|
+
system 'clear'
|
164
|
+
puts table
|
165
|
+
puts room_description
|
166
|
+
end
|
167
|
+
|
168
|
+
# Pulled this and the functions it calls below out here to make it a little
|
169
|
+
# clearer and to be able to make changes without breaking my brain.
|
170
|
+
def build_table_rows
|
171
|
+
t = []
|
172
|
+
t << [@player.name, "LVL: #{@player.level}"]
|
173
|
+
t << [display_health, display_xp]
|
174
|
+
t << [display_weapon, display_dungeon_level]
|
175
|
+
end
|
176
|
+
|
177
|
+
def display_health
|
178
|
+
current_health = @player.health[0]
|
179
|
+
max_health = @player.health[1]
|
180
|
+
hud_health = "HP: #{current_health}/#{max_health}"
|
181
|
+
|
182
|
+
return @pastel.red(hud_health) if current_health < max_health / 2
|
183
|
+
@pastel.green(hud_health)
|
184
|
+
end
|
185
|
+
|
186
|
+
def display_xp
|
187
|
+
"EXP: #{@player.xp}/#{@player.level * 10}"
|
188
|
+
end
|
189
|
+
|
190
|
+
def display_weapon
|
191
|
+
"Weapon: #{@player.weapon.name} (#{@player.weapon.damage})"
|
192
|
+
end
|
193
|
+
|
194
|
+
def display_dungeon_level
|
195
|
+
"Dungeon Level: #{@dungeon.level}/#{@dungeon.max_level}"
|
196
|
+
end
|
197
|
+
|
198
|
+
# Pretty straightforward but I have plans for the room description
|
199
|
+
# generation to be far more interesting. So check back here sometime in the
|
200
|
+
# next millenium.
|
201
|
+
def room_description
|
202
|
+
word_start = beginning(@dungeon.room.description)
|
203
|
+
puts "You are in #{word_start} #{@dungeon.room.description} room."
|
204
|
+
end
|
205
|
+
|
206
|
+
# Checks if a word is an 'an' word or an 'a' word.
|
207
|
+
# http://stackoverflow.com/a/18463759/1576860
|
208
|
+
def beginning(word)
|
209
|
+
# That weird looking thing below is interpolated as an array, so it would
|
210
|
+
# come out looking like: [a, e, i, o ,u]
|
211
|
+
%w(a e i o u).include?(word[0]) ? 'an' : 'a'
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
data/lib/zarta/player.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
# The catch-all module for Zarta
|
2
|
+
module Zarta
|
3
|
+
# The Bloated Beast. The Mother of Methods. The Function Fornicator.
|
4
|
+
class Player
|
5
|
+
# The player's name
|
6
|
+
attr_accessor :name
|
7
|
+
|
8
|
+
# The player's health in an array
|
9
|
+
# [current health / max health]
|
10
|
+
attr_accessor :health
|
11
|
+
|
12
|
+
# The player's current experience level
|
13
|
+
attr_accessor :level
|
14
|
+
|
15
|
+
# The player's accumulated experience points
|
16
|
+
attr_accessor :xp
|
17
|
+
|
18
|
+
# The player's current weapon
|
19
|
+
attr_accessor :weapon
|
20
|
+
|
21
|
+
def initialize(dungeon)
|
22
|
+
@name = 'Testy McTestface'
|
23
|
+
@health = [100, 100]
|
24
|
+
@level = 1
|
25
|
+
@xp = 0
|
26
|
+
@dungeon = dungeon
|
27
|
+
@weapon = Zarta::Weapon.new(@dungeon)
|
28
|
+
@prompt = TTY::Prompt.new
|
29
|
+
@pastel = Pastel.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def handle_weapon
|
33
|
+
@room_weapon = @dungeon.room.weapon
|
34
|
+
@room_weapon_c = @pastel.cyan.bold(@room_weapon.name)
|
35
|
+
@weapon_handled = false
|
36
|
+
Zarta::HUD.new(@dungeon)
|
37
|
+
puts "You see a #{@room_weapon_c} in this room."
|
38
|
+
|
39
|
+
# Repeat until they pick it up or leave it
|
40
|
+
prompt_weapon until @weapon_handled
|
41
|
+
end
|
42
|
+
|
43
|
+
def prompt_weapon
|
44
|
+
weapon_choice = @prompt.select('What do you want to do?') do |menu|
|
45
|
+
menu.choice 'Pick it up'
|
46
|
+
menu.choice 'Look at it'
|
47
|
+
menu.choice 'Leave it'
|
48
|
+
end
|
49
|
+
pickup_weapon if weapon_choice == 'Pick it up'
|
50
|
+
@room_weapon.inspect_weapon if weapon_choice == 'Look at it'
|
51
|
+
leave_weapon if weapon_choice == 'Leave it'
|
52
|
+
end
|
53
|
+
|
54
|
+
def pickup_weapon
|
55
|
+
puts 'Your current weapon will be replaced.'
|
56
|
+
return unless @prompt.yes?('Are you sure?')
|
57
|
+
@weapon = @room_weapon
|
58
|
+
@weapon_handled = true
|
59
|
+
Zarta::HUD.new(@dungeon)
|
60
|
+
end
|
61
|
+
|
62
|
+
def leave_weapon
|
63
|
+
return unless @prompt.yes?('Are you sure?')
|
64
|
+
@weapon_handled = true
|
65
|
+
end
|
66
|
+
|
67
|
+
def handle_enemy
|
68
|
+
@enemy = @dungeon.room.enemy
|
69
|
+
@enemy_c = @pastel.magenta.bold(@enemy.name)
|
70
|
+
puts "There is a #{@enemy_c} in here!"
|
71
|
+
|
72
|
+
# Loop until the enemy is dealt with.
|
73
|
+
prompt_enemy until @enemy.dealt_with
|
74
|
+
end
|
75
|
+
|
76
|
+
def prompt_enemy
|
77
|
+
enemy_choice = @prompt.select('What do you want to do?') do |menu|
|
78
|
+
menu.choice 'Fight!'
|
79
|
+
menu.choice 'Flee!'
|
80
|
+
menu.choice 'Look!'
|
81
|
+
end
|
82
|
+
|
83
|
+
fight if enemy_choice == 'Fight!'
|
84
|
+
flee if enemy_choice == 'Flee!'
|
85
|
+
@enemy.inspect if enemy_choice == 'Look!'
|
86
|
+
end
|
87
|
+
|
88
|
+
def fight
|
89
|
+
Zarta::HUD.new(@dungeon)
|
90
|
+
player_turn
|
91
|
+
enemy_killed && return if @enemy.dealt_with
|
92
|
+
@enemy.enemy_turn
|
93
|
+
end
|
94
|
+
|
95
|
+
def player_turn
|
96
|
+
Zarta::HUD.new(@dungeon)
|
97
|
+
hit = player_damage
|
98
|
+
@enemy.take_damage(hit)
|
99
|
+
puts "You attack the #{@enemy_c}!"
|
100
|
+
puts "You hit for #{@pastel.bright_yellow.bold(hit)} damage."
|
101
|
+
gets
|
102
|
+
end
|
103
|
+
|
104
|
+
# Another Turing-level algorithm for determining how hard the player hits.
|
105
|
+
# It would be cool to generate critical hits in here.
|
106
|
+
def player_damage
|
107
|
+
base_damage = @weapon.damage + rand(@weapon.damage)
|
108
|
+
hit = base_damage + rand(@level) + @level
|
109
|
+
hit.round
|
110
|
+
end
|
111
|
+
|
112
|
+
def flee
|
113
|
+
return unless @prompt.yes?('Are you sure?')
|
114
|
+
puts "You try to get away from the #{@enemy_c}"
|
115
|
+
flee_hit
|
116
|
+
@enemy.dealt_with = true
|
117
|
+
gets
|
118
|
+
end
|
119
|
+
|
120
|
+
def flee_hit
|
121
|
+
base = @dungeon.level + @level
|
122
|
+
level_difference = @enemy.level - @level
|
123
|
+
flee_chance = (rand(base) + level_difference) * FLEE_CHANCE
|
124
|
+
flee_damage && return if rand(base) <= flee_chance
|
125
|
+
puts 'You are successful!'
|
126
|
+
@enemy.dealt_with = true
|
127
|
+
end
|
128
|
+
|
129
|
+
def flee_damage
|
130
|
+
enemy_hit = @enemy.enemy_damage
|
131
|
+
take_damage(enemy_hit)
|
132
|
+
puts "The #{@enemy_c} hits you as you try to flee!"
|
133
|
+
puts "You take #{@pastel.red.bold(enemy_hit)} hits you as you flee!"
|
134
|
+
end
|
135
|
+
|
136
|
+
def take_damage(damage)
|
137
|
+
@health[0] -= damage
|
138
|
+
death if @health[0] <= 0
|
139
|
+
end
|
140
|
+
|
141
|
+
def death
|
142
|
+
puts 'You have been killed!'
|
143
|
+
puts @pastel.bright_red.bold('GAME OVER')
|
144
|
+
gets
|
145
|
+
exit[0]
|
146
|
+
end
|
147
|
+
|
148
|
+
def enemy_killed
|
149
|
+
xp_gained = gain_xp
|
150
|
+
puts "You have slain the #{@enemy_c}!"
|
151
|
+
puts "You gain #{@pastel.bright_blue.bold(xp_gained)} Experience."
|
152
|
+
level_up if @xp >= @level * NEXT_LEVEL_XP
|
153
|
+
@enemy.dealt_with = true
|
154
|
+
player_wins if @enemy.name == 'BOSS!'
|
155
|
+
gets
|
156
|
+
end
|
157
|
+
|
158
|
+
# I've never made this method call, so I'll just assume that it works...
|
159
|
+
def player_wins
|
160
|
+
puts 'Congrats, you won the game!'
|
161
|
+
gets
|
162
|
+
exit[0]
|
163
|
+
end
|
164
|
+
|
165
|
+
def gain_xp
|
166
|
+
xp_gained = @enemy.level + @level + @dungeon.level
|
167
|
+
@xp += xp_gained
|
168
|
+
xp_gained
|
169
|
+
end
|
170
|
+
|
171
|
+
def level_up
|
172
|
+
@xp -= @level * NEXT_LEVEL_XP
|
173
|
+
@level += 1
|
174
|
+
puts @pastel.bright_blue.bold('You gain a level!')
|
175
|
+
puts "You are now level #{@pastel.bright_blue.bold(@level)}"
|
176
|
+
health_increase
|
177
|
+
end
|
178
|
+
|
179
|
+
def health_increase
|
180
|
+
base_increase = (@level - 1) + @dungeon.level
|
181
|
+
increase = rand(base_increase..base_increase * HEALTH_INCREASE)
|
182
|
+
@health[1] += increase
|
183
|
+
@health[0] = @health[1]
|
184
|
+
puts 'Your health is replenished!'
|
185
|
+
puts "You gain #{@pastel.bright_green.bold(increase)} to max health."
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|