zarta 0.0.1
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.
- 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
|