Compare commits

..

10 Commits

  1. 10
      CHANGELOG.md
  2. 6
      Gemfile
  3. 84
      Gemfile.lock
  4. 47
      README.md
  5. 32
      src/app.rb
  6. 1
      src/data/custom/.gitignore
  7. 3
      src/data/dark_elf/lifepaths.json
  8. 5
      src/data/dark_elf/resources.json
  9. 2
      src/data/dark_elf/traits.json
  10. 51
      src/data/gold/lifepaths/dwarf.json
  11. 36
      src/data/gold/lifepaths/elf.json
  12. 23
      src/data/gold/lifepaths/man.json
  13. 27
      src/data/gold/lifepaths/orc.json
  14. 27
      src/data/gold/lifepaths/roden.json
  15. 29
      src/data/gold/lifepaths/wolf.json
  16. 5
      src/data/gold/resources/dwarf.json
  17. 5
      src/data/gold/resources/elf.json
  18. 5
      src/data/gold/resources/man.json
  19. 5
      src/data/gold/resources/orc.json
  20. 5
      src/data/gold/resources/roden.json
  21. 5
      src/data/gold/resources/wolf.json
  22. 114
      src/data/gold/starting_stat_pts/dwarf.json
  23. 146
      src/data/gold/starting_stat_pts/elf.json
  24. 90
      src/data/gold/starting_stat_pts/man.json
  25. 98
      src/data/gold/starting_stat_pts/orc.json
  26. 74
      src/data/gold/starting_stat_pts/roden.json
  27. 58
      src/data/gold/starting_stat_pts/wolf.json
  28. 130
      src/data/gold/stocks/dwarf.json
  29. 161
      src/data/gold/stocks/elf.json
  30. 99
      src/data/gold/stocks/man.json
  31. 114
      src/data/gold/stocks/orc.json
  32. 90
      src/data/gold/stocks/roden.json
  33. 75
      src/data/gold/stocks/wolf.json
  34. 8
      src/data/gold/traits.json
  35. 45
      src/data/troll/lifepaths.json
  36. 5
      src/data/troll/resources.json
  37. 82
      src/data/troll/starting_stat_pts.json
  38. 100
      src/data/troll/stock.json
  39. 4
      src/data/wizard/lifepaths.json
  40. 13
      src/lib/data.rb
  41. 27
      src/lib/data/custom.rb
  42. 6
      src/lib/data/dark_elf.rb
  43. 20
      src/lib/data/gold.rb
  44. 15
      src/lib/data/troll.rb
  45. 3
      src/lib/data/wizard.rb
  46. 34
      src/lib/stock.rb
  47. 67
      src/public/css/stocked.css
  48. 5
      src/public/js/burning-serialize.js
  49. 187
      src/public/js/burning-service.js
  50. 209
      src/public/js/burning.js
  51. 2
      src/public/js/server_settings.js
  52. 388
      src/public/js/stocked.js
  53. 42
      src/public/js/stocked/editable-input.js
  54. 1796
      src/public/js/stocked/test/data-archive.js
  55. 23
      src/public/js/stocked/test/data_1.json
  56. 7
      src/views/index.erb
  57. 2
      src/views/partials/help.erb
  58. 9
      src/views/partials/main.erb
  59. 377
      src/views/partials/stocked.erb
  60. 6
      src/views/partials/stocked/editableInput.erb
  61. 1
      tests/Algoric the Apologist Character Sheet.char
  62. 1
      tests/Algoric the Apologist Character Sheet.model
  63. 1
      tests/Tyastanarphen Character Sheet.char
  64. 1
      tests/Tyastanarphen Character Sheet.model
  65. 133
      tests/data/custom/test.lifepaths
  66. 25
      tests/data/custom/test.resources
  67. 39
      tests/data/custom/test.skills
  68. 95
      tests/data/custom/test.stock
  69. 55
      tests/data/custom/test.traits

@ -11,16 +11,6 @@ and this project adheres to [Semantic Versioning](semver).
- Custom upload for your own data files
- Updates to Roden and Great Wolves files (currently the data comes from Monster Burner and not Codex)
## [3.0.0] - 2023-09-03
### Added
- Custom stocks can now be added to a server
- Stock data moved from code to dedicated data file
- Data files split into stock, lifepath, skills, and traits files, with designated extensions
- "Born" lifepaths with arbitrary names
### Fixed
- Spite shade calculation
## [2.3.0] - 2019-07-28
### Added
- Display emotional attribute traits on PDF

@ -3,9 +3,9 @@ source 'https://rubygems.org'
gem 'thin'
gem 'sinatra'
gem 'sinatra-contrib'
gem 'prawn'
gem 'prawn-templates'
gem 'deep_merge', '~> 1.2', '>= 1.2.1'
gem 'prawn', '2.2.2'
gem 'prawn-templates', '0.1.1'
gem 'rubyzip'
group :development do
gem 'rerun'

@ -1,71 +1,73 @@
GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.1.0)
Ascii85 (1.0.3)
afm (0.2.2)
daemons (1.4.1)
deep_merge (1.2.2)
backports (3.15.0)
daemons (1.3.1)
eventmachine (1.2.7)
ffi (1.15.5)
ffi (1.11.1)
hashery (2.1.2)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
multi_json (1.15.0)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
pdf-core (0.9.0)
pdf-reader (2.11.0)
Ascii85 (~> 1.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
multi_json (1.13.1)
mustermann (1.0.3)
pdf-core (0.7.0)
pdf-reader (2.2.0)
Ascii85 (~> 1.0.0)
afm (~> 0.2.1)
hashery (~> 2.0)
ruby-rc4
ttfunk
prawn (2.4.0)
pdf-core (~> 0.9.0)
ttfunk (~> 1.7)
prawn-templates (0.1.2)
prawn (2.2.2)
pdf-core (~> 0.7.0)
ttfunk (~> 1.5)
prawn-templates (0.1.1)
pdf-reader (~> 2.0)
prawn (~> 2.2)
rack (2.2.8)
rack-protection (3.1.0)
rack (~> 2.2, >= 2.2.4)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
rack (2.0.7)
rack-protection (2.0.5)
rack
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
rerun (0.14.0)
rerun (0.13.0)
listen (~> 3.0)
ruby-rc4 (0.1.5)
ruby2_keywords (0.0.5)
sinatra (3.1.0)
mustermann (~> 3.0)
rack (~> 2.2, >= 2.2.4)
rack-protection (= 3.1.0)
ruby_dep (1.5.0)
rubyzip (2.3.2)
sinatra (2.0.5)
mustermann (~> 1.0)
rack (~> 2.0)
rack-protection (= 2.0.5)
tilt (~> 2.0)
sinatra-contrib (3.1.0)
sinatra-contrib (2.0.5)
backports (>= 2.8.2)
multi_json
mustermann (~> 3.0)
rack-protection (= 3.1.0)
sinatra (= 3.1.0)
tilt (~> 2.0)
thin (1.8.2)
mustermann (~> 1.0)
rack-protection (= 2.0.5)
sinatra (= 2.0.5)
tilt (>= 1.3, < 3)
thin (1.7.2)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
tilt (2.2.0)
ttfunk (1.7.0)
tilt (2.0.9)
ttfunk (1.5.1)
PLATFORMS
x86_64-linux
ruby
DEPENDENCIES
deep_merge (~> 1.2, >= 1.2.1)
prawn
prawn-templates
prawn (= 2.2.2)
prawn-templates (= 0.1.1)
rerun
rubyzip
sinatra
sinatra-contrib
thin
BUNDLED WITH
2.3.7
1.17.2

@ -1,10 +1,14 @@
# Charred Gold
This is a fork of [Charred Black](https://github.com/modality/charred-black) Below is a partial fork of the original README of Charred Black; especially since there is a departure from some of the original values. The unofficial, online, Burning Wheel Gold (+Codex) character burner. Adapted from [Charred](https://charred.herokuapp.com/). Later adapted from [Charred Black](https://github.com/modality/charred-black)
This is a fork of [https://github.com/modality/charred-black](Charred Black)
Below is the original README of Charred Black, possibly to be updated in the future;
especially since there is a departure from some of the original values.
The unofficial, online, Burning Wheel Gold (+Codex) character burner. Adapted from [Charred](https://charred.herokuapp.com/).
## Project Structure
* `Dockerfile` and `Dockerfile.dev` - Container definition files. The dev container has automated reloading if you mount the container's filesystem to the host machine. These mostly serve as examples to launch the app yourself, and are no longer supported. The app uses Sinatra to hope, and you should just be able to `bundle install` and then run `ruby app.rb` from the source directory. I will try to publish a SysV initscript soon.
* `Dockerfile` and `Dockerfile.dev` - Container definition files. The dev container has automated reloading if you mount the container's filesystem to the host machine. If you are unfamiliar with Docker, this app uses Sinatra and you can _probably_ get by with a bundle install and `ruby ./src/app.rb`.
* `src/data` - Binaries and data files for lifepaths live here, see `dark_elf` and `wizard` directory for well-defined examples
* `src/lib` - Ruby scripts for caching, PDF generation, and data loading
* `src/public` - Javascript and CSS
@ -13,27 +17,38 @@ This is a fork of [Charred Black](https://github.com/modality/charred-black) Bel
## Beliefs
We welcome community contributions, and you are welcome to fork this source code if you want to go your own way. As the maintainer, here's what you can expect from me when I judge contributions.
I welcome community contributions, and you are welcome to fork this source code if you want to go your own way. As the maintainer,
here's what you can expect from me when I judge contributions.
### Do One Thing Well
Charred Gold is a character creation utility. You are welcome to use the data, source code, or character files in the creation of other gaming tools, but let's keep this tool focused on one thing and do it really well.
Charred Black is a character creation utility. You are welcome to use the data, source code, or character files in the creation
of other gaming tools, but let's keep this tool focused on one thing and do it really well.
### Stick To Published Material
In order to keep the scope of my maintainership finite, I'm not planning to accept community-made lifepaths et al. for inclusion in this codebase. Each additional data set increases Charred Gold's startup time and memory requirements. The design of your lifepath requirements and emotional attributes may not be supported by the editor, or may be convoluted to implement. Most importantly, deciding to include any community-made content makes me an arbiter of quality, and I'd prefer not to have the Enmity Clause invoked because I rejected someone's homebrew.
In order to keep the scope of my maintainership finite, I'm not planning to accept community-made lifepaths et al. for inclusion
in this codebase. Each additional data set increases Charred Black's startup time and memory requirements. The design of your lifepath
requirements and emotional attributes may not be supported by the editor, or may be convoluted to implement. Most importantly, deciding to include any community-made content makes me an arbiter of quality, and I'd prefer not to have the Enmity Clause invoked because I rejected
someone's homebrew.
The current maintainers of Charred Gold are working on some tooling to allow "easy" creation of new settings, stocks, LPs, and so on. The exact method for handling has not been decided precisely.
**However**, I am working on a solution for uploading lifepaths et. al which would be stored locally in your browser and not permanently on
a server. This way, you can create data files to use with this tool and share them with your friends. If someone else
wants to keep a repository or forum thread of data files known to work with Charred Black, I am happy to link to it in this
documentation and from the website itself. I don't want to discourage contributions, I just want do one thing well.
### Keep It Mostly Stateless
Charred Gold uses an in-memory cache to allow users to upload JSON and then download .char and .pdf files. I don't know how the original Charred handled this, but the tradeoffs of this approach are:
Charred Black uses an in-memory cache to allow users to upload JSON and then download .char and .pdf files. I don't know how the
original Charred handled this, but the tradeoffs of this approach are:
1. Works as expected without an update to the frontend
2. PDF generation happens entirely in one process, limiting the amount of futzing you have to do with distributed systems
3. Because the cache is in memory, you can't scale processes horizontally
The cache has a limited number of keys, and only the first 16kb of data are used, with the aim of making this this app useless for nefarious purposes. The average size of a 4-lifepath character is around 4kb, so this should be more than enough. If you're trying to do something weird and your character file is bigger than this, consider using a pencil and paper.
The cache has a limited number of keys, and only the first 16kb of data are used, with the aim of making this this app useless
for nefarious purposes. The average size of a 4-lifepath character is around 4kb, so this should be more than enough. If you're
trying to do something weird and your character file is bigger than this, consider using a pencil and paper.
More guidelines:
@ -126,19 +141,3 @@ Some notes:
}
}
```
# Building
Requires Ruby 3.1 or better for support.
On Debian or derivatives this requires `apt-get install ruby-dev` in order to allow eventmachine to work (which is a hard dependency for thin, the webserver we're using for charred.
In order to run in normal use, simply run:
```
# sudo apt-get install ruby-dev bundler
# bundler install # on some distros this might be bundler3.1
# cd src
# HOST=0.0.0.0 PORT=7878 RACK_ENV=production ruby app.rb
```

@ -33,6 +33,11 @@ get /\/([\w]+)_partial/ do
erb "partials/#{partial}".to_sym
end
get /\/stocked\/([\w]+)_partial/ do
partial = params['captures'].first
erb "partials/stocked/#{partial}".to_sym
end
get '/namegen/:gender' do
if params['gender'] == 'female'
['Ada', 'Belle', 'Carmen', 'Desdemona', 'Edie'].sample
@ -49,20 +54,24 @@ get '/traits' do
json DATA[:traits]
end
get '/stocks' do
json DATA[:stocks]
end
get '/lifepaths/:stock' do
if DATA[:stocks].keys.include? params['stock']
if DATA[:stocks].include? params['stock']
json DATA[:lifepaths][params['stock']]
else
404
end
end
get '/starting_stat_pts/:stock' do
if DATA[:stocks].include? params['stock']
json DATA[:stat_pts][params['stock']]
else
404
end
end
get '/resources/:stock' do
if DATA[:stocks].keys.include? params['stock']
if DATA[:stocks].include? params['stock']
json DATA[:resources][params['stock']]
else
404
@ -114,6 +123,17 @@ post '/wiki' do
"/get_file?file=#{key}&download_name=#{data['name']} Character Sheet.wiki"
end
post '/stocked_download' do
request.body.rewind
raw = request.body.readpartial(16 * 1024)
puts raw
data = JSON.parse(raw)
key = "stock-#{Time.now.strftime('%Y%m%d%H%M%S%L')}-#{rand(1...10000)}"
CACHE.store key, data
"/get_file?file=#{key}&download_name=#{data['Name']}.stock"
end
get '/get_file' do
data = nil
if params['download_name'].match(/\.pdf$/)

@ -1,6 +1,4 @@
{
"stock": "elf",
"settings": {
"Path Of Spite Subsetting": {
"Griever": {
"time": 3,
@ -296,4 +294,3 @@
}
}
}
}

@ -1,6 +1,4 @@
{
"stock": "elf",
"resources": [
[
{
"name": "Bitter Poison",
"type": "gear",
@ -114,4 +112,3 @@
]
}
]
}

@ -10,7 +10,7 @@
"desc": "The Deceptive trait acts as a call-on for Sleight of Hand and Falsehood. However this trait so infuses the liar, he has a hard time telling the truth. Deceptive also incurs a +1 Ob penalty to Oratory, Command tests and a +2 Ob to Ugly Truth or Litany of Fools."
},
"Femme Fatale/Homme Fatale": {
"cost": 2,
"cost": 1,
"type": "call_on",
"desc": "Call-on for Seduction."
},

@ -1,6 +1,4 @@
{
"stock": "dwarf",
"settings": {
"Clansman Setting": {
"Born Clansman": {
"time": 20,
@ -17,10 +15,18 @@
"traits": [
1
],
"common_traits": [
"Accustomed To The Dark",
"Bearded",
"Greed",
"Oathsworn",
"Shaped From Earth And Stone",
"Stout",
"Tough"
],
"key_leads": [
"Guilder Setting"
],
"born": true
]
},
"Tender": {
"time": 20,
@ -347,10 +353,18 @@
"traits": [
1
],
"common_traits": [
"Accustomed To The Dark",
"Bearded",
"Greed",
"Oathsworn",
"Shaped From Earth And Stone",
"Stout",
"Tough"
],
"key_leads": [
"Clansman Setting"
],
"born": true
]
},
"Wordbearer": {
"time": 15,
@ -649,10 +663,18 @@
"traits": [
1
],
"common_traits": [
"Accustomed To The Dark",
"Bearded",
"Greed",
"Oathsworn",
"Shaped From Earth And Stone",
"Stout",
"Tough"
],
"key_leads": [
"Clansman Setting"
],
"born": true
]
},
"Ardent": {
"time": 21,
@ -931,11 +953,19 @@
2,
"Dvergar"
],
"common_traits": [
"Accustomed To The Dark",
"Bearded",
"Greed",
"Oathsworn",
"Shaped From Earth And Stone",
"Stout",
"Tough"
],
"key_leads": [
"Guilder Setting",
"Artificer Setting"
],
"born": true
]
},
"Abecedart": {
"time": 20,
@ -1943,4 +1973,3 @@
}
}
}
}

@ -1,6 +1,4 @@
{
"stock": "elf",
"settings": {
"Wilderlands Setting": {
"Born Wilder Elf": {
"time": 20,
@ -22,10 +20,17 @@
"traits": [
1
],
"common_traits": [
"Born Under The Silver Stars",
"Essence Of The Earth",
"Fair And Statuesque",
"First Born",
"Grief",
"Keen Sight"
],
"key_leads": [
"Citadel Setting"
],
"born": true
]
},
"Rider": {
"time": 20,
@ -512,10 +517,17 @@
"traits": [
1
],
"common_traits": [
"Born Under The Silver Stars",
"Essence Of The Earth",
"Fair And Statuesque",
"First Born",
"Grief",
"Keen Sight"
],
"key_leads": [
"Wilderlands Setting"
],
"born": true
]
},
"Servitor": {
"time": 20,
@ -1177,11 +1189,18 @@
"Fea",
"Aman"
],
"common_traits": [
"Born Under The Silver Stars",
"Essence Of The Earth",
"Fair And Statuesque",
"First Born",
"Grief",
"Keen Sight"
],
"key_leads": [
"Wilderlands Setting",
"Citadel Setting"
],
"born": true
]
},
"Student": {
"time": 25,
@ -2027,4 +2046,3 @@
}
}
}
}

@ -1,6 +1,4 @@
{
"stock": "man",
"settings": {
"Peasant Setting": {
"Born Peasant": {
"time": 8,
@ -25,8 +23,7 @@
"Professional Soldier Subsetting",
"Seafaring Setting",
"Religious Subsetting"
],
"born": true
]
},
"Farmer": {
"time": 8,
@ -702,7 +699,6 @@
},
"Villager Setting": {
"Village Born": {
"born": true,
"time": 10,
"res": 4,
"leads": [
@ -2410,7 +2406,6 @@
},
"City Dweller Setting": {
"City Born": {
"born": true,
"time": 12,
"res": 10,
"leads": [
@ -3075,10 +3070,10 @@
],
"traits": [
2,
"Catamite",
"Flamboyant",
"Comely",
"Sharp Dresser",
"Catalyst"
"Sharp Dresser"
],
"key_leads": [
"Outcast Subsetting",
@ -5139,8 +5134,7 @@
"Professional Soldier Subsetting",
"Seafaring Setting",
"Religious Subsetting"
],
"born": true
]
},
"Bastard": {
"time": 6,
@ -5475,8 +5469,10 @@
],
"skills": [
[
8,
10,
"Mounted Combat Training",
"Shield Training",
"Armor Training",
"Appropriate Weapons",
"Intimidation",
"Hunting",
@ -7619,8 +7615,7 @@
"key_leads": [
"Professional Soldier Subsetting",
"Outcast Subsetting"
],
"born": true
]
},
"Ditch Digging": {
"time": 4,
@ -9877,7 +9872,6 @@
},
"Seafaring Setting": {
"Son Of A Gun": {
"born": true,
"time": 8,
"res": 3,
"leads": [
@ -11818,4 +11812,3 @@
}
}
}
}

@ -1,6 +1,4 @@
{
"stock": "orc",
"settings": {
"Chattel Setting": {
"Born Chattel": {
"time": 10,
@ -24,11 +22,19 @@
"traits": [
1
],
"common_traits": [
"Cannibal",
"Cold Black Blood",
"Breeder",
"Fanged And Clawed",
"Loathsome And Twisted",
"Lynx-eyed, Like Burning Coals",
"Vile Language"
],
"key_leads": [
"Black Legion Subsetting",
"Servant Of The Dark Blood Subsetting"
],
"born": true
]
},
"Cattle Slave": {
"time": 5,
@ -426,10 +432,18 @@
"Born To Rule Them All",
"Enemy Of The Sun"
],
"common_traits": [
"Cannibal",
"Cold Black Blood",
"Breeder",
"Fanged And Clawed",
"Loathsome And Twisted",
"Lynx-eyed, Like Burning Coals",
"Vile Language"
],
"key_leads": [
"Servant Of The Dark Blood Subsetting"
],
"born": true
]
},
"The Rites": {
"time": 3,
@ -1579,4 +1593,3 @@
}
}
}
}

@ -1,6 +1,4 @@
{
"stock": "roden",
"settings": {
"Field Setting": {
"Born To The Fields": {
"time": 8,
@ -18,10 +16,18 @@
3,
"Vegetarian"
],
"common_traits": [
"Aecer's Likeness",
"Coat Of Fur",
"Communal",
"Enlarged Incisors",
"Quick-blooded",
"Tail",
"Large Ears"
],
"key_leads": [
"Society Subsetting"
],
"born": true
]
},
"Hand": {
"time": 3,
@ -566,10 +572,18 @@
3,
"Tunnel Vision"
],
"common_traits": [
"Aecer's Likeness",
"Coat Of Fur",
"Communal",
"Enlarged Incisors",
"Quick-blooded",
"Tail",
"Large Ears"
],
"key_leads": [
"Society Subsetting"
],
"born": true
]
},
"Pinky": {
"time": 2,
@ -1724,4 +1738,3 @@
}
}
}
}

@ -1,6 +1,4 @@
{
"stock": "wolf",
"settings": {
"Wild Pack Setting": {
"Born To The Pack": {
"time": 1,
@ -21,10 +19,19 @@
"traits": [
2
],
"common_traits": [
"Crushing Jaws",
"Deep Fur",
"Great Lupine Form",
"Lupine Intellect",
"Long-legged",
"Wolf's Eyes",
"Wolf's Snout",
"Woodland Ear"
],
"key_leads": [
"Captive Subsetting"
],
"born": true
]
},
"Yearling": {
"time": 1,
@ -328,10 +335,19 @@
"Vile Language",
"Demented"
],
"common_traits": [
"Crushing Jaws",
"Deep Fur",
"Great Lupine Form",
"Lupine Intellect",
"Long-legged",
"Wolf's Eyes",
"Wolf's Snout",
"Woodland Ear"
],
"key_leads": [
"Captive Subsetting"
],
"born": true
]
},
"Caged And Beaten": {
"time": 0.5,
@ -953,4 +969,3 @@
}
}
}
}

@ -1,6 +1,4 @@
{
"stock": "dwarf",
"resources": [
[
{
"name": "Shoddy Arms",
"rp": 5,
@ -152,4 +150,3 @@
"type": "gear"
}
]
}

@ -1,6 +1,4 @@
{
"stock": "elf",
"resources": [
[
{
"name": "Run Of The Mill Bow",
"type": "gear",
@ -176,4 +174,3 @@
]
}
]
}

@ -1,6 +1,4 @@
{
"stock": "man",
"resources": [
[
{
"name": "Arms",
"type": "gear",
@ -434,4 +432,3 @@
]
}
]
}

@ -1,6 +1,4 @@
{
"stock": "orc",
"resources": [
[
{
"name": "Rags",
"rp": 1,
@ -211,4 +209,3 @@
"type": "gear"
}
]
}

@ -1,6 +1,4 @@
{
"stock": "roden",
"resources": [
[
{
"name": "Arms",
"rp": 5,
@ -139,4 +137,3 @@
]
}
]
}

@ -1,6 +1,4 @@
{
"stock": "wolf",
"resources": [
[
{
"name": "Territory",
"type": "property",
@ -42,4 +40,3 @@
]
}
]
}

@ -0,0 +1,114 @@
[
{
"range": [
1,
20
],
"m": 6,
"p": 13
},
{
"range": [
21,
30
],
"m": 7,
"p": 13
},
{
"range": [
31,
50
],
"m": 7,
"p": 14
},
{
"range": [
51,
76
],
"m": 8,
"p": 15
},
{
"range": [
77,
111
],
"m": 8,
"p": 16
},
{
"range": [
112,
151
],
"m": 9,
"p": 16
},
{
"range": [
152,
199
],
"m": 9,
"p": 17
},
{
"range": [
200,
245
],
"m": 10,
"p": 18
},
{
"range": [
246,
300
],
"m": 11,
"p": 17
},
{
"range": [
301,
345
],
"m": 11,
"p": 16
},
{
"range": [
346,
396
],
"m": 12,
"p": 15
},
{
"range": [
397,
445
],
"m": 11,
"p": 14
},
{
"range": [
446,
525
],
"m": 11,
"p": 13
},
{
"range": [
526,
600
],
"m": 10,
"p": 12
}
]

@ -0,0 +1,146 @@
[
{
"range": [
1,
25
],
"m": 7,
"p": 13
},
{
"range": [
26,
60
],
"m": 8,
"p": 13
},
{
"range": [
61,
100
],
"m": 9,
"p": 14
},
{
"range": [
101,
125
],
"m": 9,
"p": 15
},
{
"range": [
126,
160
],
"m": 10,
"p": 16
},
{
"range": [
161,
225
],
"m": 10,
"p": 17
},
{
"range": [
226,
325
],
"m": 11,
"p": 17
},
{
"range": [
326,
425
],
"m": 12,
"p": 17
},
{
"range": [
426,
525
],
"m": 13,
"p": 18
},
{
"range": [
526,
625
],
"m": 13,
"p": 19
},
{
"range": [
626,
725
],
"m": 14,
"p": 19
},
{
"range": [
726,
825
],
"m": 14,
"p": 20
},
{
"range": [
826,
925
],
"m": 15,
"p": 20
},
{
"range": [
926,
1025
],
"m": 15,
"p": 21
},
{
"range": [
1026,
1125
],
"m": 15,
"p": 22
},
{
"range": [
1126,
1225
],
"m": 15,
"p": 23
},
{
"range": [
1226,
1325
],
"m": 15,
"p": 24
},
{
"range": [
1326,
9999
],
"m": 16,
"p": 24
}
]

@ -0,0 +1,90 @@
[
{
"range": [
1,
10
],
"m": 5,
"p": 10
},
{
"range": [
11,
14
],
"m": 6,
"p": 13
},
{
"range": [
15,
16
],
"m": 6,
"p": 16
},
{
"range": [
17,
25
],
"m": 7,
"p": 16
},
{
"range": [
26,
29
],
"m": 7,
"p": 15
},
{
"range": [
30,
35
],
"m": 7,
"p": 14
},
{
"range": [
36,
40
],
"m": 7,
"p": 13
},
{
"range": [
41,
55
],
"m": 7,
"p": 12
},
{
"range": [
56,
65
],
"m": 7,
"p": 11
},
{
"range": [
66,
79
],
"m": 7,
"p": 10
},
{
"range": [
80,
100
],
"m": 6,
"p": 9
}
]

@ -0,0 +1,98 @@
[
{
"range": [
1,
10
],
"m": 3,
"p": 10
},
{
"range": [
11,
16
],
"m": 4,
"p": 11
},
{
"range": [
17,
22
],
"m": 5,
"p": 12
},
{
"range": [
23,
30
],
"m": 5,
"p": 13
},
{
"range": [
31,
40
],
"m": 6,
"p": 14
},
{
"range": [
41,
50
],
"m": 6,
"p": 15
},
{
"range": [
51,
60
],
"m": 7,
"p": 16
},
{
"range": [
61,
80
],
"m": 7,
"p": 17
},
{
"range": [
81,
99
],
"m": 8,
"p": 17
},
{
"range": [
100,
125
],
"m": 8,
"p": 18
},
{
"range": [
126,
150
],
"m": 9,
"p": 18
},
{
"range": [
151,
9999
],
"m": 9,
"p": 19
}
]

@ -0,0 +1,74 @@
[
{
"range": [
1,
5
],
"m": 6,
"p": 10
},
{
"range": [
6,
9
],
"m": 7,
"p": 13
},
{
"range": [
10,
15
],
"m": 7,
"p": 14
},
{
"range": [
16,
24
],
"m": 8,
"p": 15
},
{
"range": [
25,
30
],
"m": 8,
"p": 14
},
{
"range": [
31,
36
],
"m": 7,
"p": 13
},
{
"range": [
37,
40
],
"m": 7,
"p": 12
},
{
"range": [
41,
45
],
"m": 7,
"p": 11
},
{
"range": [
46,
49
],
"m": 6,
"p": 10
}
]

@ -0,0 +1,58 @@
[
{
"range": [
1,
1.5
],
"m": 6,
"p": 12
},
{
"range": [
2,
3.5
],
"m": 7,
"p": 16
},
{
"range": [
4,
5.5
],
"m": 7,
"p": 17
},
{
"range": [
6,
7.5
],
"m": 7,
"p": 16
},
{
"range": [
8,
9.5
],
"m": 6,
"p": 14
},
{
"range": [
10,
11.5
],
"m": 6,
"p": 12
},
{
"range": [
12,
15.5
],
"m": 5,
"p": 10
}
]

@ -1,130 +0,0 @@
{
"key": "dwarf",
"name": "Dwarf",
"stride": 6,
"adjective": "dwarven",
"common_traits": [
"Accustomed To The Dark",
"Bearded",
"Greed",
"Oathsworn",
"Shaped From Earth And Stone",
"Stout",
"Tough"
],
"starting_stats":
[
{
"range": [
1,
20
],
"m": 6,
"p": 13
},
{
"range": [
21,
30
],
"m": 7,
"p": 13
},
{
"range": [
31,
50
],
"m": 7,
"p": 14
},
{
"range": [
51,
76
],
"m": 8,
"p": 15
},
{
"range": [
77,
111
],
"m": 8,
"p": 16
},
{
"range": [
112,
151
],
"m": 9,
"p": 16
},
{
"range": [
152,
199
],
"m": 9,
"p": 17
},
{
"range": [
200,
245
],
"m": 10,
"p": 18
},
{
"range": [
246,
300
],
"m": 11,
"p": 17
},
{
"range": [
301,
345
],
"m": 11,
"p": 16
},
{
"range": [
346,
396
],
"m": 12,
"p": 15
},
{
"range": [
397,
445
],
"m": 11,
"p": 14
},
{
"range": [
446,
525
],
"m": 11,
"p": 13
},
{
"range": [
526,
600
],
"m": 10,
"p": 12
}
]
}

@ -1,161 +0,0 @@
{
"key": "elf",
"name": "Elf",
"stride": 8,
"adjective": "elven",
"common_traits": [
"Born Under The Silver Stars",
"Essence Of The Earth",
"Fair And Statuesque",
"First Born",
"Grief",
"Keen Sight"
],
"starting_stats":
[
{
"range": [
1,
25
],
"m": 7,
"p": 13
},
{
"range": [
26,
60
],
"m": 8,
"p": 13
},
{
"range": [
61,
100
],
"m": 9,
"p": 14
},
{
"range": [
101,
125
],
"m": 9,
"p": 15
},
{
"range": [
126,
160
],
"m": 10,
"p": 16
},
{
"range": [
161,
225
],
"m": 10,
"p": 17
},
{
"range": [
226,
325
],
"m": 11,
"p": 17
},
{
"range": [
326,
425
],
"m": 12,
"p": 17
},
{
"range": [
426,
525
],
"m": 13,
"p": 18
},
{
"range": [
526,
625
],
"m": 13,
"p": 19
},
{
"range": [
626,
725
],
"m": 14,
"p": 19
},
{
"range": [
726,
825
],
"m": 14,
"p": 20
},
{
"range": [
826,
925
],
"m": 15,
"p": 20
},
{
"range": [
926,
1025
],
"m": 15,
"p": 21
},
{
"range": [
1026,
1125
],
"m": 15,
"p": 22
},
{
"range": [
1126,
1225
],
"m": 15,
"p": 23
},
{
"range": [
1226,
1325
],
"m": 15,
"p": 24
},
{
"range": [
1326,
9999
],
"m": 16,
"p": 24
}
]
}

@ -1,99 +0,0 @@
{
"key": "man",
"name": "Man",
"stride": 7,
"adjective": "mannish",
"common_traits": [
],
"starting_stats":
[
{
"range": [
1,
10
],
"m": 5,
"p": 10
},
{
"range": [
11,
14
],
"m": 6,
"p": 13
},
{
"range": [
15,
16
],
"m": 6,
"p": 16
},
{
"range": [
17,
25
],
"m": 7,
"p": 16
},
{
"range": [
26,
29
],
"m": 7,
"p": 15
},
{
"range": [
30,
35
],
"m": 7,
"p": 14
},
{
"range": [
36,
40
],
"m": 7,
"p": 13
},
{
"range": [
41,
55
],
"m": 7,
"p": 12
},
{
"range": [
56,
65
],
"m": 7,
"p": 11
},
{
"range": [
66,
79
],
"m": 7,
"p": 10
},
{
"range": [
80,
100
],
"m": 6,
"p": 9
}
]
}

@ -1,114 +0,0 @@
{
"key": "orc",
"name": "Orc",
"stride": 7,
"adjective": "orcish",
"common_traits": [
"Cannibal",
"Cold Black Blood",
"Breeder",
"Fanged And Clawed",
"Loathsome And Twisted",
"Lynx-eyed, Like Burning Coals",
"Vile Language"
],
"starting_stats":
[
{
"range": [
1,
10
],
"m": 3,
"p": 10
},
{
"range": [
11,
16
],
"m": 4,
"p": 11
},
{
"range": [
17,
22
],
"m": 5,
"p": 12
},
{
"range": [
23,
30
],
"m": 5,
"p": 13
},
{
"range": [
31,
40
],
"m": 6,
"p": 14
},
{
"range": [
41,
50
],
"m": 6,
"p": 15
},
{
"range": [
51,
60
],
"m": 7,
"p": 16
},
{
"range": [
61,
80
],
"m": 7,
"p": 17
},
{
"range": [
81,
99
],
"m": 8,
"p": 17
},
{
"range": [
100,
125
],
"m": 8,
"p": 18
},
{
"range": [
126,
150
],
"m": 9,
"p": 18
},
{
"range": [
151,
9999
],
"m": 9,
"p": 19
}
]
}

@ -1,90 +0,0 @@
{
"key": "roden",
"name": "Roden",
"stride": 8,
"adjective": "roden",
"common_traits": [
"Aecer's Likeness",
"Coat Of Fur",
"Communal",
"Enlarged Incisors",
"Quick-blooded",
"Tail",
"Large Ears"
],
"starting_stats":
[
{
"range": [
1,
5
],
"m": 6,
"p": 10
},
{
"range": [
6,
9
],
"m": 7,
"p": 13
},
{
"range": [
10,
15
],
"m": 7,
"p": 14
},
{
"range": [
16,
24
],
"m": 8,
"p": 15
},
{
"range": [
25,
30
],
"m": 8,
"p": 14
},
{
"range": [
31,
36
],
"m": 7,
"p": 13
},
{
"range": [
37,
40
],
"m": 7,
"p": 12
},
{
"range": [
41,
45
],
"m": 7,
"p": 11
},
{
"range": [
46,
49
],
"m": 6,
"p": 10
}
]
}

@ -1,75 +0,0 @@
{
"key": "wolf",
"name": "Wolf",
"stride": 11,
"adjective": "wolfish",
"common_traits": [
"Crushing Jaws",
"Deep Fur",
"Great Lupine Form",
"Lupine Intellect",
"Long-legged",
"Wolf's Eyes",
"Wolf's Snout",
"Woodland Ear"
],
"starting_stats":
[
{
"range": [
1,
1.5
],
"m": 6,
"p": 12
},
{
"range": [
2,
3.5
],
"m": 7,
"p": 16
},
{
"range": [
4,
5.5
],
"m": 7,
"p": 17
},
{
"range": [
6,
7.5
],
"m": 7,
"p": 16
},
{
"range": [
8,
9.5
],
"m": 6,
"p": 14
},
{
"range": [
10,
11.5
],
"m": 6,
"p": 12
},
{
"range": [
12,
15.5
],
"m": 5,
"p": 10
}
]
}

@ -632,14 +632,14 @@
],
"desc": "So vile are they, Orcs will not hesitate to slay and eat their companions."
},
"Catalyst": {
"cost": 3,
"type": "die",
"Catamite": {
"cost": 1,
"type": "character",
"restrict": [
"mannish",
"special"
],
"desc": "You are the center of something great and terrible. Earn a persona point each time your actions cause a conflict between two powerful personas or organizations."
"desc": "Catamite has been chosen as the trait name to represent the openly homosexual characters in the Burning Wheel. Honestly it was a pejorative medieval term -- a slur. It is how society would refer to them, not necessarily how they refer to themselves. Any Character may be homosexual via the player's choice, but by taking the Catamite trait the player is acknowledging that his character is open about his orientation. The ramifications of such a decision in a conservative medieval society are grist for great game situations"
},
"Charging Blindly": {
"cost": 0,

@ -1,6 +1,4 @@
{
"stock": "troll",
"settings": {
"Wild Setting": {
"Born Wild": {
"time": 5,
@ -14,13 +12,23 @@
"traits": [
1
],
"common_traits": [
"Black Nails",
"Fangs",
"Night Blooded",
"Night Eyed (Troll)",
"Massive Stature (Troll)",
"Stone's Age",
"Tough (Troll)",
"Troll Skin",
"Voracious Carnivore"
],
"leads": [
"Pit"
],
"key_leads": [
"Pit Setting"
],
"born": true
]
},
"Bogey": {
"time": 7,
@ -245,13 +253,23 @@
"traits": [
2
],
"common_traits": [
"Black Nails",
"Fangs",
"Night Blooded",
"Night Eyed (Troll)",
"Massive Stature (Troll)",
"Stone's Age",
"Tough (Troll)",
"Troll Skin",
"Voracious Carnivore"
],
"leads": [
"Pit"
],
"key_leads": [
"Pit Setting"
],
"born": true
]
},
"Dweller": {
"time": 4,
@ -379,6 +397,17 @@
2,
"Vile Language"
],
"common_traits": [
"Black Nails",
"Fangs",
"Night Blooded",
"Night Eyed (Troll)",
"Massive Stature (Troll)",
"Stone's Age",
"Tough (Troll)",
"Troll Skin",
"Voracious Carnivore"
],
"leads": [
"Pit",
"Cave"
@ -386,8 +415,7 @@
"key_leads": [
"Pit Setting",
"Cavedweller Setting"
],
"born": true
]
},
"Tortured": {
"time": 3,
@ -587,4 +615,3 @@
}
}
}
}

@ -1,6 +1,4 @@
{
"stock": "troll",
"resources": [
[
{
"name": "Rags",
"type": "gear",
@ -57,4 +55,3 @@
"rp": 5
}
]
}

@ -0,0 +1,82 @@
[
{
"range": [
1,
5
],
"m": 3,
"p": 11
},
{
"range": [
6,
12
],
"m": 4,
"p": 14
},
{
"range": [
13,
19
],
"m": 4,
"p": 17
},
{
"range": [
20,
27
],
"m": 4,
"p": 19
},
{
"range": [
28,
57
],
"m": 4,
"p": 20
},
{
"range": [
58,
80
],
"m": 4,
"p": 19
},
{
"range": [
81,
124
],
"m": 4,
"p": 18
},
{
"range": [
125,
213
],
"m": 5,
"p": 17
},
{
"range": [
214,
390
],
"m": 5,
"p": 16
},
{
"range": [
391,
712
],
"m": 6,
"p": 15
}
]

@ -1,100 +0,0 @@
{
"key": "troll",
"name": "Troll",
"stride": 7,
"adjective": "trollish",
"common_traits": [
"Black Nails",
"Fangs",
"Night Blooded",
"Night Eyed (Troll)",
"Massive Stature (Troll)",
"Stone's Age",
"Tough (Troll)",
"Troll Skin",
"Voracious Carnivore"
],
"starting_stats":
[
{
"range": [
1,
5
],
"m": 3,
"p": 11
},
{
"range": [
6,
12
],
"m": 4,
"p": 14
},
{
"range": [
13,
19
],
"m": 4,
"p": 17
},
{
"range": [
20,
27
],
"m": 4,
"p": 19
},
{
"range": [
28,
57
],
"m": 4,
"p": 20
},
{
"range": [
58,
80
],
"m": 4,
"p": 19
},
{
"range": [
81,
124
],
"m": 4,
"p": 18
},
{
"range": [
125,
213
],
"m": 5,
"p": 17
},
{
"range": [
214,
390
],
"m": 5,
"p": 16
},
{
"range": [
391,
712
],
"m": 6,
"p": 15
}
]
}

@ -1,9 +1,6 @@
{
"stock": "man",
"settings": {
"Special Gifted Lifepaths": {
"Gifted Child": {
"born": true,
"time": 9,
"res": 4,
"leads": [
@ -1225,4 +1222,3 @@
}
}
}
}

@ -1,11 +1,7 @@
require 'deep_merge'
require_relative 'data/gold'
require_relative 'data/wizard'
require_relative 'data/dark_elf'
require_relative 'data/troll'
require_relative 'stock'
require_relative 'data/custom'
module Charred
class Data
@ -13,13 +9,11 @@ module Charred
include Charred::Wizard
include Charred::DarkElf
include Charred::Troll
include Charred::Custom
attr :data
def initialize
@data = {}
@data[:stocks] = {}
puts 'loading gold'
load_gold(@data)
@ -33,9 +27,6 @@ module Charred
puts 'loading trolls'
load_troll(@data)
puts 'loading custom stocks'
load_custom(@data)
@data[:traits] = @data[:traits].sort.to_h
@data[:skills] = @data[:skills].sort.to_h
end
@ -62,9 +53,5 @@ module Charred
[]
end
end
def json_get(filename)
JSON.parse(File.read(filename))
end
end
end

@ -1,27 +0,0 @@
require 'json'
module Charred
module Custom
def load_custom(data)
Dir.glob("data/custom/**/*") { |file|
if File.file?(file)
case File.extname(file)
when ".skills"
verbose_merge data[:skills], json_get(file)
when ".traits"
verbose_merge data[:traits], json_get(file)
when ".stock"
stock = Stock.new(json_get(file))
data[:stocks].deep_merge!({ stock.key => stock })
when ".lifepaths"
contents = json_get(file)
data[:lifepaths].ko_deep_merge!({ contents["stock"] => contents["settings"]})
when ".resources"
contents = json_get(file)
data[:resources].ko_deep_merge!({ contents["stock"] => contents["resources"]})
end
end
}
end
end
end

@ -12,12 +12,10 @@ module Charred
verbose_merge data[:traits], traits
file = File.read('data/dark_elf/lifepaths.json')
contents = JSON.parse(file)
lifepaths = contents["settings"]
lifepaths = JSON.parse(file)
file = File.read("data/dark_elf/resources.json")
contents = JSON.parse(file)
resources = contents["resources"]
resources = JSON.parse(file)
data[:resources]['elf'] += resources
elf = data[:lifepaths]['elf']

@ -1,5 +1,4 @@
require 'json'
require_relative '../stock'
module Charred
module Gold
@ -12,29 +11,28 @@ module Charred
lifepaths = {}
resources = {}
stocks = {}
stat_pts = {}
gold_stocks = ['dwarf', 'elf', 'man', 'orc', 'roden', 'wolf']
stocks = ['dwarf', 'elf', 'man', 'orc', 'roden', 'wolf']
gold_stocks.each do |stock|
stocks.each do |stock|
file = File.read("data/gold/lifepaths/#{stock}.json")
contents = JSON.parse(file)
lifepaths[stock] = contents["settings"]
lifepaths[stock] = JSON.parse(file)
file = File.read("data/gold/resources/#{stock}.json")
contents = JSON.parse(file)
resources[stock] = contents["resources"]
resources[stock] = JSON.parse(file)
file = File.read("data/gold/stocks/#{stock}.json")
stocks[stock] = Stock.new(JSON.parse(file))
file = File.read("data/gold/starting_stat_pts/#{stock}.json")
stat_pts[stock] = JSON.parse(file)
end
data.merge!({
:stocks => stocks,
:skills => skills,
:traits => traits,
:lifepaths => lifepaths,
:resources => resources,
:stocks => stocks
:stat_pts => stat_pts
})
end
end

@ -1,9 +1,10 @@
require 'json'
require_relative '../stock'
module Charred
module Troll
def load_troll(data)
data[:stocks] << 'troll'
file = File.read('data/troll/skills.json')
skills = JSON.parse(file)
verbose_merge data[:skills], skills
@ -13,18 +14,16 @@ module Charred
verbose_merge data[:traits], traits
file = File.read('data/troll/lifepaths.json')
contents = JSON.parse(file)
lifepaths = contents["settings"]
lifepaths = JSON.parse(file)
data[:lifepaths]['troll'] = lifepaths
file = File.read("data/troll/resources.json")
contents = JSON.parse(file)
resources = contents["resources"]
resources = JSON.parse(file)
data[:resources]['troll'] = resources
file = File.read("data/troll/stock.json")
stock = JSON.parse(file)
data[:stocks]['troll'] = Stock.new(stock)
file = File.read("data/troll/starting_stat_pts.json")
stats = JSON.parse(file)
data[:stat_pts]['troll'] = stats
end
end
end

@ -4,8 +4,7 @@ module Charred
module Wizard
def load_wizard(data)
file = File.read('data/wizard/lifepaths.json')
contents = JSON.parse(file)
wizard_data = contents["settings"]
wizard_data = JSON.parse(file)
file = File.read('data/wizard/skills.json')
wizard_skills = JSON.parse(file)

@ -1,34 +0,0 @@
module Charred
class Stock
@@default_stride = 7
attr :key
attr :name
attr :stride
attr :common_traits
attr :starting_stats
def initialize(h)
@key = h["key"]
@name = h["name"] || @key
@stride = h["stride"] || @@default_stride
@adjective = h["adjective"] || @key+"ish"
@common_traits = h["common_traits"]
@starting_stats = h["starting_stats"]
end
def as_json(options = {})
{
"key" => @key,
"name" => @name,
"stride" => @stride,
"adjective" => @adjective,
"common_traits" => @common_traits,
"starting_stats" => @starting_stats
}
end
def to_json(*a)
as_json.to_json(*a)
end
end
end

@ -0,0 +1,67 @@
a.panel-title:link,.panel-title a:link { text-decoration: none; }
a.panel-title:visited,.panel-title a:visited { text-decoration: none; }
a.panel-title:hover,.panel-title a:hover { text-decoration: underline; }
a.panel-title:active,.panel-title a:active { text-decoration: underline; }
input.editable-name {
color: #333;
display: inline;
width: 12em;
}
input.editable-line {
color: #333;
}
input.editable-num {
color: #333;
display: inline;
width: 3em;
}
input.editable-name.not-editing {
background: #F5F5F5;
font-weight: bold;
}
input.editable-line.not-editing {
background: #F5F5F5;
}
input.editable-num.not-editing {
background: #F5F5F5;
}
.add-skills-traits-container {
margin-top: 1em;
margin-bottom: 1em;
}
.horizontal-input-pair {
display: flex;
align-items: center;
gap: 0.5em;
}
.horizontal-input-pair label {
margin-bottom: 0;
}
.horizontal-input-pair input {
flex-grow: 1;
}
div.skill-even {
}
div.skill-odd {
background: #F5F5F5;
}
tr.trait-even {
background: lightgray;
}
tr.trait-odd {
background: #F5F5F5;
}
textarea.trait-desc {
height: 5em;
width: 98%;
margin: 1em;
}
table.traits{
width: 100%;
}
table.traits input.editable-name {
width: 100%;
}

@ -2,7 +2,7 @@ function loadCurrentCharacterFromStruct($scope, charStruct, burningData, appropr
$scope.name = charStruct.name;
$scope.gender = charStruct.gender;
$scope.stock = charStruct.stock;
$scope.ensureStockLoaded($scope.stock).then(() => {
// Appropriate weapons must be loaded before calculateLifepathSkills is called.
if(serverSettings.storageType != 'server'){
appropriateWeapons.appropriateWeapons = charStruct.approp_weapons;
@ -173,8 +173,7 @@ function loadCurrentCharacterFromStruct($scope, charStruct, burningData, appropr
$scope.attributeModifierQuestionResults = loadAttributeModifierQuestionResultsFromSave($scope, charStruct.attr_mod_questions);
$scope.brutalLifeWithdrawn = charStruct.brutal_life_withdrawn;
$scope.$digest();
});
}
function convertCurrentCharacterToStruct($scope, appropriateWeapons) {

@ -1,4 +1,4 @@
var DEBUG = false;
/**** Class Settings (Angular Service) ****/
function Settings() {
this.enforceLifepathReqts = true;
@ -218,14 +218,13 @@ function CharacterStorageService($http) {
/* Load character names from server */
this.loadCharacterNames = function(){
fetch("/list_chars/user1")
.then((response) => response.json())
.then((data) => {
$http.get("/list_chars/user1", {'timeout': 3000} ).
success(function(data,status,headers,config){
myself.characterIdAndNames = data;
console.log("Loaded saved character names");
})
.catch((error) => {
console.log("Error: Loading saved character names from server failed: "+error);
}).
error(function(data,status,headers,config){
console.log("Error: Loading saved character names from server failed: HTTP code " + status + ": " + data);
});
}
@ -237,10 +236,6 @@ function CharacterStorageService($http) {
/**** Class BurningDataService (Angular Service) ****/
// This service is used to load the lifepaths, skills, traits, etc. from the server.
function BurningDataService($http) {
// Used to reference the object from within functions and callbacks
var myself = this;
/* JSON Data structure representing lifepaths. The structure is:
stock:
setting_name:
@ -268,88 +263,122 @@ function BurningDataService($http) {
// A hash of StartingStatPoints objects keyed by stock.
this.startingStatPts = {};
/* Loading of stocks, skills, and traits begins on initializing the service
/* Load stocks from server */
this.whenStocksLoaded = fetch("/stocks")
.then((response) => response.json())
.then((data) => {
if(DEBUG) {
console.log("Loaded stock data:");
console.log(data);
this.dataSetsLoaded = 0;
// Total data sets:
// lifepaths: 7 (man, dwarf, elf, orc, roden, wolf, troll)
// stat points: 7 (man, dwarf, elf, orc, roden, wolf, troll)
// skills
// traits
// resources: 7 (man, dwarf, elf, orc, roden, wolf. troll)
// TOTAL: 23
this.totalDataSets = 23;
this.onAllDatasetsLoaded = null;
this.registerOnAllDatasetsLoaded = function(callback){
if ( this.dataSetsLoaded >= this.totalDataSets ){
callback();
}
myself.stocks = data;
for (var stock of Object.keys(data)) {
myself.startingStatPts[stock] = new StartingStatPoints(myself.stocks[stock].starting_stats);
this.onAllDatasetsLoaded = callback;
}
})
.catch((error) => {
console.log("Error: Getting stocks from server failed: "+error);
});
/* Load skills from server */
this.whenSkillsLoaded = fetch("/skills")
.then((response) => response.json())
.then((data) => {
myself.skills = data;
if(DEBUG) {
console.log("Loaded skill data:");
console.log(data);
this.datasetLoaded = function(){
this.dataSetsLoaded += 1;
if ( this.onAllDatasetsLoaded && (this.dataSetsLoaded >= this.totalDataSets) ){
this.onAllDatasetsLoaded();
}
if ( this.dataSetsLoaded > this.totalDataSets){
console.log("Error: the totalDataSets setting in BurningDataService is too low! This will cause wierd errors. Please adjust it");
}
})
.catch((error) => {
console.log("Error: Getting skills from server failed: "+error);
});
/* Load traits from server */
this.whenTraitsLoaded = fetch("/traits")
.then((response) => response.json())
.then((data) => {
myself.traits = data;
if(DEBUG) {
console.log("Loaded trait data:");
console.log(data);
}
})
.catch((error) => {
console.log("Error: Getting traits from server failed: "+error);
});
/* Lifepaths and resources defer until their stock is selected */
var stocks = ["man", "dwarf", "elf", "orc", "roden", "wolf", "troll"];
var myself = this;
this.whenLifePathsLoadedForStock = {};
/* Load lifepaths from server */
this.loadLifepathsForStock = function(stock){
return myself.whenLifePathsLoadedForStock[stock] = fetch("/lifepaths/" + stock)
.then((response) => response.json())
.then((data) => {
var loadLifepathsForStock = function(stock){
if( ! isValidStock(stock) ){
console.log("Loading lifepaths failed: asked to load lifepaths for invalid stock " + stock);
return
}
$http.get("/lifepaths/" + stock, {'timeout': 3000} ).
success(function(data,status,headers,config){
myself.lifepaths[stock] = data;
if(DEBUG) {
console.log("Loaded "+stock+" lifepaths:");
console.log(data);
myself.datasetLoaded();
console.log("Loaded "+stock+" lifepaths. " + Object.keys(myself.lifepaths).length + " settings");
}).
error(function(data,status,headers,config){
myself.datasetLoaded();
console.log("Error: Getting "+stock+" lifepaths from server failed: HTTP code " + status + ": " + data);
});
}
})
.catch((error) => {
console.log("Error: Getting "+stock+" lifepaths from server failed: "+error);
/* Load starting stat points table from server */
var loadStartingStatPtsForStock = function(stock){
if( ! isValidStock(stock) ){
console.log("Loading starting stat points failed: asked to load pts for invalid stock " + stock);
return
}
$http.get("/starting_stat_pts/" + stock, {'timeout': 3000} ).
success(function(data,status,headers,config){
myself.startingStatPts[stock] = new StartingStatPoints(data);
myself.datasetLoaded();
console.log("Loaded "+stock+" starting stat points. ");
}).
error(function(data,status,headers,config){
myself.datasetLoaded();
console.log("Error: Getting "+stock+" stat points from server failed: HTTP code " + status + ": " + data);
});
};
/* Load resources from server */
this.whenResourcesLoadedForStock = {};
this.loadResourcesForStock = function(stock){
return myself.whenResourcesLoadedForStock[stock] = fetch("/resources/" + stock)
.then((response) => response.json())
.then((data) => {
}
/* Load starting stat points table from server */
var loadResourcesForStock = function(stock){
if( ! isValidStock(stock) ){
console.log("Loading resources failed: asked to load for invalid stock " + stock);
return
}
$http.get("/resources/" + stock, {'timeout': 3000} ).
success(function(data,status,headers,config){
myself.resources[stock] = data;
if(DEBUG) {
console.log("Loaded "+stock+" resources:");
console.log(data);
myself.datasetLoaded();
console.log("Loaded "+stock+" resources. ");
}).
error(function(data,status,headers,config){
myself.datasetLoaded();
console.log("Error: Getting "+stock+" stat points from server failed: HTTP code " + status + ": " + data);
});
}
})
.catch((error) => {
console.log("Error: Getting "+stock+" resources from server failed: "+error);
for (var i = 0; i < stocks.length; i++) {
loadLifepathsForStock(stocks[i]);
loadStartingStatPtsForStock(stocks[i]);
loadResourcesForStock(stocks[i]);
}
/* Load skills from server */
$http.get("/skills", {'timeout': 3000} ).
success(function(data,status,headers,config){
myself.skills = data;
myself.datasetLoaded();
console.log("Loaded skills. ");
}).
error(function(data,status,headers,config){
myself.datasetLoaded();
console.log("Error: Getting skills from server failed: HTTP code " + status + ": " + data);
});
/* Load traits from server */
$http.get("/traits", {'timeout': 3000} ).
success(function(data,status,headers,config){
myself.traits = data;
myself.datasetLoaded();
console.log("Loaded traits. ");
}).
error(function(data,status,headers,config){
myself.datasetLoaded();
console.log("Error: Getting traits from server failed: HTTP code " + status + ": " + data);
});
};
}
/**** End BurningDataService ****/

@ -61,6 +61,7 @@ burningModule.config(function($routeProvider) {
when('/', {controller: BurningCtrl, templateUrl:'/main_partial'}).
when('/config', {controller: ConfigCtrl, templateUrl:'/config_partial'}).
when('/help', {controller: ConfigCtrl, templateUrl:'/help_partial'}).
when('/stocked', {controller: StockedCtrl, templateUrl:'/stocked_partial'}).
otherwise({redirectTo:'/'});
});
@ -160,7 +161,7 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
}
};
// Setting names for use in the Add Lifepath section
$scope.settingNames = [];
$scope.settingNames = ["Loading..."]
$scope.currentSettingLifepathNames = [];
// The currently selected lifepath
$scope.currentSettingLifepath = "Loading...";
@ -206,6 +207,12 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
// Character name
$scope.name = "";
// Character stock. One of man, dwarf, orc, elf
if ( ! isValidStock(stock) ){
console.log("Invalid stock '"+stock+"' passed to BurningCtrl.initialize. Defaulting to man");
stock = "man";
}
$scope.stock = stock;
// Character id (server side id)
@ -296,7 +303,7 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
}
$scope.initialize();
$scope.initialize("man");
if ( characterStorage.currentCharacter ){
//console.log("Loading current character");
@ -349,6 +356,7 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
return result;
}
$scope.onGenderChange = function(){
if ($scope.name.length == 0) {
$scope.generateName();
@ -357,15 +365,8 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
}
$scope.onStockChange = function(){
if(!$scope.stock) return;
if(!$scope.stockSelected) { // Removes 'select a stock' after the first selection
$scope.stocks.shift();
$scope.stockSelected = true;
}
$scope.ensureStockLoaded($scope.stock).then(() => {
var oldName = $scope.name;
// Make a blank character sheet
$scope.initialize($scope.stock);
@ -374,25 +375,11 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
} else {
$scope.name = oldName;
}
calculateSettingNames($scope, burningData);
calculateCurrentSettingLifepathNames($scope, burningData);
calculateSpecialTraitsForDisplay($scope, burningData);
calculateGearSelectionLists($scope, burningData);
calculatePropertySelectionLists($scope, burningData);
$scope.$digest();
});
}
$scope.ensureStockLoaded = function(stock) {
let loadPromises = [];
if(!burningData.lifepaths[stock]) {
loadPromises.push(burningData.loadLifepathsForStock(stock));
}
if(!burningData.resources[stock]) {
loadPromises.push(burningData.loadResourcesForStock(stock));
}
return Promise.all(loadPromises);
};
$scope.onSettingChange = function(){
@ -417,10 +404,8 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
calculateUnspentSkillPoints($scope);
}
burningData.whenStocksLoaded.then(() => {
$scope.stocks = [{ name: "Select a stock" }, ...Object.values(burningData.stocks)];
$scope.stockSelected = false;
$scope.$digest();
burningData.registerOnAllDatasetsLoaded(function(){
onLifepathsLoad($scope, burningData);
});
$scope.$on('$locationChangeStart', function(event, nextUrl, currentUrl) {
@ -758,18 +743,13 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
return;
$scope.attributeShade[attrName] = 'G';
if('Grief' == attrName) {
$scope.attributeShade['Spite'] = 'G';
}
}
else if (attr.shade == 'G'){
$scope.attributeShade[attrName] = 'B';
if('Spite' == attrName) {
$scope.attributeShade['Grief'] = 'B';
}
}
else
console.log("Error: changing shade of attribute failed: unknown shade " + stat.shade);
}
$scope.incrementSkill = function(skill){
@ -950,7 +930,7 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
for(var key in burningData.skills){
if ( !(key in $scope.lifepathSkills) && !(key in $scope.generalSkills) ){
var displaySkill = burningData.skills[key];
if ( !displaySkill.stock || restrictionStockToValidStock(burningData.stocks, displaySkill.stock) == $scope.stock ) {
if ( !displaySkill.stock || restrictionStockToValidStock(displaySkill.stock) == $scope.stock ) {
result.push(key);
}
}
@ -1045,9 +1025,20 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
return {"shade" : "", "exp" : 10 - $scope.statsByName["Will"].exp() + bonus};
}
else if ( "Stride" == name ){
// This is a hack: if stock is unselected, use 0 for stride to not throw error; it shouldn't be displayed anyway
var stride = $scope.stock ? burningData.stocks[$scope.stock].stride : 0;
var stride = 0;
if( $scope.stock == 'dwarf' )
stride = 6;
else if( $scope.stock == 'elf' )
stride = 8;
else if( $scope.stock == 'roden' )
stride = 8;
else if( $scope.stock == 'wolf' )
stride = 11;
else
stride = 7;
stride += bonus;
return {"shade" : "", "exp" : stride};
}
else if ( "Circles" == name ){
@ -1115,8 +1106,7 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
}
else if ( "Spite" == name ){
var spite = computeModifiers(name);
// If Grief is grey, the shadeshift cost has already been payed in the Grief exponent calculation
if($scope.attributeShade[name] == 'G' && $scope.attributeShade["Grief"] == 'B'){
if($scope.attributeShade[name] == 'G'){
spite -= 5;
}
spite += bonus;
@ -1200,6 +1190,30 @@ function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapo
return list;
}
/*
$scope.specialTraitsForDisplay = function(){
var list = [];
for(var traitName in burningData.traits) {
var trait = burningData.traits[traitName];
if ('restrict' in trait){
if ( trait.restrict.indexOf(validStockToRestrictionStock($scope.stock)) >= 0 &&
(trait.restrict.indexOf("special") >= 0 || trait.restrict.indexOf("character") >= 0) ){
list.push(new DisplayTrait(traitName, burningData.traits));
}
} else {
// No restriction! As long as cost > 0 (cost 0 is for traits with no cost; not purchaseable)
if ( trait.cost > 0 ) {
list.push(new DisplayTrait(traitName, burningData.traits));
}
}
}
return list;
}
*/
$scope.addLifepathTrait = function(traitName){
if ( $scope.unspentTraitPoints < 1 && $scope.enforcePointLimits )
@ -1937,31 +1951,63 @@ function calculateAge($scope){
$scope.age = age;
}
function isSubsetting(setting) { return !Object.values(setting).some((lp) => lp.born); }
function calculateSettingNames($scope, burningData){
let stockSettings = burningData.lifepaths[$scope.stock];
$scope.settingNames = Object.keys(stockSettings);
var settingNames = null;
var lastCurrentSetting = $scope.currentSetting;
if ($scope.enforceLifepathReqts){
if ( $scope.selectedLifepaths.length == 0 ){
if ( ! $scope.enforceLifepathReqts ) {
// Display all settings and subsettings
settingNames = [];
for(key in burningData.lifepaths[$scope.stock]){
settingNames.push(key);
}
}
else if ( $scope.selectedLifepaths.length == 0 ){
// All settings are allowed. Subsettings have no Born lifepath so don't include them.
$scope.settingNames = $scope.settingNames.filter(s => !isSubsetting(stockSettings[s]));
settingNames = [];
for(key in burningData.lifepaths[$scope.stock]){
if( key.toLowerCase().indexOf("subsetting") < 0 ){
settingNames.push(key);
}
}
}
else {
// Only settings that are leads from the last lifepath are allowed
var lastLifepath = $scope.selectedLifepaths[$scope.selectedLifepaths.length-1];
settingNames = [];
var all = Object.keys(burningData.lifepaths[$scope.stock]);
for(var i = 0; i < all.length; i++){
//console.log("calculateSettingNames: checking if '"+all[i]+"' is allowed");
var setting = all[i];
if ( lastLifepath.setting == setting ){
settingNames.push(setting);
continue;
}
if ( lastLifepath.leads ){
for(var j = 0; j < lastLifepath.keyLeads.length; j++){
var lead = lastLifepath.keyLeads[j];
//console.log("calculateSettingNames: checking lead: '"+lead+"' is allowed");
// Doing this filtering instead of [lastLifepath.setting, ...lastLifepath.keyLeads] for two reasons:
// only presents settings present in this server and maintains relative order.
$scope.settingNames = $scope.settingNames.filter(s => s == lastLifepath.setting
|| (Array.isArray(lastLifepath.keyLeads) && lastLifepath.keyLeads.includes(s)));
if( setting == lead ){
settingNames.push(setting);
}
}
}
}
}
var currentSettingNeedsUpdate = !$scope.settingNames.includes(lastCurrentSetting);
$scope.settingNames = settingNames;
var currentSettingNeedsUpdate = true;
for(var i = 0; i < $scope.settingNames.length; i++){
if( $scope.settingNames[i] == lastCurrentSetting){
currentSettingNeedsUpdate = false;
break;
}
}
if ( currentSettingNeedsUpdate && $scope.settingNames.length > 0 ){
$scope.currentSetting = $scope.settingNames[0];
@ -1974,6 +2020,13 @@ function calculatePTGS($scope) {
}
function isBornLifepath(lifepathName) {
return lifepathName.indexOf("Born") >= 0 ||
lifepathName == "Son Of A Gun" ||
lifepathName == "Gifted Child";
}
function calculateCurrentSettingLifepathNames($scope, burningData){
var currentSettingLifepathNames = null;
@ -1985,7 +2038,7 @@ function calculateCurrentSettingLifepathNames($scope, burningData){
if ( $scope.selectedLifepaths.length == 0 ){
// Only "Born" lifepaths are allowed
for(var i = 0; i < all.length; i++){
if ( burningData.lifepaths[$scope.stock][$scope.currentSetting][all[i]].born ){
if ( isBornLifepath(all[i]) ){
currentSettingLifepathNames.push(all[i]);
}
}
@ -1995,7 +2048,7 @@ function calculateCurrentSettingLifepathNames($scope, burningData){
for(var j = 0; j < lifepathNames.length; j++){
var lifepathName = lifepathNames[j];
if ( burningData.lifepaths[$scope.stock][$scope.currentSetting][lifepathName].born )
if ( isBornLifepath(lifepathName) )
continue;
var rexpr = burningData.lifepaths[$scope.stock][$scope.currentSetting][lifepathName].requires_expr
@ -2276,7 +2329,7 @@ function setCommonTraits($scope, burningData){
if( $scope.selectedLifepaths.length == 0 )
return;
var common = burningData.stocks[$scope.stock].common_traits;
var common = $scope.selectedLifepaths[0].commonTraits;
if(common.length > 0){
for(var j = 0; j < common.length; j++){
var name = common[j];
@ -2708,7 +2761,7 @@ function calculateSpecialTraitsForDisplay($scope, burningData){
var trait = burningData.traits[traitName];
if ('restrict' in trait){
if ( trait.restrict.indexOf(validStockToRestrictionStock(burningData.stocks, $scope.stock)) >= 0 &&
if ( trait.restrict.indexOf(validStockToRestrictionStock($scope.stock)) >= 0 &&
(trait.restrict.indexOf("special") >= 0 || trait.restrict.indexOf("character") >= 0) ){
list.push(new DisplayTrait(traitName, burningData.traits));
}
@ -2756,12 +2809,44 @@ function calculateUnspentResourcePoints($scope){
$scope.unspentResourcePoints = unspentResourcePoints;
}
function restrictionStockToValidStock(stocks, stockAdjective){
return Object.values(stocks).findLast(s => s.adjective == stockAdjective).key;
}
function validStockToRestrictionStock(stocks, stockName){
return stocks[stockName].adjective;
function isValidStock(stock){
return stock == "man" || stock == "elf" || stock == "orc" || stock == "dwarf" || stock == "roden" || stock == "wolf" || stock =="troll";
}
function restrictionStockToValidStock(stock){
if ( stock == "mannish" )
return "man";
else if ( stock == "elven" )
return "elf";
else if ( stock == "orcish" )
return "orc";
else if ( stock == "dwarven" )
return "dwarf";
else if ( stock == "wolfish" )
return "wolf";
else if ( stock == "roden" )
return "roden";
else if ( stock == "trollish" )
return "troll";
}
function validStockToRestrictionStock(stock){
if ( stock == "man" )
return "mannish";
else if ( stock == "elf" )
return "elven";
else if ( stock == "orc" )
return "orcish";
else if ( stock == "dwarf" )
return "dwarven";
else if ( stock == "roden" )
return "roden";
else if ( stock == "wolf" )
return "wolfish";
else if ( stock == "troll" )
return "trollish";
}
function attributeModifyingQuestions($scope, attribute)

@ -1,5 +1,5 @@
var serverSettings = {
'versionString' : '3.0.0',
'versionString' : '2.3.0',
'storageType' : 'client',
'displayAttrMath' : 'false'
}

@ -0,0 +1,388 @@
// class TimeLogger {
// constructor() {
// this.logs = [];
// this.unlabelled_id = 0;
// this.start = this.previous = Date.now();
// this.log('start');
// }
// log(label) {
// var now = Date.now();
// var log = {
// 'when': now,
// 'label': label,
// 'elapsed': now - this.previous,
// 'total': now - this.start
// };
// this.logs.push(log);
// this.previous = now;
// return log;
// }
// }
// const timelogger = new TimeLogger;
// console.log(timelogger.logs[0]);
//
// console.log(timelogger.log("post-data"));
// Settings
function StockedSetting(name, charredSettingData) {
this.isSubsetting = false;
if(result = name.match(/(.*) setting/i)) {
this.name = result[1];
} else if(result = name.match(/(.*) subsetting/i)) {
this.name = result[1];
this.isSubsetting = true;
} else {
this.name = name;
}
this.lifepaths = [];
for (let name in charredSettingData) {
this.lifepaths.push(new StockedLifePath(name, charredSettingData[name]));
}
}
StockedSetting.prototype.addLifepath = function() {
this.lifepaths.push(new StockedLifePath(this.newLifepathName, {}));
this.newLifepathName = "";
}
StockedSetting.prototype.removeLifepath = function(index) {
this.lifepaths.splice(index, 1);
}
StockedSetting.prototype.toCharred = function() {
// let name = this.name + (this.isSubsetting ? " Subsetting" : " Setting";
let charred = {};
for(let lp of this.lifepaths) {
charred[lp.name] = lp.toCharred();
}
// common traits
// stride
return charred;
}
function StockedLifePath(name, charredPathData) {
if(charredPathData) {
this.name = name;
this.time = charredPathData.time;
this.res = charredPathData.res;
this.requires = charredPathData.requires;
this.restrict = charredPathData.restrict;
this.stat = new StockedLifePathStats(charredPathData.stat);
this.skills = new StockedLifePathSkills(charredPathData.skills);
this.traits = new StockedLifePathTraits(charredPathData.traits);
} else {
this.stat = new StockedLifePathStats();
this.skills = new StockedLifePathSkills();
this.traits = new StockedLifePathTraits();
}
this.leads_dict = {};
if(charredPathData.leads) {
for(let lead of charredPathData.leads) { this.leads_dict[lead] = true; }
}
this.leads = () => Object.keys(this.leads_dict).filter(l => this.leads_dict[l]);
}
StockedLifePath.prototype.toCharred = function() {
let charred = {};
charred.time = this.time;
charred.res = this.res;
charred.requires = this.requires;
charred.restrict = this.restrict;
charred.stat = this.stat.toCharred();
charred.skills = this.skills.toCharred();
charred.traits = this.traits.toCharred();
charred.leads = this.leads();
return charred;
}
function StockedLifePathStats(charredStatData) {
this.P = 0;
this.M = 0;
this.PM = 0;
if(charredStatData) {
for (let stat of charredStatData) {
if(stat[1].toUpperCase() == 'P') {
this.P += stat[0];
}
else if(stat[1].toUpperCase() == 'M') {
this.M += stat[0];
}
else if(stat[1].toUpperCase() == 'PM' || stat[0].toUpperCase() == 'MP') {
this.PM += stat[0];
}
}
}
}
StockedLifePathStats.prototype.toString = function() {
let strs = [];
if (this.P) { strs.push("+" + this.P + "P"); }
if (this.M) { strs.push("+" + this.M + "M"); }
if (this.PM) { strs.push("+" + this.PM + "P/M"); }
return strs.join(",");
};
StockedLifePathStats.prototype.toCharred = function() {
let charred = [];
if(this.P > 0) charred.push([this.P, "p"]);
if(this.M > 0) charred.push([this.M, "m"]);
if(this.PM > 0) charred.push([this.PM, "pm"]);
return charred;
}
function StockedLifePathSkills(charredLpSkillData) {
this.lpPoints = 0;
this.generalPoints = 0;
this.lpSkills = [];
if (charredLpSkillData) {
for (let skill of charredLpSkillData) {
if(skill.length == 1) {
this.lpPoints = skill[0];
}
if(skill.length >= 2) {
if(skill[1].toLowerCase() == "general") {
this.generalPoints += skill[0];
} else {
this.lpPoints += skill[0];
this.lpSkills = this.lpSkills.concat(skill.slice(1));
}
}
}
}
}
StockedLifePathSkills.prototype.removeSkill = function(index) {
this.lpSkills.splice(index, 1);
}
StockedLifePathSkills.prototype.addSkill = function(skill) {
this.lpSkills.push(skill);
}
StockedLifePathSkills.prototype.toString = function() {
let strs = [];
if (this.lpPoints) { strs.push(StockedUtil.pts(this.lpPoints, this.lpSkills)); }
if (this.generalPoints) { strs.push(StockedUtil.pts(this.generalPoints, ["General"])); }
return strs.join("; ");
};
StockedLifePathSkills.prototype.toCharred = function() {
let charred = [];
if(this.lpSkills.length > 0 || this.lpPoints > 0){
charred.push([this.lpPoints].concat(this.lpSkills));
}
if(this.generalPoints > 0)
charred.push([this.generalPoints, "General"]);
return charred;
}
function StockedLifePathTraits(charredTraitData) {
this.lpTraits = [];
if(charredTraitData) {
this.points = charredTraitData[0];
this.lpTraits = charredTraitData.slice(1);
}
}
StockedLifePathTraits.prototype.removeTrait = function(index) {
this.lpTraits.splice(index, 1);
}
StockedLifePathTraits.prototype.addTrait = function(skill) {
this.lpTraits.push(skill);
}
StockedLifePathTraits.prototype.toString = function() {
return StockedUtil.pts(this.points, this.lpTraits);
}
StockedLifePathTraits.prototype.toCharred = function() {
return [this.points].concat(this.lpTraits);
}
// Skills
function StockedSkill(name, charredSkillData) {
this.name = name;
this.magic = charredSkillData.magic ? true : false;
this.roots = {
Perception: false,
Will: false,
Forte: false,
Power: false,
Agility: false,
Speed: false,
};
if(charredSkillData.roots){
charredSkillData.roots.map(rootName => this.roots[rootName] = true);
}
this.stockSpecific = "TODO";
}
// Traits
function StockedTrait(name, charredTraitData) {
this.name = name;
this.cost = charredTraitData.cost;
this.type = charredTraitData.type;
this.desc = charredTraitData.desc;
// No logic for these yet
this.bonus = charredTraitData.bonus;
this.restrict = charredTraitData.restrict;
}
//Ctrl
var testscope;
function StockedCtrl($scope, $http, burningData) {
$scope.to_id = function(input) { return input.replaceAll(/\W/g, '_'); };
$scope.StockedUtil = StockedUtil;
$scope.TRAIT_TYPES = ["character", "die", "call_on"];
$scope.settings = [];
/* testing */
testscope = $scope;
$scope.general = {
'Name': 'Foo',
'Stride': 7,
'CommonTraits': [],
'selectedTrait': ''
};
// $scope.parseStock = function (stockData){
// let settings = [];
// for (let name in stockData) {
// settings.push(new StockedSetting(name, stockData[name]));
// }
// return settings;
// };
// $scope.parseSkills = function (skillsData){
// let skills = [];
// for (let name in skillsData) {
// skills.push(new StockedSkill(name, skillsData[name]));
// }
// return skills;
// };
// $scope.parseTraits = function (traitsData){
// let traits = [];
// for (let name in traitsData) {
// traits.push(new StockedTrait(name, traitsData[name]));
// }
// return traits;
// };
/* end testing */
/* Input/Output */
/* end Input/Output */
$scope.addSetting = function (){
this.settings.push(new StockedSetting(this.newSettingName, {}));
this.newSettingName = "";
};
$scope.removeSetting = function (index) {
this.settings.splice(index, 1);
};
$scope.addSkill = function (){
this.skills.push(new StockedSkill("New", {}));
};
$scope.removeSkill = function (index) {
this.skills.splice(index, 1);
};
$scope.addTrait = function (){
this.traits.push(new StockedTrait("New", {}));
};
$scope.removeTrait = function (index) {
this.traits.splice(index, 1);
};
$scope.collapseBody = function(data_target, $event) {
$(data_target).collapse("toggle");
if($event) { $event.stopPropagation(); }
};
$scope.editField = function($event, edit) {
$($event.target).toggleClass("not-editing");
};
burningData.registerOnAllDatasetsLoaded(function() {
onLifepathsLoad_Stocked($scope, burningData);
});
$scope.editLeads = function($event) {
let container = $($event.target).closest('.path-leads');
container.find(".path-leads-read").toggle(false);
container.find(".path-leads-write").toggle(true);
};
$scope.readLeads = function($event) {
let container = $($event.target).closest('.path-leads');
container.find(".path-leads-read").toggle(true);
container.find(".path-leads-write").toggle(false);
};
$scope.stocked_loadCharredModel = function() {
file = document.getElementById("stocked_charred_file");
file.files[0].text().then((text) => loadCharredModel(this, JSON.parse(text)));
};
$scope.stocked_downLoadCharredModel = function() {
model = serializeToCharredModel(this);
$http.post("/stocked_download", model).
success(function(data,status,headers,config){
console.log("huzzah, converting stocked model to charred succeeded. File URL: " + data);
var frame = document.getElementById("downloadframe");
if ( frame ){
frame.src = data;
}
}).
error(function(data,status,headers,config){
console.log("boo, converting stocked model to charred failed. File URL: " + data);
$scope.addAlert('tools', "converting stocked model to charred failed: " + data);
});
}
}
// Accepts an object, JSON text should be parsed first
function loadCharredModel($scope, model) {
let settings = [];
for (let name in model) {
settings.push(new StockedSetting(name, model[name]));
}
$scope.settings = settings;
$scope.$apply();
}
function serializeToCharredModel($scope) {
let model = {};
model.settings = {};
model.Name = $scope.general.Name;
$scope.settings.forEach((s) =>
model.settings[s.name + (s.isSubsetting ? " Subsetting" : " Setting")] = s.toCharred())
return JSON.stringify(model);
}
function onLifepathsLoad_Stocked($scope, burningData) {
// $scope.settings = $scope.parseStock(test_data);
// $scope.skills = $scope.parseSkills(test_skills_data);
// $scope.traits = $scope.parseTraits(test_traits_data);
$scope.charredTraits = Object.keys(burningData.traits);
$scope.charredSkills = Object.keys(burningData.skills);
}
var StockedUtil = {
"pluralize": function(num, thing, withE, nospace) {
str = "";
str += num;
if (!nospace) { str += ' '; }
str += thing;
if(num != 1) {
if (withE) { str += 'e'; }
str += 's';
}
return str;
},
"pts": function(num, entries) {
let str = StockedUtil.pluralize(num, "pt") + ": ";
if(Array.isArray(entries) && entries.length > 0) { str += entries.join(", "); }
else { str += "—" }
return str;
}//, "filter$properties": (key, value) => (key.startsWith('$') ? undefined : value)
};

@ -0,0 +1,42 @@
function EditableInputController($scope, $element, $attrs) {
console.log($scope);
console.log($element);
var ctrl = this;
ctrl.isEditing = false;
ctrl.handleModeChange = function() {
if (ctrl.isEditing) {
ctrl.onUpdate({value: ctrl.modelValue});
ctrl.modelValueCopy = ctrl.modelValue;
}
ctrl.isEditing = !ctrl.isEditing;
};
ctrl.reset = function() {
ctrl.modelValue = ctrl.modelValueCopy;
};
ctrl.$onInit = function() {
// Make a copy of the initial value to be able to reset it later
ctrl.modelValueCopy = ctrl.modelValue;
// Set a default inputType
if (!ctrl.inputType) {
ctrl.inputType = 'text';
}
};
}
export function register(module) {
console.log(module);
module.component('editableInput', {
templateUrl: '/stocked/editableInput_partial',
controller: EditableInputController,
bindings: {
modelValue: '<',
inputID: '<',
inputType: '@?',
onUpdate: '&'
}
});
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,23 @@
{
"Foo Setting": {
"Born Foo": {
"time": 1,
"res": 5,
"stat": [
[1, "pm"]
],
"leads": [
"Bar"
],
"key_leads": [
"Bar Setting"
],
"skills": [
[3, "General"]
],
"traits": [1],
"common_traits": []
}
},
"Bar Setting": {}
}

@ -3,6 +3,7 @@
<head>
<link href='/css/bootstrap.min.css' rel='stylesheet' type='text/css'>
<link href='/css/style.css' rel='stylesheet' type='text/css'>
<link href='/css/stocked.css' rel='stylesheet' type='text/css'>
<script src='/js/server_settings.js'></script>
<script src='/js/angular.min.js'></script>
<script src='/js/angular-resource.js'></script>
@ -14,11 +15,12 @@
<script src='/js/burning-service.js'></script>
<script src='/js/burning-modal.js'></script>
<script src='/js/burning-serialize.js'></script>
<script src='/js/stocked.js'></script>
<script src='/js/burning.js'></script>
<title>Charred - The Burning Wheel Gold Character Burner</title>
</head>
<body>
<a href="https://git.obscuritus.ca:3000/danwizard208/charred-gold/" class="github-corner" target="_blank" aria-label="View source on GitTea"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
<a href="https://github.com/modality/charred-black" class="github-corner" target="_blank" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
<div class='container' ng-controller='BurningCtrl'>
<nav class='navbar navbar-default' role='navigation'>
<div class='navbar-header'>
@ -34,6 +36,9 @@
<li>
<a target='_blank' href='https://forums.burningwheel.com/t/charred-an-unofficial-online-bwg-character-burner/14299'>Forum Thread</a>
</li>
<li>
<a href='#/stocked'>Stocked</a>
</li>
</ul>
</nav>
<div ng-view=''></div>

@ -58,7 +58,7 @@
in the main view. At the end is a description of some tricks for specific stocks.
</p>
<p>
The source code for this version can be found on Gittea: <a href="https://git.obscuritus.ca:3000/danwizard208/charred-gold" target="_blank">Charred Gold source</a>.
The source code for this version can be found on Github: <a href="https://github.com/modality/charred-black" target="_blank">Charred Black source</a>.
</p>
<h2 id='tools'>Tools</h2>
<p>

@ -94,7 +94,14 @@
</strong>
</div>
<div class='col-md-2'>
<select class='form-control' ng-change='onStockChange()' ng-model='stock' ng-options='s.key as s.name for s in stocks'>
<select class='form-control' ng-change='onStockChange()' ng-model='stock'>
<option value='man'>Man</option>
<option value='dwarf'>Dwarf</option>
<option value='elf'>Elf</option>
<option value='orc'>Orc</option>
<option value='roden'>Roden</option>
<option value='wolf'>Great Wolf</option>
<option value='troll'>Troll</option>
</select>
</div>
<div class='col-md-1'>

@ -0,0 +1,377 @@
<div class='container' ng-controller='StockedCtrl'>
<h1 id='intro'>Stocked</h1>
<div class='well'>
Stocked (Stock&middot;ed)
<br>
<br>
<ol>
<li><i>adj.</i> Furnished with more than enough. <i>(ref https://www.vocabulary.com/dictionary/stocked)</i></li>
<li><i>n.</i> A portmanteau of <i>stock</i> and <i>editor</i>.</li>
</ol>
</div>
<div class='panel panel-default'>
<div class='panel-heading' >
<h4 class='panel-title'>
<a href='' ng-click='collapseBody("#collapse_tools")'>▸</a>
Tools
</h4>
</div>
<div class='panel-collapse collapse in' id='collapse_tools'>
<div class='panel-body'>
<div class='alert alert-danger alert-dismissable' ng-repeat="warn in alertsOfType('tools', 'warn')">
<button aria-hidden='true' class='close' ng-click="removeAlert('tools',warn)" type='button'>&times;</button>
{{warn}}
</div>
<div class='alert alert-success alert-dismissable' ng-repeat="warn in alertsOfType('tools', 'succ')">
<button aria-hidden='true' class='close' ng-click="removeAlert('tools',warn)" type='button'>&times;</button>
{{warn}}
</div>
<div class='container'>
<div class='row'>
<div class='col-md-3'>
<input type="file" id="stocked_charred_file"/>
<a href='' ng-click='stocked_loadCharredModel()'>
TODO: Load charred stock model
</a>
</div>
<div class='col-md-3'>
<a href='' ng-click='stocked_downloadCharredModel()'>
TODO: Download charred stock model
</a>
</div>
<div class='col-md-3'>
<a href='' ng-click='stocked_generateMarkdown()'>
TODO: Generate Markdown
</a>
</div>
</div>
</div>
</div>
</div>
<div class='panel-heading'>
<h4 class='panel-title'>
<a href='' ng-click='collapseBody("#collapse_general")'>▸</a>
General
</h4>
</div>
<div class='panel-collapse collapse in' id='collapse_general'>
<div class='panel-body'>
<label for='stock-name'>Name:</label>
<input class='form-control input-lg not-editing editable-name' name='stock-name' id='stock-name'
ng-model="general.Name" ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
<label for='stock-stride'>Stride:</label>
<input type="number" class='not-editing editable-num' name='stock-stride' id='stock-stride'
ng-model="general.Stride" ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
<br />
<div>
<span>Common traits: {{general.CommonTraits.join(", ")}}</span>
<select class='form-control' ng-model='general.selectedTrait' ng-options='t for t in charredTraits'></select>
</div>
<div>
<a href='' ng-click='general.CommonTraits.push(general.selectedTrait)'>Add trait</a>
</div>
<div>
<a href='' ng-click=''>TODO: Add new trait</a>
</div>
<br />
TODO: other things in General section?
</div>
</div>
<div class='panel-heading'>
<h4 class='panel-title'>
<a href='' ng-click='collapseBody("#collapse_settings")'>▸</a>
Settings
</h4>
</div>
<div class='panel-collapse collapse in' id='collapse_settings'>
<div class='panel-body'>
<span class="note-label">Note:</span>
<span class="note-content">
Settings will have " Setting" (or " Subsetting" for subsettings)
appended to the name in the generated charred model, which is how charred will display them.
</span>
<br />
<a ng-click='collapseBody(".collapse_all_settings", $event)' href=''>
collapse/expand all settings
</a>
<div class='list-group'>
<div ng-repeat="setting in settings" class='list-group-item'>
<div class='panel-heading'>
<a href='' class="panel-title" ng-click='collapseBody("#collapse_" + to_id(setting.name))'>▸</a>
<input class='form-control input-lg not-editing editable-name'
ng-model="setting.name" ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
<span class="panel-title" style="font-weight: bold;">
<input type="checkbox" ng-model="setting.isSubsetting" /> Subsetting?
</span>
<a href='' ng-click='removeSetting($index)'>[X]</a>
</div>
<div class='panel-collapse collapse in collapse_all_settings' id='collapse_{{to_id(setting.name)}}'>
<div class='panel-body'>
<div class="panel">
<div class="panel-body">
<span class="note-label">Note:</span>
<span class="note-content">
Prefix a lifepath's name with "Born " to have charred consider it a born lifepath;
i.e. selectable if and only if it is the first lifepath.
</span>
<br />
<a ng-click='collapseBody(".collapse_all_"+to_id(setting.name), $event)' href=''>
collapse/expand all paths in setting
</a>
<div class='container-fluid'>
<div class='row'>
<div class='h4 col-md-3'>Lifepath</div>
<div class='h4 col-md-1'>Time</div>
<div class='h4 col-md-1'>Res</div>
<div class='h4 col-md-3'>Stat</div>
<div class='h4 col-md-4'>Leads</div>
</div>
</div>
</div>
<div ng-repeat="path in setting.lifepaths">
<div class='panel-heading'>
<div class='container-fluid'>
<div class='row'>
<div class="col-md-3">
<a href='' class="panel-title" ng-click='collapseBody("#collapse_" + to_id(path.name))'>▸</a>
<input ng-model="path.name"
class='form-control input-lg not-editing editable-name'
ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
<a href='' ng-click='setting.removeLifepath($index)'>[X]</a>
</div>
<div class="col-md-1">
<input type="number" ng-model="path.time"
class='not-editing editable-num'
ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
<span>yrs</span>
</div>
<div class="col-md-1">
<input type="number" ng-model="path.res"
class='not-editing editable-num'
ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
</div>
<div class="col-md-3">
<label for="{{to_id(path.name)}}_M">M: </label>
<input type="number" ng-model="path.stat.M" id="{{to_id(path.name)}}_M"
class='not-editing editable-num'
ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
<label for="{{to_id(path.name)}}_P">P: </label>
<input type="number" ng-model="path.stat.P" id="{{to_id(path.name)}}_M"
class='not-editing editable-num'
ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
<label for="{{to_id(path.name)}}_PM">P/M: </label>
<input type="number" ng-model="path.stat.PM" id="{{to_id(path.name)}}_M"
class='not-editing editable-num'
ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
</div>
<div class='h5 col-md-4 path-leads'>
<div class="path-leads-read">
<span>Leads: </span><i>{{path.leads().join(", ")}}</i>
<a href='' ng-click='editLeads($event)'>Edit</a>
</div>
<div class="path-leads-write" hidden="hidden">
<div ng-repeat="setting in settings">
<input type="checkbox" id='{{to_id(path.name)}}-to-{{to_id(setting.name)}}'
ng-model='path.leads_dict[setting.name]' value='{{to_id(setting.name)}}' />
<label for='{{to_id(path.name)}}-to-{{to_id(setting.name)}}'>
{{setting.name}}
</label>
</div>
<a href='' ng-click='readLeads($event)'>Done</a>
</div>
</div>
</div>
</div>
</div>
<div class='panel-collapse collapse in collapse_all_{{to_id(setting.name)}}' id='collapse_{{to_id(path.name)}}'>
<div class='panel-body'>
<span ng-if='$first'>TODO: Born/common traits</span>
<div>
<b><i>Skills:</i></b>
<span ng-if='path.skills.lpSkills.length > 0' ng-click='editPoints($event)'>
{{StockedUtil.pluralize(path.skills.lpPoints, "pt")}}:
</span>
<div style="display: inline;" ng-repeat='skill in path.skills.lpSkills track by $index'>
{{skill}}<a href='' ng-click='path.skills.removeSkill($index)'>[X]</a><!--
--><span ng-if='!$last'>,</span></div><!--
--><span ng-if='path.skills.lpSkills.length > 0 && path.skills.generalPoints > 0'>;</span>
<span ng-if='path.skills.generalPoints > 0'>{{StockedUtil.pluralize(path.skills.generalPoints, "pt")}}: General</span>
</div>
<div>
<b><i>Traits:</i></b>
<span ng-if='path.traits.lpTraits.length == 0'>—</span>
<span ng-if='path.traits.lpTraits.length > 0' ng-click='editPoints($event)'>
{{StockedUtil.pluralize(path.traits.points, "pt")}}:
</span>
<div style="display: inline;" ng-repeat='trait in path.traits.lpTraits track by $index'>
{{trait}}<a href='' ng-click='path.traits.removeTrait($index)'>[X]</a><!--
--><span ng-if='!$last'>,</span></div>
</div>
<div ng-if="path.requires" class="horizontal-input-pair">
<label for="{{to_id(path.name)}}-requires"><b><i>Requires: </i></b></label>
<input ng-model="path.requires" id="{{to_id(path.name)}}-requires"
class='form-control not-editing editable-line'
ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
</div>
<div ng-if="path.restrict" class="horizontal-input-pair">
<label for="{{to_id(path.name)}}-restrict"><b><i>Restrictions: </i></b></label>
<input ng-model="path.restrict" id="{{to_id(path.name)}}-restrict"
class='form-control not-editing editable-line'
ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
</div>
<div class="add-skills-traits-container" class="container">
<div class='row'>
<div class='col-md-3'>
<select class='form-control' ng-model='path.selectedSkill' ng-options='s for s in charredSkills'></select>
</div>
<div class='col-md-1'>
<a href='' ng-click='path.skills.addSkill(path.selectedSkill)'>Add skill</a>
</div>
<div class='col-md-2'>
<a href='' ng-click=''>TODO: Add new skill</a>
</div>
<div class='col-md-3'>
<select class='form-control' ng-model='path.selectedTrait' ng-options='t for t in charredTraits'></select>
</div>
<div class='col-md-1'>
<a href='' ng-click='path.traits.addTrait(path.selectedTrait)'>Add trait</a>
</div>
<div class='col-md-2'>
<a href='' ng-click=''>TODO: Add new trait</a>
</div>
</div>
</div>
<span>WISHLIST: requires expression</span>
</div>
</div>
</div>
<div class='panel panel-default'>
<input class='form-control input-lg not-editing editable-name'
name='new-lifepath-name' id='new-lifepath-name'
ng-model="setting.newLifepathName" ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
<a href='' ng-click='setting.addLifepath()'>Add new lifepath</a>
</div>
</div>
</div>
</div>
</div>
<div class='panel panel-default'>
<input class='form-control input-lg not-editing editable-name'
name='new-setting-name' id='new-setting-name'
ng-model="newSettingName" ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
<a href='' ng-click='addSetting()'>Add new setting</a>
</div>
</div>
</div>
</div>
<div class='panel-heading'>
<h4 class='panel-title'>
<a href='' ng-click='collapseBody("#collapse_skills")'>▸</a>
Skills
</h4>
</div>
<div class='panel-collapse collapse in' id='collapse_skills'>
<div class='panel-body'>
<div class='container-fluid'>
<div class='row'>
<div class='h4 col-md-3'>Skill</div>
<div class='h4 col-md-1'>Sorcerous?</div>
<div class='h4 col-md-8'>Roots</div>
<%# <div class='h4 col-md-2'>Stock specific?</div> %>
</div>
</div>
<div ng-repeat='skill in skills' class='row' ng-class-even="'skill-even'" ng-class-odd="'skill-odd'">
<div class='h4 col-md-3'>
<input ng-model="skill.name"
class='form-control input-lg not-editing editable-name'
ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
</div>
<div class='h4 col-md-1'><input type="checkbox" ng-model='skill.magic'></div>
<div class='h4 col-md-8'>
<div class="skill-roots-write">
<span ng-repeat="(root,is) in skill.roots">
<input type="checkbox" id='{{to_id(skill.name)-to_id(root)}}' ng-model='is'/>
<label for='{{to_id(skill.name)-to_id(root)}}'> {{root}} </label>
</div>
</div>
</div>
</div>
<div class='row'>
<div class='h4 col-md-12'><a href='' ng-click='addSkill()'>Add new skill</a></div>
</div>
</div>
</div>
<div class='panel-heading'>
<h4 class='panel-title'>
<a href='' ng-click='collapseBody("#collapse_traits")'>▸</a>
Traits
</h4>
</div>
<div class='panel-collapse collapse in' id='collapse_traits'>
<div class='panel-body'>
<table class="traits">
<tr>
<th>Trait</th>
<th>Cost</th>
<th>Type</th>
<%# <div class='h4 col-md-2'>Bonus</div> %>
<%# <div class='h4 col-md-2'>Restrictions</div> %>
</tr>
<%# </div> %>
<tr ng-repeat-start='trait in traits' ng-class-even="'trait-even'" ng-class-odd="'trait-odd'">
<td>
<input ng-model="trait.name"
class='form-control input-lg not-editing editable-name'
ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
</td>
<td>
<input type="number" class='not-editing editable-num'
ng-model="trait.cost" ng-click="$event.stopPropagation()"
ng-focus='editField($event, true)' ng-blur='editField($event, false)' />
</td>
<td>
<select class='form-control' ng-model='trait.type' ng-options='t for t in TRAIT_TYPES'></select>
</td>
<td><a href='' ng-click='removeTrait($index)'>[X]</a></td>
</tr>
<%# <div class='h4 col-md-2'>WISHLIST</div> %>
<%# <div class='h4 col-md-2'>WISHLIST</div> %>
<tr ng-repeat-end ng-class-even="'trait-even'" ng-class-odd="'trait-odd'">
<td colspan="99"><textarea class='trait-desc' ng-model='trait.desc'></textarea></td>
</tr>
</table>
<div class='row'>
<div class='h4 col-md-12'><a href='' ng-click='addTrait()'>Add new trait</a></div>
</div>
</div>
</div>
</div>

@ -0,0 +1,6 @@
<span ng-if='$ctrl.label' for='{{$ctrl.inputID}}'>{{$ctrl.label}}</span>
<span ng-switch='$ctrl.isEditing'>
<input ng-switch-when="true" ng-model='$ctrl.modelValue' ng-blur='$ctrl.handleModeChange()'
id='{{$ctrl.inputID}}' type='{{$ctrl.inputType}}' class="editable-input editing">
<span ng-switch-default class="editable-input">{{$ctrl.modelValue}}</span>
</span>

@ -1 +0,0 @@
{"serialize_version":1,"name":"Algoric the Apologist","gender":"male","stock":"man","lifepaths":[["Peasant Setting","Born Peasant",0,null,null,null,[]],["College of Magic Setting","Supplicant",0,null,null,null,[]],["College of Magic Setting","Junior Student",0,null,null,null,[[1,"p"]]],["Outcast Subsetting","Rogue Wizard",0,null,null,null,[[1,"p"],[1,"m"]]],["Servitude And Captive Setting","Captive Of War",0,null,null,null,[]],["City Dweller Setting","Criminal",0,null,null,null,[[1,"pm"]]],["City Dweller Setting","Temple Acolyte",0,null,null,null,[[1,"m"]]],["Religious Subsetting","Priest",0,null,null,null,[[1,"m"]]]],"stats":[{"name":"Will","shade":"B","mentalPoints":5,"physicalPoints":0,"eitherPoints":1},{"name":"Perception","shade":"B","mentalPoints":5,"physicalPoints":0,"eitherPoints":0},{"name":"Power","shade":"B","mentalPoints":0,"physicalPoints":3,"eitherPoints":0},{"name":"Forte","shade":"B","mentalPoints":0,"physicalPoints":4,"eitherPoints":0},{"name":"Agility","shade":"B","mentalPoints":0,"physicalPoints":4,"eitherPoints":0},{"name":"Speed","shade":"B","mentalPoints":0,"physicalPoints":3,"eitherPoints":0}],"skills":{"lifepath":[{"name":"Animal Husbandry","lifepathPoints":1,"generalPoints":0},{"name":"Firebuilding","lifepathPoints":1,"generalPoints":0},{"name":"Read","lifepathPoints":1,"generalPoints":0},{"name":"Write","lifepathPoints":1,"generalPoints":0},{"name":"Ancient History","lifepathPoints":1,"generalPoints":0},{"name":"Circination","lifepathPoints":2,"generalPoints":0},{"name":"Illuminations","lifepathPoints":1,"generalPoints":0},{"name":"Astrology","lifepathPoints":2,"generalPoints":0},{"name":"Symbology","lifepathPoints":1,"generalPoints":0},{"name":"Sorcery","lifepathPoints":5,"generalPoints":0},{"name":"Inconspicuous","lifepathPoints":1,"generalPoints":0},{"name":"Graveyard-wise","lifepathPoints":1,"generalPoints":0},{"name":"Bloodletting","lifepathPoints":1,"generalPoints":0},{"name":"Ugly Truth","lifepathPoints":1,"generalPoints":0},{"name":"Apocalypse-wise","lifepathPoints":1,"generalPoints":0},{"name":"Enchanting","lifepathPoints":2,"generalPoints":0},{"name":"Alchemy","lifepathPoints":1,"generalPoints":0},{"name":"Cell-wise","lifepathPoints":1,"generalPoints":0},{"name":"Chain-wise","lifepathPoints":1,"generalPoints":0},{"name":"Streetwise","lifepathPoints":1,"generalPoints":0},{"name":"Intimidation","lifepathPoints":1,"generalPoints":0},{"name":"Knives","lifepathPoints":1,"generalPoints":0},{"name":"Climbing","lifepathPoints":1,"generalPoints":0},{"name":"Doctrine","lifepathPoints":1,"generalPoints":0},{"name":"Bureaucracy","lifepathPoints":1,"generalPoints":0},{"name":"Temple-wise","lifepathPoints":1,"generalPoints":0},{"name":"Oratory","lifepathPoints":1,"generalPoints":0},{"name":"Suasion","lifepathPoints":2,"generalPoints":0},{"name":"Ritual","lifepathPoints":1,"generalPoints":0},{"name":"Religious History","lifepathPoints":1,"generalPoints":0}],"general":[{"name":"Whip-wise","lifepathPoints":0,"generalPoints":1},{"name":"Herbalism","lifepathPoints":0,"generalPoints":2}]},"traits":[{"name":"Gifted"},{"name":"Second Sight"},{"name":"Fey Blood"},{"name":"Aura Of Fear"},{"name":"Obscure Aura"},{"name":"Poker Face"},{"name":"Faithful"}],"gear":[{"cost":1,"desc":"Clothes"},{"cost":1,"desc":"Traveling Gear"},{"cost":1,"desc":"Personal Effects"},{"cost":5,"desc":"Riding Mount Or Pack Animal"},{"cost":10,"desc":"Armor, Light Mail, Run Of The Mill Quality"},{"cost":14,"desc":"Spells"}],"property":[{"cost":10,"desc":"Property, A House"}],"relationships":[],"affiliations":[{"desc":"The Scurrilous Foes","importance":"small"}],"reputations":[{"desc":"An Escaped Prisoner","importance":"regional"}],"attr_mod_questions":{"Health":[{"question":"Was the character severely wounded in the past?","answer":true},{"question":"Is the character athletic and active?","answer":true}],"Steel":[{"question":"Has the character ever been severely wounded?","answer":true},{"question":"Has the character ever murdered or killed with his own hand more than once?","answer":true},{"question":"Has the character been raised in a competitive (but non-violent) culture - sports, debate, strategy games, courting?","answer":true}],"Faith":[{"question":"Is it only through God that you best serve your allies?","answer":false}]},"brutal_life_withdrawn":false,"approp_weapons":{}}

@ -1 +0,0 @@
{"name":"Algoric the Apologist","age":41,"stock":"man","lifepaths":["Born Peasant","Supplicant","Junior Student","Rogue Wizard","Captive Of War","Criminal","Temple Acolyte","Priest"],"stats":{"will":["B",6],"perception":["B",5],"power":["B",3],"forte":["B",4],"agility":["B",4],"speed":["B",3]},"attributes":{"mortal wound":["B",9],"reflexes":["B",4],"health":["B",5],"steel":["B",6],"hesitation":["",4],"stride":["",7],"circles":["B",3],"resources":["B",3],"faith":["B",3]},"skills":[["Animal Husbandry","B",3,false],["Firebuilding","B",2,false],["Read","B",2,false],["Write","B",2,false],["Ancient History","B",2,false],["Circination","B",2,false],["Illuminations","B",2,false],["Astrology","B",3,false],["Symbology","B",2,false],["Sorcery","B",5,false],["Inconspicuous","B",3,false],["Graveyard-wise","B",2,null],["Bloodletting","B",2,false],["Ugly Truth","B",2,false],["Apocalypse-wise","B",2,null],["Enchanting","B",2,false],["Alchemy","B",2,false],["Cell-wise","B",2,null],["Chain-wise","B",2,null],["Streetwise","B",2,false],["Intimidation","B",3,false],["Knives","B",2,false],["Climbing","B",1,false],["Doctrine","B",2,false],["Bureaucracy","B",3,false],["Temple-wise","B",2,null],["Oratory","B",3,false],["Suasion","B",4,false],["Ritual","B",2,false],["Religious History","B",2,false],["Whip-wise","B",2,false],["Herbalism","B",3,false]],"traits":[["Gifted","die"],["Second Sight","die"],["Fey Blood","die"],["Aura Of Fear","die"],["Obscure Aura","die"],["Poker Face","call_on"],["Faithful","die"],["Broken In","die"],["Hazed","die"],["Spooky","character"],["Claustrophobic","die"],["Cynical","character"],["Believer","die"],["Vested","die"]],"gear":["Clothes","Traveling Gear","Personal Effects","Riding Mount Or Pack Animal","Armor, Light Mail, Run Of The Mill Quality","Spells"],"property":["Property, A House"],"relationships":[],"reputations":["An Escaped Prisoner 2D"],"affiliations":["The Scurrilous Foes 1D"],"ptgs":{"su":3,"li":5,"mi":6,"se":7,"tr":8,"mo":9},"attr_mod_questions":{"Health":[{"question":"Does the character live in squalor and filth?","math_label":"(-1 Health)","modifier":-1},{"question":"Is the character frail or sickly?","math_label":"(-1 Health)","modifier":-1},{"question":"Was the character severely wounded in the past?","math_label":"(-1 Health)","modifier":-1,"answer":true},{"question":"Has the character been tortured and enslaved?","math_label":"(-1 Health)","modifier":-1},{"question":"Is the character athletic and active?","math_label":"(+1 Health)","modifier":1,"answer":true},{"question":"Does the character live in a really clean and happy place, like the hills in the Sound of Music?","math_label":"(+1 Health)","modifier":1}],"Steel":[{"question":"Has the character ever been severely wounded?","math_label":"(+1 Steel if combat lifepath taken/-1 Steel if not)","computeModifier":true,"answer":true},{"question":"Has the character ever murdered or killed with his own hand more than once?","math_label":"(+1 Steel)","modifier":1,"answer":true},{"question":"Has the character been tortured, enslaved or beaten terribly over time?","math_label":"(+1 Steel if Will is > 4, -1 Steel if Will < 4, +0 if Will is 4)","computeModifier":true},{"question":"Has the character lead a sheltered life, free of violence and pain?","math_label":"(-1 Steel)","modifier":-1},{"question":"Has the character been raised in a competitive (but non-violent) culture - sports, debate, strategy games, courting?","math_label":"(+1 Steel)","modifier":1,"answer":true},{"question":"Has the character given birth to a child?","math_label":"(+1 Steel)","modifier":1}],"Faith":[{"question":"Is God who you trust the most?","math_label":"(+1 Faith)","modifier":1},{"question":"When in danger, do you consult God for aid?","math_label":"(+1 Faith)","modifier":1},{"question":"Is it only through God that you best serve your allies?","math_label":"(+1 Faith)","modifier":1,"answer":false}]}}

File diff suppressed because one or more lines are too long

@ -1 +0,0 @@
{"name":"Tyastanarphen","age":676,"stock":"elf","lifepaths":["Citadel Born","Wanderer","Song Singer","Bard","Loremaster","Griever","Liar","Deceiver","Recluse"],"stats":{"will":["B",8],"perception":["G",6],"power":["B",6],"forte":["B",5],"agility":["B",5],"speed":["B",5]},"attributes":{"mortal wound":["B",11],"reflexes":["B",6],"health":["B",5],"steel":["B",9],"hesitation":["",2],"stride":["",8],"circles":["B",4],"resources":["B",1],"grief":["G",4],"spite":["B",9]},"skills":[["Elven Script","G",4,false],["Sing","B",4,false],["Song Of Paths And Ways","G",3,false],["Air Of Gates","B",4,false],["Foraging","G",3,false],["Road-wise","G",3,false],["Song Of Songs","B",4,false],["Song Of Soothing","B",4,false],["Lament Of Stars","B",4,false],["Song Of Merriment","B",4,false],["Lament Of Mourning","B",4,false],["Tract Of Enmity","B",4,false],["Oratory","B",6,false],["Conspicuous","B",4,false],["Lyre","B",3,false],["Flute","B",3,false],["Ancient And Obscure History","G",3,false],["Research","G",3,false],["Ballad Of History","G",3,false],["Lyric Of Law","G",3,false],["Canticle Of Years","B",4,false],["Rhyme Of Tongues","B",4,false],["Sorrow Of Truth","B",4,false],["Dark Elf-wise","G",3,null],["Falsehood","B",4,false],["Soothing Platitudes","B",4,false],["Persuasion","B",6,false],["Twisted Tongue","B",4,false],["Sleight Of Hand","B",4,false],["Disguise","B",3,false],["Inconspicuous","B",4,false],["Rhyme Of The Unraveller","B",4,false],["Ancient History","G",3,false],["Dwarf-wise","G",3,false],["Elven Politics-wise","G",3,null],["Cut Of The Quickened Mind","B",4,false],["Antiphon Union Training","B",5,true],["Almanac","G",3,false]],"traits":[["Grim","character"],["Glib","call_on"],["Oikofugic","character"],["Voice In The Crowd","character"],["Spite","character"],["Compulsive Liar","character"],["Deceptive","call_on"],["Vengeful","die"],["Born Under The Silver Stars","character"],["Essence Of The Earth","die"],["Fair And Statuesque","character"],["First Born","die"],["Grief","die"],["Keen Sight","die"]],"gear":["Morlin Armor, Light Mail","Long Knife","Tome Of Lore","Cloak Of Darkness"],"property":["Remote Refuge, Safe House"],"relationships":[],"reputations":[],"affiliations":[],"ptgs":{"su":3,"li":6,"mi":8,"se":9,"tr":10,"mo":11},"attr_mod_questions":{"Health":[{"question":"Does the character live in squalor and filth?","math_label":"(-1 Health)","modifier":-1,"answer":true},{"question":"Is the character frail or sickly?","math_label":"(-1 Health)","modifier":-1},{"question":"Was the character severely wounded in the past?","math_label":"(-1 Health)","modifier":-1,"answer":true},{"question":"Has the character been tortured and enslaved?","math_label":"(-1 Health)","modifier":-1,"answer":true},{"question":"Is the character athletic and active?","math_label":"(+1 Health)","modifier":1,"answer":true},{"question":"Does the character live in a really clean and happy place, like the hills in the Sound of Music?","math_label":"(+1 Health)","modifier":1}],"Steel":[{"question":"Has the character ever been severely wounded?","math_label":"(+1 Steel if combat lifepath taken/-1 Steel if not)","computeModifier":true,"answer":true},{"question":"Has the character ever murdered or killed with his own hand more than once?","math_label":"(+1 Steel)","modifier":1,"answer":true},{"question":"Has the character been tortured, enslaved or beaten terribly over time?","math_label":"(+1 Steel if Will is > 4, -1 Steel if Will < 4, +0 if Will is 4)","computeModifier":true,"answer":true},{"question":"Has the character lead a sheltered life, free of violence and pain?","math_label":"(-1 Steel)","modifier":-1},{"question":"Has the character been raised in a competitive (but non-violent) culture - sports, debate, strategy games, courting?","math_label":"(+1 Steel)","modifier":1,"answer":true},{"question":"Has the character given birth to a child?","math_label":"(+1 Steel)","modifier":1,"answer":true}],"Grief":[{"question":"Does the character's history include tragedy?","math_label":"(+1 Grief)","modifier":1,"answer":true},{"question":"Has the character lived among non-Elven people?","math_label":"(+1 Grief)","modifier":1,"answer":true}],"Spite":[{"question":"Has the character been betrayed by their friends?","math_label":"(+1 Spite)","modifier":1,"answer":true},{"question":"Is the character lovesick or broken hearted?","math_label":"(+1 Spite)","modifier":1,"answer":true},{"question":"Has the character been abandoned by those they held dear?","math_label":"(+1 Spite)","modifier":1,"answer":true},{"question":"Has the character been abused or tortured?","math_label":"(+1 Spite)","modifier":1,"answer":true},{"question":"Does the character still respect or admire someone on the other side?","math_label":"(-1 Spite)","modifier":-1,"answer":true},{"question":"Does the character still love someone on the other side?","math_label":"(-2 Spite)","modifier":-2}]}}

@ -1,133 +0,0 @@
{
"stock": "test",
"settings": {
"Test Setting": {
"Born Test": {
"time": 7,
"res": 5,
"skills": [
[
1,
"General"
]
],
"traits": [
2
],
"leads": [
"Nowhere"
],
"key_leads": [
"Nowhere Setting"
]
},
"A thing": {
"time": 5,
"res": 2,
"stat": [
[
2,
"p"
]
],
"skills": [
[
4,
"Testing",
"Forest-wise",
"Stealthy"
]
],
"traits": [
1
],
"leads": [
"Nowhere"
],
"key_leads": [
"Nowhere Setting"
]
}
},
"Nowhere Setting": {
"Born Nothing": {
"time": 0,
"res": 0,
"skills": [
],
"traits": [
],
"leads": [
],
"key_leads": [
]
},
"Not A Thing": {
"time": 15,
"res": 1,
"stat": [
[
1,
"pm"
]
],
"skills": [
[
2,
"Nothinging",
"Voiding"
]
],
"traits": [
3
],
"leads": [
"Somewhere"
],
"key_leads": [
"Somewhere Subsetting"
]
}
},
"Somewhere Subsetting": {
"Something Special": {
"time": 30,
"res": 21,
"stat": [
[
1,
"m"
],[
1,
"p"
],[
1,
"pm"
]
],
"skills": [
[
7,
"Specializing",
"Soothing",
"History",
"Sorcery",
"Spirit Binding",
"Harming"
]
],
"traits": [
3,
"Stubborn",
"Shy"
],
"leads": [
"Nowhere"
],
"key_leads": [
"Nowhere Setting"
]
}
}
}
}

@ -1,25 +0,0 @@
{
"stock": "test",
"resources": [
{
"name": "Rags",
"type": "gear",
"rp": 1
},
{
"name": "Riches",
"type": "gear",
"rp": 50
},
{
"name": "A firey soul",
"type": "gear",
"rp": 3
},
{
"name": "Chest or Footlocker",
"type": "gear",
"rp": 3
}
]
}

@ -1,39 +0,0 @@
{
"Testing": {
"stock": "testish",
"roots": [
"Perception"
]
},
"Nothinging": {
"stock": "testish",
"roots": [
"Will",
"Agility"
]
},
"Voiding": {
"stock": "testish",
"roots": [
"Speed"
]
},
"Specializing": {
"stock": "testish",
"roots": [
"Forte"
]
},
"Soothing": {
"stock": "testish",
"roots": [
"Power"
]
},
"Harming": {
"stock": "testish",
"roots": [
"Will"
]
}
}

@ -1,95 +0,0 @@
{
"key": "test",
"name": "Test",
"stride": 9,
"adjective": "testish",
"common_traits": [
"Trait 1",
"testy",
"Trait #3",
"Night Eyed (Test)"
],
"starting_stats":
[
{
"range": [
1,
10
],
"m": 1,
"p": 2
},
{
"range": [
11,
20
],
"m": 2,
"p": 4
},
{
"range": [
21,
30
],
"m": 3,
"p": 6
},
{
"range": [
31,
40
],
"m": 4,
"p": 8
},
{
"range": [
41,
50
],
"m": 5,
"p": 10
},
{
"range": [
51,
60
],
"m": 6,
"p": 12
},
{
"range": [
61,
70
],
"m": 7,
"p": 14
},
{
"range": [
71,
80
],
"m": 8,
"p": 16
},
{
"range": [
81,
90
],
"m": 9,
"p": 18
},
{
"range": [
91,
100
],
"m": 10,
"p": 20
}
]
}

@ -1,55 +0,0 @@
{
"Trait 1": {
"cost": 0,
"type": "die",
"restrict": [
"testish",
"common"
],
"desc": ""
},
"testy": {
"cost": 0,
"type": "die",
"restrict": [
"testish",
"common"
],
"desc": ""
},
"Trait #3": {
"cost": 0,
"type": "die",
"restrict": [
"testish",
"common"
],
"desc": ""
},
"Night Eyed (Test)": {
"cost": 1,
"type": "die",
"restrict": [
"testish",
"common"
],
"desc": "The test version"
},
"Shy": {
"cost": 1,
"type": "character",
"restrict": [
"testish",
"lifepath"
]
},
"Special": {
"cost": 3,
"type": "die",
"restrict": [
"testish",
"special"
],
"desc": "Snowflake"
}
}
Loading…
Cancel
Save