pathfinder-dnd-tools 0.1.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.
@@ -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,5 @@
1
+ require 'pathfinder_dnd/character_sheet'
2
+ require 'pathfinder_dnd/state_manager'
3
+
4
+ # does nothing.
5
+
@@ -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: []