pathfinder-dnd-tools 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/pathfinder +53 -0
- data/lib/pathfinder_dnd.rb +5 -0
- data/lib/pathfinder_dnd/character_sheet.rb +145 -0
- data/lib/pathfinder_dnd/oauth.rb +93 -0
- data/lib/pathfinder_dnd/state_manager.rb +67 -0
- data/lib/pathfinder_dnd/tools.rb +88 -0
- metadata +99 -0
data/bin/pathfinder
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Run skill checks against the stats from your Google Drive character
|
4
|
+
# sheet.
|
5
|
+
|
6
|
+
require "pathfinder_dnd"
|
7
|
+
require "pry" # my fav console evar
|
8
|
+
|
9
|
+
# OAuth flow
|
10
|
+
state = Pathfinder::StateManager.new
|
11
|
+
|
12
|
+
# Set up the env...
|
13
|
+
character = nil
|
14
|
+
while character.nil?
|
15
|
+
begin
|
16
|
+
character = state.get_character_sheet
|
17
|
+
rescue GoogleDrive::AuthenticationError => error
|
18
|
+
puts "There was an error authenticating with Google Drive:\n"
|
19
|
+
puts error.to_s
|
20
|
+
puts "\n\nRe-linking with Google Drive..."
|
21
|
+
state.authorize
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Boom, UI.
|
26
|
+
Pry.config.prompt = [ proc { 'd&d>> ' }, proc { ' | ' } ]
|
27
|
+
|
28
|
+
class Fixnum
|
29
|
+
# Calculate the ability modifier from an integer
|
30
|
+
# @example strength.modifier
|
31
|
+
def modifier
|
32
|
+
((self - 10) / 2).to_i
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# all skills & most stats included
|
37
|
+
puts '--- Pathfinder Character Tools 2: Revenge of Google Docs ---
|
38
|
+
version 0.2 "Beware of OAuth"
|
39
|
+
|
40
|
+
You are in a fully-interactive ruby console.
|
41
|
+
You can `ls` to see methods.
|
42
|
+
|
43
|
+
Roll basic skill checks and saving throws by typing `check <bonus>`
|
44
|
+
You can roll a 20-sided dice using `check` without a bonus.
|
45
|
+
Roll dice with `roll <count>, <sides>`
|
46
|
+
Tab completion is enabled.
|
47
|
+
|
48
|
+
type `help` for an overview of console usage
|
49
|
+
type `show-doc <method>` to view help for that specific method
|
50
|
+
|
51
|
+
'
|
52
|
+
|
53
|
+
character.pry
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# D&D functions
|
2
|
+
require "pathfinder_dnd/tools"
|
3
|
+
|
4
|
+
module Pathfinder
|
5
|
+
# The whole of the Pathfinder Spreadsheet-tools environment.
|
6
|
+
# The character-sheet provides one-word lookup of almost all essential
|
7
|
+
# character statistics.
|
8
|
+
#
|
9
|
+
# On a technical level, a CharacterSheet wraps access to a
|
10
|
+
# GoogleDrive::Worksheet with friendly helper methods. Skill accessor
|
11
|
+
# methods are dynamically added at object instantiation.
|
12
|
+
#
|
13
|
+
# CharacterSheet is currently the gameplay interface, so it includes
|
14
|
+
# Pathfinder::Tools for easy dice rolling, skill checks, and ninja
|
15
|
+
# business.
|
16
|
+
class CharacterSheet
|
17
|
+
|
18
|
+
# include standard D&D tools
|
19
|
+
include Pathfinder::Tools
|
20
|
+
|
21
|
+
# What sheet title to pull stats from?
|
22
|
+
STATS_SHEET = 'Stats, Skills, Weapons'
|
23
|
+
|
24
|
+
attr_reader :doc, :stats
|
25
|
+
attr_accessor :hp
|
26
|
+
|
27
|
+
# reads a cell directly from the spreadsheet.
|
28
|
+
def self.cell_reader(name, row_or_coord, col = nil, sheet_index = 0)
|
29
|
+
define_method(name) do
|
30
|
+
sheets = instance_variable_get('@sheets')
|
31
|
+
sheet = sheets[sheet_index]
|
32
|
+
|
33
|
+
if row_or_coord.is_a? Numeric
|
34
|
+
return sheet[row_or_coord, col].to_i
|
35
|
+
else
|
36
|
+
return sheet[row_or_coord].to_i
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
###############################
|
42
|
+
# @!group Stats and Abilities
|
43
|
+
# access modifiers like `strength.modifier`
|
44
|
+
|
45
|
+
cell_reader :name, 'M2'
|
46
|
+
cell_reader :level, 'V5'
|
47
|
+
|
48
|
+
# Ability stats
|
49
|
+
cell_reader :strength, 'E14'
|
50
|
+
cell_reader :dexterity, 'E17'
|
51
|
+
cell_reader :constitution, 'E20'
|
52
|
+
cell_reader :intelligence, 'E23'
|
53
|
+
cell_reader :wisdom, 'E26'
|
54
|
+
cell_reader :charisma, 'E29'
|
55
|
+
|
56
|
+
# Defence
|
57
|
+
cell_reader :max_hp, 'U11'
|
58
|
+
cell_reader :ac, 'E33'
|
59
|
+
cell_reader :touch_ac, 'E36'
|
60
|
+
|
61
|
+
# Saving throws
|
62
|
+
cell_reader :fortitude, 'H40'
|
63
|
+
cell_reader :reflex, 'H42'
|
64
|
+
cell_reader :will, 'H44'
|
65
|
+
|
66
|
+
# Combat stuff
|
67
|
+
cell_reader :initiative, 'W30'
|
68
|
+
cell_reader :bab, 'L46'
|
69
|
+
cell_reader :cmb, 'E50'
|
70
|
+
cell_reader :cmd, 'E52'
|
71
|
+
|
72
|
+
# @!endgroup
|
73
|
+
################################
|
74
|
+
|
75
|
+
|
76
|
+
# session: a google_drive session
|
77
|
+
# key: the Drive identifier for the character sheet document
|
78
|
+
# (it's the key= query param when you load the char sheet in the browser)
|
79
|
+
def initialize(session, key)
|
80
|
+
|
81
|
+
# This is where failure will occur if Oauth is fucked
|
82
|
+
@doc = session.spreadsheet_by_key(key)
|
83
|
+
|
84
|
+
|
85
|
+
# all we need for now
|
86
|
+
@stats = @doc.worksheet_by_title(STATS_SHEET)
|
87
|
+
@sheets = [@stats]
|
88
|
+
|
89
|
+
if @stats.nil?
|
90
|
+
raise "Couldn't load the Stats charsheet"
|
91
|
+
end
|
92
|
+
|
93
|
+
# set starting HP
|
94
|
+
@hp = self.max_hp
|
95
|
+
|
96
|
+
# write in skill values
|
97
|
+
inject_instance_properties(get_raw_skills())
|
98
|
+
end
|
99
|
+
|
100
|
+
# Refeshes this instance with new data from the online character sheet,
|
101
|
+
# including updates to skills.
|
102
|
+
def refresh
|
103
|
+
@sheets.each {|s| s.reload()}
|
104
|
+
inject_instance_properties(get_raw_skills())
|
105
|
+
end
|
106
|
+
|
107
|
+
# Builds a <String> => <Integer> hash of skill scores from the character spreadsheet.
|
108
|
+
def get_raw_skills(start_loc = 'AH16', offset = 6)
|
109
|
+
# scrape the spreadsheet skills list
|
110
|
+
skills = {}
|
111
|
+
start, names_col = @stats.cell_name_to_row_col(start_loc)
|
112
|
+
skills_col = names_col + offset
|
113
|
+
(start..start+38).each do |row|
|
114
|
+
# more clear to split this up
|
115
|
+
skill_name = @stats[row, names_col]
|
116
|
+
skill_val = @stats[row, skills_col]
|
117
|
+
skills[skill_name] = skill_val.to_i
|
118
|
+
end
|
119
|
+
|
120
|
+
skills
|
121
|
+
end
|
122
|
+
|
123
|
+
# Injects a <String> => <Any> hash of properties into this instance as attribute accessors.
|
124
|
+
# If the accessor methods already exist, then the local variables they wrap are updated.
|
125
|
+
#
|
126
|
+
# This method is used in conjuction with `get_raw_skill` to populate the intance with skill
|
127
|
+
# fields at runtime.
|
128
|
+
def inject_instance_properties(props)
|
129
|
+
# for adding these properties to only THIS instance
|
130
|
+
metaclass = (class << self; self; end)
|
131
|
+
|
132
|
+
props.each do |name, value|
|
133
|
+
safe_name = name.downcase.gsub(/\s/, '_').gsub(/[^a-zA-Z0-9_]/, '').to_sym
|
134
|
+
|
135
|
+
# define the accessor for this skill if we haven't already
|
136
|
+
if not self.respond_to? safe_name
|
137
|
+
metaclass.class_eval { attr_reader safe_name }
|
138
|
+
end
|
139
|
+
|
140
|
+
# update the skill value
|
141
|
+
instance_variable_set("@#{safe_name}", value)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require "oauth2"
|
2
|
+
|
3
|
+
module Pathfinder
|
4
|
+
# Abstracts over the process to OAuth a command-line client with Google.
|
5
|
+
# Saves the user's OAuth refresh token in whatever transactional Pstore-like
|
6
|
+
# bucket you provide.
|
7
|
+
class OAuth
|
8
|
+
# TODO: move into ENV
|
9
|
+
#CLIENT_ID = ENV['CLIENT_ID']
|
10
|
+
#CLIENT_SECRET = ENV['CLIENT_SECRET']
|
11
|
+
|
12
|
+
CLIENT_ID = '539096868219.apps.googleusercontent.com'
|
13
|
+
CLIENT_SECRET = 'HSJKoPRwscFvYvkP-6HhmEaH'
|
14
|
+
|
15
|
+
# this tells Google what permissions we are requesting
|
16
|
+
# I'd prefer to use ReadOnly, but I don't want to rewrite this gem
|
17
|
+
# SCOPE = 'https://www.googleapis.com/auth/drive.readonly'
|
18
|
+
SCOPE = "https://docs.google.com/feeds/ " +
|
19
|
+
"https://docs.googleusercontent.com/ " +
|
20
|
+
"https://spreadsheets.google.com/feeds/"
|
21
|
+
|
22
|
+
# REDIRECT_URI = 'http://localhost' # see the Google API console
|
23
|
+
REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
|
24
|
+
|
25
|
+
attr_reader :access_token
|
26
|
+
|
27
|
+
# Pass in a PSTORE to use for refresh_token storage
|
28
|
+
def initialize(storage)
|
29
|
+
@client = OAuth2::Client.new(CLIENT_ID, CLIENT_SECRET, {
|
30
|
+
:site => 'https://accounts.google.com',
|
31
|
+
:authorize_url => '/o/oauth2/auth',
|
32
|
+
:token_url => '/o/oauth2/token'
|
33
|
+
})
|
34
|
+
|
35
|
+
@storage = storage
|
36
|
+
end
|
37
|
+
|
38
|
+
# Round-trip the user throught the Google OAuth process
|
39
|
+
def authorize()
|
40
|
+
# Step 1
|
41
|
+
puts "\n\nOpen this URL into your browser to connect this app with Google: "
|
42
|
+
puts @client.auth_code.authorize_url(
|
43
|
+
:scope => SCOPE,
|
44
|
+
:access_type => 'offline',
|
45
|
+
:redirect_uri => REDIRECT_URI,
|
46
|
+
:approval_prompt => 'force'
|
47
|
+
)
|
48
|
+
|
49
|
+
# Step 2 is performed in the browser by the user
|
50
|
+
|
51
|
+
# Step 3
|
52
|
+
puts "\n\nPaste the `code` parameter from the redirect URL here to finish authorization: "
|
53
|
+
code = gets.chomp
|
54
|
+
|
55
|
+
@access_token = @client.auth_code.get_token(code, {
|
56
|
+
:redirect_uri => REDIRECT_URI,
|
57
|
+
:token_method => :post
|
58
|
+
})
|
59
|
+
end
|
60
|
+
|
61
|
+
# write the refresh token to storage for future use
|
62
|
+
def save_token()
|
63
|
+
if @access_token.nil?
|
64
|
+
raise 'No access token to store'
|
65
|
+
end
|
66
|
+
|
67
|
+
# wow look at the saftey!
|
68
|
+
@storage.transaction do
|
69
|
+
@storage[:refesh_token] = @access_token.refresh_token
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
# create a new session token from whatever refresh token is in the storage.
|
75
|
+
def load_token()
|
76
|
+
token = nil
|
77
|
+
refresh = nil
|
78
|
+
|
79
|
+
@storage.transaction do
|
80
|
+
refresh = @storage[:refesh_token]
|
81
|
+
end
|
82
|
+
|
83
|
+
if refresh.nil?
|
84
|
+
puts 'Could not load OAuth token from storage'
|
85
|
+
return nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# fuck this
|
89
|
+
@access_token = OAuth2::AccessToken.from_hash(@client, :refresh_token => refresh).refresh!
|
90
|
+
@access_token
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "pstore"
|
4
|
+
|
5
|
+
require "google_drive"
|
6
|
+
|
7
|
+
require "pathfinder_dnd/oauth"
|
8
|
+
require "pathfinder_dnd/character_sheet"
|
9
|
+
|
10
|
+
module Pathfinder
|
11
|
+
|
12
|
+
# Default storage place
|
13
|
+
STORAGE = PStore.new(File.expand_path('~/.config/pathfinder.pstore'))
|
14
|
+
|
15
|
+
# Sets up the OAuth connection to Google Drive and manages user settings
|
16
|
+
# such as the Google Drive key/id of the character sheet document.
|
17
|
+
class StateManager
|
18
|
+
|
19
|
+
# for ease of use
|
20
|
+
attr_accessor :token, :session, :auth
|
21
|
+
attr_reader :doc_id
|
22
|
+
|
23
|
+
# create a StateManager with a optional storage backend.
|
24
|
+
# Loads data including OAuth tokens and the document identifier from the PStore
|
25
|
+
# By default all StateMangers read/write to the Pathfinder settings
|
26
|
+
# PStore in ~/.config/pathfinder.pstore
|
27
|
+
def initialize(storage = Pathfinder::STORAGE)
|
28
|
+
@storage = storage
|
29
|
+
@auth = Pathfinder::OAuth.new(@storage)
|
30
|
+
@token = @auth.load_token
|
31
|
+
|
32
|
+
@storage.transaction do
|
33
|
+
@doc_id = @storage[:doc]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Round-trip the user through Google's OAuth process
|
38
|
+
def authorize
|
39
|
+
@auth.authorize
|
40
|
+
@auth.save_token
|
41
|
+
@token = @auth.access_token
|
42
|
+
end
|
43
|
+
|
44
|
+
def doc_id=(k)
|
45
|
+
@doc_id = k
|
46
|
+
@storage.transaction do
|
47
|
+
@storage[:doc] = @doc_id
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Perform all necessary input to get a character sheet from a user's Google Drive.
|
52
|
+
def get_character_sheet
|
53
|
+
if @token.nil?
|
54
|
+
self.authorize()
|
55
|
+
end
|
56
|
+
|
57
|
+
if @doc_id.nil?
|
58
|
+
puts "\n\nPaste the `key=` parameter from your character's Google Drive URL here:"
|
59
|
+
self.doc_id = gets.chomp
|
60
|
+
end
|
61
|
+
|
62
|
+
@session = GoogleDrive.login_with_oauth(@token)
|
63
|
+
|
64
|
+
return Pathfinder::CharacterSheet.new(@session, @doc_id)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Pathfinder
|
2
|
+
|
3
|
+
# This is where all the in-game functions are defined, like rolling dice.
|
4
|
+
module Tools
|
5
|
+
|
6
|
+
# Deep sum arrays of integers and arrays.
|
7
|
+
# @param array [Array] the list to sum up.
|
8
|
+
# @return [Integer] the total
|
9
|
+
def sum(array)
|
10
|
+
res = 0
|
11
|
+
array.each do |i|
|
12
|
+
if i.respond_to? :each
|
13
|
+
res += sum(i)
|
14
|
+
else
|
15
|
+
res += i
|
16
|
+
end
|
17
|
+
end
|
18
|
+
res
|
19
|
+
end
|
20
|
+
|
21
|
+
# Roll one dice with N sides.
|
22
|
+
# When rolling 20-sided dice, alerts on very high or low rolls.
|
23
|
+
#
|
24
|
+
# @param sides [Integer] number of sides on the dice.
|
25
|
+
# @param crit_level [Integer] alert the user to dice
|
26
|
+
# rolls at or above this level when rolling 20-sided dice. Default 19.
|
27
|
+
# @param failure_level [integer] alert the user to dice rolls
|
28
|
+
# at or below this level when rolling 20-sided dice. Default 1.
|
29
|
+
# @return [Integer] result of the dice roll
|
30
|
+
def single_roll(sides, crit_level = 19, failure_level = 1)
|
31
|
+
res = 1 + rand(sides)
|
32
|
+
if sides == 20 and res >= crit_level
|
33
|
+
puts "Crit: rolled #{res}"
|
34
|
+
end
|
35
|
+
|
36
|
+
if sides == 20 and res <= failure_level
|
37
|
+
puts "Low roll: rolled #{res}"
|
38
|
+
end
|
39
|
+
|
40
|
+
res
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Roll a number of dice
|
45
|
+
# @param dice [Integer] number of dice to roll. Default 1.
|
46
|
+
# @param sides [Integer] number of sides on each die. Default 6.
|
47
|
+
# @see #single_roll
|
48
|
+
# @return [Array<Integer>] list of dice roll results
|
49
|
+
def roll(dice = 1, sides = 6, crit_level = 19, failure_level = 1)
|
50
|
+
(1..dice).to_a.map{ |_| single_roll(sides, crit_level, failure_level) }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Roll a 20-sided dice and add an optional skill bonus
|
54
|
+
# @param skill [Integer] your skill-check or saving-throw bonus. Default 0.
|
55
|
+
# @return [Integer]
|
56
|
+
def check(skill = 0)
|
57
|
+
sum(roll(1, 20)) + skill
|
58
|
+
end
|
59
|
+
|
60
|
+
# roll to hit
|
61
|
+
# Rolls a basic check with an additional bonus
|
62
|
+
# @param bonus [Integer] added bonus, usually from Anne's blung-ing or haste
|
63
|
+
# @param base [Integer] your usual attack bonus. Character-specific default 14.
|
64
|
+
# @see #check
|
65
|
+
def atk_roll(bonus = 0, base = 14)
|
66
|
+
check(base) + bonus
|
67
|
+
end
|
68
|
+
|
69
|
+
# roll for damage
|
70
|
+
# Character-specific to Shalizara
|
71
|
+
# @return [Array<Integer>] magic and normal components of the attack
|
72
|
+
def normal_damage(magic_damage_dice = 2)
|
73
|
+
magic = sum(roll(magic_damage_dice, 6)) + 2
|
74
|
+
dagger = sum(roll(1, 4)) + 2
|
75
|
+
[magic, dagger]
|
76
|
+
end
|
77
|
+
|
78
|
+
# Roll for sneak-attack damage
|
79
|
+
# Character-specific to Shalizara
|
80
|
+
# @see #normal_damage
|
81
|
+
def sneak_damage(magic_damage_dice = 2)
|
82
|
+
sneak = sum(roll(5, 6))
|
83
|
+
reg = normal_damage(magic_damage_dice)
|
84
|
+
reg << sneak
|
85
|
+
reg
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pathfinder-dnd-tools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jake Teton-Landis
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: pry
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: google_drive
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: oauth2
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Companion tools for the Pathfinder Google Drive character sheet template
|
63
|
+
email: just.1.jake@gmail.com
|
64
|
+
executables:
|
65
|
+
- pathfinder
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- lib/pathfinder_dnd/character_sheet.rb
|
70
|
+
- lib/pathfinder_dnd/tools.rb
|
71
|
+
- lib/pathfinder_dnd/state_manager.rb
|
72
|
+
- lib/pathfinder_dnd/oauth.rb
|
73
|
+
- lib/pathfinder_dnd.rb
|
74
|
+
- bin/pathfinder
|
75
|
+
homepage: http://jake.teton-landis.org/projects/pathfinder-dnd
|
76
|
+
licenses: []
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 1.8.24
|
96
|
+
signing_key:
|
97
|
+
specification_version: 3
|
98
|
+
summary: D&D Tools
|
99
|
+
test_files: []
|