pathfinder-dnd-tools 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []