├── css ├── Shadowrun Bold.ttf ├── Shadowrun Italic.ttf ├── Shadowrun Regular.ttf ├── fonts │ ├── shadowrun_bold.ttf │ ├── shadowrun_italic.ttf │ └── shadowrun_regular.ttf ├── images │ ├── ui-icons_000000_256x240.png │ ├── ui-icons_00498f_256x240.png │ ├── ui-icons_1f1f1f_256x240.png │ ├── ui-icons_75abff_256x240.png │ ├── ui-icons_9ccdfc_256x240.png │ ├── ui-icons_ffffff_256x240.png │ ├── ui-bg_hexagon_30_0b58a2_12x10.png │ ├── ui-bg_hexagon_30_a32d00_12x10.png │ ├── ui-bg_inset-soft_40_00498f_1x100.png │ ├── ui-bg_diagonals-small_40_0a0a0a_40x40.png │ ├── ui-bg_diagonals-small_50_262626_40x40.png │ └── ui-bg_diagonals-small_60_000000_40x40.png ├── sr_tools.css └── jquery-ui.css ├── LICENSE ├── js ├── sr_tools_roll.js ├── sr_tools_run.js ├── sr_tools_storage.js ├── sr_tools_gen.js └── sr_tools.js ├── README.md └── index.html /css/Shadowrun Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/Shadowrun Bold.ttf -------------------------------------------------------------------------------- /css/Shadowrun Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/Shadowrun Italic.ttf -------------------------------------------------------------------------------- /css/Shadowrun Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/Shadowrun Regular.ttf -------------------------------------------------------------------------------- /css/fonts/shadowrun_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/fonts/shadowrun_bold.ttf -------------------------------------------------------------------------------- /css/fonts/shadowrun_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/fonts/shadowrun_italic.ttf -------------------------------------------------------------------------------- /css/fonts/shadowrun_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/fonts/shadowrun_regular.ttf -------------------------------------------------------------------------------- /css/images/ui-icons_000000_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-icons_000000_256x240.png -------------------------------------------------------------------------------- /css/images/ui-icons_00498f_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-icons_00498f_256x240.png -------------------------------------------------------------------------------- /css/images/ui-icons_1f1f1f_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-icons_1f1f1f_256x240.png -------------------------------------------------------------------------------- /css/images/ui-icons_75abff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-icons_75abff_256x240.png -------------------------------------------------------------------------------- /css/images/ui-icons_9ccdfc_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-icons_9ccdfc_256x240.png -------------------------------------------------------------------------------- /css/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /css/images/ui-bg_hexagon_30_0b58a2_12x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-bg_hexagon_30_0b58a2_12x10.png -------------------------------------------------------------------------------- /css/images/ui-bg_hexagon_30_a32d00_12x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-bg_hexagon_30_a32d00_12x10.png -------------------------------------------------------------------------------- /css/images/ui-bg_inset-soft_40_00498f_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-bg_inset-soft_40_00498f_1x100.png -------------------------------------------------------------------------------- /css/images/ui-bg_diagonals-small_40_0a0a0a_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-bg_diagonals-small_40_0a0a0a_40x40.png -------------------------------------------------------------------------------- /css/images/ui-bg_diagonals-small_50_262626_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-bg_diagonals-small_50_262626_40x40.png -------------------------------------------------------------------------------- /css/images/ui-bg_diagonals-small_60_000000_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toktic/sr_gmt/HEAD/css/images/ui-bg_diagonals-small_60_000000_40x40.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 toktic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /js/sr_tools_roll.js: -------------------------------------------------------------------------------- 1 | var roll = { 2 | dval: function(dice) 3 | { 4 | return Math.floor(Math.random() * dice + 1); 5 | }, 6 | 7 | d: function(count, options) 8 | { 9 | if (options === undefined) 10 | { 11 | options = {}; 12 | } 13 | 14 | options = $.extend({}, { 15 | pre_edge: false 16 | }, options); 17 | 18 | var hits = 0, i, exploding_count = count, v, e, res = { 19 | glitch: false, 20 | crit_glitch: false 21 | }; 22 | 23 | var rolls = []; 24 | 25 | for (i = count; i > 0; i--) 26 | { 27 | v = this.dval(6); 28 | rolls.push(v); 29 | hits += (v >= 5) ? 1 : 0; 30 | if (v === 6 && options.pre_edge) 31 | { 32 | e = this.d(1, {pre_edge: true}); 33 | exploding_count++; 34 | hits += e.hits; 35 | rolls = rolls.concat(e.rolls); 36 | } 37 | } 38 | 39 | res.hits = hits; 40 | 41 | res.rolls = rolls.sort(function (a, b) 42 | { 43 | return a - b; 44 | }); 45 | 46 | res.misses = rolls.filter(function (i) {return i === 1}).length; 47 | 48 | if (res.misses > (exploding_count / 2)) 49 | { 50 | if (hits === 0) 51 | { 52 | res.crit_glitch = true; 53 | } 54 | else 55 | { 56 | res.glitch = true; 57 | } 58 | } 59 | 60 | return res; 61 | }, 62 | 63 | half: function(pool, down) 64 | { 65 | if (down) 66 | { 67 | return Math.floor(pool / 2); 68 | } 69 | return Math.ceil(pool / 2); 70 | }, 71 | 72 | random_attribute: function () 73 | { 74 | switch(this.dval(2)) 75 | { 76 | case 1: 77 | return this.random_mental_attribute(); 78 | default: 79 | return this.random_physical_attribute(); 80 | } 81 | }, 82 | 83 | random_mental_attribute: function () 84 | { 85 | switch(this.dval(4)) 86 | { 87 | case 1: 88 | return 'will'; 89 | case 2: 90 | return 'logic'; 91 | case 3: 92 | return 'intuition'; 93 | default: 94 | return 'charisma'; 95 | } 96 | }, 97 | 98 | random_physical_attribute: function () 99 | { 100 | switch(this.dval(4)) 101 | { 102 | case 1: 103 | return 'body'; 104 | case 2: 105 | return 'agility'; 106 | case 3: 107 | return 'reaction'; 108 | default: 109 | return 'strength'; 110 | } 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to my Shadowrun GM Mob Master! 2 | 3 | The GM Mob Master was created as a set of tools for Shadowrun game masters. It started as a way of generating extra NPCs for combat-heavy runs, and as a thought exercise in how to expand the list of grunts in the Core Rulebook to more situations. The intent was to include generators and lists that would make it easier for game masters to get mechanics out of the way and spend more time telling stories. My hope was that this will eventually make it easy for game masters to generate an entire session of entertainment with the click of a few buttons. 4 | 5 | It is divided into several sections, all of which can be accessed by the buttons on the left. There is also a dice-simulator at the top to let you quickly simulate a fist-full of dice. Remember, as a GM you don't have to be honest with how many successes or failures you "rolled"... 6 | 7 | - The first section is **Cast of Shadows**. This holds the full assortment of npcs that you create and save for use later. The "Full Cast" lists all saved cast members. You can create plenty of custom tabs and list only the cast members you need for a scene on that tab. 8 | - Next up is the **NPC Generator**. You can use the tabs within to create a single NPC or a horde of them. Just be aware that if you create one and don't save them to the **Cast of Shadows**, they will disappear and be lost. 9 | - **Settings** has a selection of, you guessed it, Settings that you can change. This also has a Nuke button if you want to delete all of your saved information and start over from scratch. 10 | 11 | This has also served as a creative outlet for programming for my own enjoyment. I have spent quite a few hours on this utility in the name of fun. There was never any intention of turning this into a money-generating enterprise. I don't want to deal with the legal or infrastructure headaches. Though if you happen to find me at a convention and want to say thanks, you can buy me a beer at GenCon. If you want to complain or sue me, I don't exist and don't attend GenCon. :D 12 | 13 | On data-retention: I use a piece of web programming functionality called Local Storage that act a little bit like cookies. The data is stored in your browser's cache for you for that browser. I have to specify both of those because it's not shared to other users on your computer, nor even other browsers. It certainly isn't shared to other computers, and I don't have access to it at all. So if you spend the time to create and save a cast of shadow denizens, make sure you create it on the computer you're bringing to the game session. Or print them out, that works too! 14 | 15 | As with many side projects, there is a whole list of things I want to add to this utility. There are plenty of features that I haven't had the time to add yet. Since I'm not getting paid to work on this, it only happens when I do have spare time that isn't being spent on other things. And there will eventually come a time that I wash my hands of this and let it sit on a shelf to die from neglect. It may not be a nice thing to say, but it's the truth. This utility isn't meant to be a replacement for the Shadowrun books, either. I make an assumption that users are familiar with the basics of Shadowrun, from what an Adept is to what constitutes a Mr. Johnson, and are able to look things up as needed, like specific equipment information. It is meant as a supplement and assistant, much like how HeroLab and Chummer make PC creation easier for runners. I will not try to make this fully comprehensive and cover every possible option. There are too many source books to keep up with and this isn't meant to be a replacement for sourcebooks. 16 | 17 | Have fun with this tool! It is meant to be used and intended to make your life easier as a GM. Speaking of which, always try to offer your players the chance to make a deal with a dragon. 18 | 19 | _The Topps Company, Inc. has sole ownership of the names, logo, artwork, marks, photographs, sounds, audio, video and/or any proprietary matrial used in connection with the game Shadowrun. The Topps Company, Inc. has granted permission to GM Mob Master to use such names, logos, artwork, marks and/or any proprietary materials for promotional and informational purposes on its website but does not endorse, and is not affiliated with this utility in any official capacity whatsoever.
Original content within GM Mob Master is licensed under the Creative Commons Attribution-NonCommercial 4.0 International. So if you use anything here, please give credit where it is due. GM Mob Master is provided "as is" and no warranties or guarantees are provided or inferred. In no event shall the copyright holder or contributors be liable for damage however they are caused. Don't be a jerk!_ 20 | -------------------------------------------------------------------------------- /js/sr_tools_run.js: -------------------------------------------------------------------------------- 1 | var run = { 2 | _employer_type: [ 3 | 'Secret Society', 4 | 'Political or Activist Group', 5 | 'Government', 6 | 'Minor Corporation', 7 | 'A Corporation', 8 | 'AA Corporation', 9 | 'AAA Corporation', 10 | 'Criminal Syndicate', 11 | 'Magical Group', 12 | 'Private Individual', 13 | 'Exotic' 14 | ], 15 | 16 | _employer_sub_type: { 17 | 'Secret Society': [ 18 | 'Black Lodge', 19 | 'Human Nation' 20 | ], 21 | 'Political or Activist Group': [ 22 | 'Humanis Policlub', 23 | 'Mothers of Metahumans', 24 | 'Sons of Sauron' 25 | ], 26 | 'Government': [ 27 | 'Govenor', 28 | 'Senator', 29 | 'Local Department' 30 | ], 31 | 'Minor Corporation': [], 32 | 'A Corporation': [], 33 | 'AA Corporation': [], 34 | 'AAA Corporation': [ 35 | 'Ares Macrotechnology', 36 | 'Aztechnology', 37 | 'EVO Corporation', 38 | 'Horizon Group', 39 | 'Mitsuhama Computer Technologies', 40 | 'NeoNET', 41 | 'Renraku Computer Systems', 42 | 'Saeder-Krupp Heavy Industries', 43 | 'Shiawase Corporation', 44 | 'Wuxing Incorporated' 45 | ], 46 | 'Criminal Syndicate': [ 47 | 'Koshari', 48 | 'Mafia', 49 | 'Triad', 50 | 'Yakuza', 51 | 'Vory' 52 | ], 53 | 'Magical Group': [ 54 | 'Illuminates of the New Dawn', 55 | 'Draco Foundation' 56 | ], 57 | 'Private Individual': [], 58 | 'Exotic': [ 59 | 'AI', 60 | 'Dragon', 61 | 'Free Spirit' 62 | ] 63 | }, 64 | 65 | _run_type:['Datasteal', 'Assassination', 'Destruction', 'Extraction', 'Insertion', 'Misdirection', 'Protection', 'Delivery'], 66 | 67 | _run_sub_type: { 68 | Datasteal: [ 69 | 'Prototype Object', 70 | 'Research', 71 | 'Hidden Records' 72 | ], 73 | 74 | Assassination: [ 75 | 'Politician', 76 | 'Corporate Management', 77 | 'Whistleblower' 78 | ], 79 | 80 | Destruction: [ 81 | 'Vehicle', 82 | 'Public Infrastructure', 83 | 'Private Residence', 84 | 'Corporate Property' 85 | ], 86 | Extraction: [ 87 | 'Scientist', 88 | 'Mole', 89 | 'Hostage', 90 | 'Artist', 91 | 'Test Subject' 92 | ], 93 | Insertion: [ 94 | 'Mole' 95 | ], 96 | Misdirection: [ 97 | 'Draw Security', 98 | 'Plant Evidence', 99 | 'Create Illusion' 100 | ], 101 | Protection: [ 102 | 'Rescue Hostage', 103 | 'Prevent Extraction', 104 | 'Prevent Destruction' 105 | ], 106 | Delivery: [ 107 | 'Creature', // Lab creature, pet of someone important, pet that ate data 108 | 'Equipment' // Something magical, something toxic 109 | ] 110 | }, 111 | 112 | _objectives: [ 113 | { 114 | run_type: 'Extraction', 115 | run_sub_type: 'Mole', 116 | title: 'Rescue mole before their discovery', // Print-ready title of some type 117 | motivation: '', // Why is the employer doing this? Print-ready 118 | target: { 119 | 120 | // Maybe flag for needing to generate a company? 121 | // Or generate special NPCs for it? 122 | }, 123 | pay: { 124 | base: 1000, 125 | increment: 500 126 | }, 127 | karma: 2, 128 | street_cred: 0, 129 | noteriety: 0, 130 | timeline: '2 days' 131 | }, 132 | { 133 | run_type: 'Datasteal', 134 | title: 'Steal prototype commlink model', // Print-ready title of some type 135 | motivation: "Employer wants early access to the commlink's physical dimensions in order to start making accessories before any competition", 136 | target: { 137 | 138 | // Maybe flag for needing to generate a company? 139 | // Or generate special NPCs for it? 140 | }, 141 | pay: { 142 | base: 2000, 143 | increment: 100 144 | }, 145 | karma: 2, 146 | street_cred: 0, 147 | noteriety: 0, 148 | timeline: '72 hours' 149 | } 150 | ], 151 | 152 | _twists: [ 153 | { 154 | run_type: 'Delivery', // If present or set, limits where the twist can be 155 | run_sub_type: 'Creature', // If present or set, limits where the twist can be 156 | is_main: false, // If true, this means it can be a required twist 157 | is_push: false, // If true, this means it can be included when Pushing the Envelope 158 | title: '', // Print-ready, what is going on 159 | notes: '', // Notes to the GM on what is going on 160 | is_separate_scene: false, // If true, will be included as it's own scene in the write-up 161 | pay: { // Only really included in the mission pay when this is a required twist, it is ignored when an optional twist 162 | base: 500, // Add this to the base pay 163 | increment: 25 164 | } 165 | }, 166 | { 167 | is_push: true, // If true, this means it can be included when Pushing the Envelope 168 | title: 'Troll Partakes Kamikaze', // Print-ready, what is going on 169 | notes: 'As the party is travelling, they encounter a Troll ganger who has gotten their first taste of the drug Kamikaze. In a drug-fueled haze, the Troll decides to take on the PCs in combat.', 170 | is_separate_scene: true, 171 | npcs: [ 172 | { 173 | name: 'TPK Troll', 174 | race: 'Troll', 175 | professional_type: 'ganger', 176 | notes: "This troll has just taken a dose of Kamikaze and has decided to forcefully eject the PCs from the gang's turf, or maybe just kill them outright. Negotiations are not likely to happen." 177 | } 178 | ] 179 | } 180 | ], 181 | 182 | get_run_type: function () 183 | { 184 | var i = roll.dval(this._run_type.length) - 1; 185 | 186 | return this._run_type[i]; 187 | } 188 | }; 189 | -------------------------------------------------------------------------------- /js/sr_tools_storage.js: -------------------------------------------------------------------------------- 1 | var storage = { 2 | initialize_storage: function() 3 | { 4 | // Set up any expected things in localStorage 5 | localStorage.build_id = build_id; 6 | 7 | // // Cast of Shadows 8 | // What tab ID did we last create? 9 | localStorage.cast_tab_id = 1; 10 | 11 | // What character ID did we last create? 12 | localStorage.cast_character_id = 0; 13 | 14 | // What tab did we show last? 15 | // Start with the management tab, so new users see it at least once 16 | localStorage.cast_current_tab = 0; 17 | 18 | // What tabs are there? 19 | localStorage.cast_tabs = JSON.stringify([ 20 | { 21 | tab_id: 1, 22 | name: 'Full Cast', 23 | order: 1, 24 | characters: [] // This is an array of {character_id, order} objects 25 | } 26 | ]); 27 | 28 | // Save the characters 29 | localStorage.cast_characters = JSON.stringify([]); 30 | 31 | // Save a character template 32 | localStorage.cast_character_template = JSON.stringify({ 33 | character_id: null, 34 | type: '', 35 | data: null 36 | }); 37 | 38 | // Settings 39 | localStorage.setting_condition_monitor = 'combined'; 40 | localStorage.setting_wound_penalty = '3'; 41 | }, 42 | 43 | // Return an array of tabs with their name, tab ID, and display ordering 44 | get_cast_tabs: function() 45 | { 46 | var stored_tabs = $.parseJSON(localStorage.cast_tabs); 47 | 48 | stored_tabs.forEach(function(tab) 49 | { 50 | tab.href = tab.name.replace(/( )/g, '_').replace(/\W/g, ''); 51 | }); 52 | 53 | stored_tabs.sort(function (a, b) 54 | { 55 | return a.order - b.order; 56 | }); 57 | 58 | return stored_tabs; 59 | }, 60 | 61 | // Return the currently displayed tab ID 62 | get_current_cast_tab: function() 63 | { 64 | return parseInt(localStorage.cast_current_tab); 65 | }, 66 | 67 | // Set which tab we are viewing now 68 | set_current_cast_tab: function(id) 69 | { 70 | localStorage.cast_current_tab = id; 71 | }, 72 | 73 | // Get information about a tab 74 | get_cast_tab: function(tab_id) 75 | { 76 | var stored_tabs = $.parseJSON(localStorage.cast_tabs), ret = null; 77 | 78 | stored_tabs.forEach(function(tab) 79 | { 80 | tab.href = tab.name.replace(/( )/g, '_').replace(/\W/g, ''); 81 | if (tab.tab_id === tab_id) 82 | ret = tab; 83 | }); 84 | 85 | if (ret === null) 86 | console.log('ERROR: get_cast_tab() unable to find specified tab', tab_id); 87 | 88 | return ret; 89 | }, 90 | 91 | // Update a given tab, also for adding a new tab 92 | set_cast_tab: function(tab_id, tab_data) 93 | { 94 | // If the order isn't the same as the existing tabs, update other tabs to match? 95 | var stored_tabs = $.parseJSON(localStorage.cast_tabs); 96 | 97 | var lower_order, upper_order, change_direction = false; 98 | 99 | stored_tabs.forEach(function(tab) 100 | { 101 | if (tab_id === tab.tab_id) 102 | { 103 | if (tab_data.name != null && tab_data.name !== tab.name) 104 | { 105 | tab.name = tab_data.name; 106 | } 107 | 108 | if (tab_data.hasOwnProperty('characters') && tab_data.characters.length !== tab.characters.length) 109 | tab.characters = tab_data.characters; 110 | 111 | if (Number.isInteger(tab_data.order) && tab_data.order !== tab.order) 112 | { 113 | upper_order = Math.max(tab.order, tab_data.order); 114 | lower_order = Math.min(tab.order, tab_data.order); 115 | change_direction = (tab.order > tab_data.order) ? 1 : -1; 116 | tab.order = tab_data.order; 117 | } 118 | } 119 | }); 120 | 121 | if (change_direction !== false) 122 | { 123 | stored_tabs.forEach(function(tab) 124 | { 125 | if (tab_id !== tab.tab_id && tab.order >= lower_order && tab.order <= upper_order) 126 | { 127 | tab.order += change_direction; 128 | } 129 | }); 130 | } 131 | 132 | localStorage.cast_tabs = JSON.stringify(stored_tabs); 133 | }, 134 | 135 | // Delete a given tab from storage 136 | delete_cast_tab: function(tab_id) 137 | { 138 | if (tab_id === 1) 139 | return; 140 | 141 | var stored_tabs = $.parseJSON(localStorage.cast_tabs), new_tabs = []; 142 | 143 | stored_tabs.forEach(function(tab) 144 | { 145 | if (tab.tab_id !== tab_id) 146 | new_tabs.push(tab); 147 | }); 148 | 149 | localStorage.cast_tabs = JSON.stringify(new_tabs); 150 | }, 151 | 152 | generate_character_id: function() 153 | { 154 | var id = parseInt(localStorage.cast_character_id) + 1; 155 | localStorage.cast_character_id = id; 156 | return id; 157 | }, 158 | 159 | generate_cast_tab_id: function() 160 | { 161 | var id = parseInt(localStorage.cast_tab_id) + 1; 162 | localStorage.cast_tab_id = id; 163 | return id; 164 | }, 165 | 166 | get_characters: function() 167 | { 168 | return $.parseJSON(localStorage.cast_characters); 169 | }, 170 | 171 | get_character: function(id) 172 | { 173 | var character = null, all = this.get_characters(); 174 | 175 | all.forEach(function(char) 176 | { 177 | if (id === char.character_id) 178 | character = char; 179 | }); 180 | 181 | return character; 182 | }, 183 | 184 | set_character: function(data) 185 | { 186 | var cast_characters = $.parseJSON(localStorage.cast_characters), new_char = false; 187 | var updated_cast = []; 188 | 189 | if (!data.hasOwnProperty('character_id')) 190 | { 191 | new_char = true; 192 | data.character_id = this.generate_character_id(); 193 | } 194 | 195 | if (new_char) 196 | { 197 | cast_characters.push(data); 198 | updated_cast = cast_characters; 199 | } 200 | else 201 | { 202 | cast_characters.forEach(function(char) 203 | { 204 | if (char.character_id === data.character_id) 205 | updated_cast.push(data); 206 | else 207 | updated_cast.push(char); 208 | }); 209 | } 210 | 211 | localStorage.cast_characters = JSON.stringify(updated_cast); 212 | 213 | return data; 214 | }, 215 | 216 | delete_character_from_tab: function(tab_id, character_id) 217 | { 218 | var tab_data = this.get_cast_tab(tab_id); 219 | 220 | tab_data.characters = tab_data.characters.filter(function(id) 221 | { 222 | return id !== character_id; 223 | }); 224 | 225 | this.set_cast_tab(tab_id, tab_data); 226 | }, 227 | 228 | delete_character: function(id) 229 | { 230 | var old_cast = $.parseJSON(localStorage.cast_characters), new_cast = [], i = 0; 231 | 232 | for (i; i < old_cast.length; i++) 233 | { 234 | if (id !== old_cast[i].character_id) 235 | new_cast.push(old_cast[i]); 236 | } 237 | 238 | localStorage.cast_characters = JSON.stringify(new_cast); 239 | 240 | var tabs = this.get_cast_tabs(); 241 | 242 | tabs.forEach(function(tab) 243 | { 244 | storage.delete_character_from_tab(tab.tab_id, id); 245 | }); 246 | }, 247 | 248 | // Clone the character, optionally adding them to the same tabs as the original 249 | clone_character: function(id, clone_tabs) 250 | { 251 | var old_character = this.get_character(id), new_character, new_id; 252 | 253 | new_character = $.extend({}, old_character); 254 | delete new_character.character_id; 255 | 256 | // Change the name, either adding "Copy", or updating the copy # 257 | var copiedCharacter = new RegExp('.* Copy ([0-9])([0-9])'); 258 | var copyTest = copiedCharacter.exec(new_character.name); 259 | 260 | if (copiedCharacter.test(new_character.name)) 261 | { 262 | // Increment the copy number 263 | new_character.name = new_character.name.slice(0, -2); 264 | 265 | var newName = parseInt(copyTest[1]) * 10 + parseInt(copyTest[2]) + 1; 266 | 267 | if (newName < 10) 268 | { 269 | newName = '0' + newName; 270 | } 271 | 272 | new_character.name += newName; 273 | } 274 | else if (new_character.name.slice(-5) == ' Copy') 275 | { 276 | new_character.name += ' 01'; 277 | } 278 | else 279 | { 280 | new_character.name += ' Copy'; 281 | } 282 | 283 | new_character = this.set_character(new_character); 284 | new_id = new_character.character_id; 285 | 286 | if (clone_tabs === true) { 287 | var tabs = this.get_cast_tabs(); 288 | 289 | tabs.forEach(function(tab) 290 | { 291 | var old_char_index = tab.characters.indexOf(id); 292 | 293 | if (tab.characters.includes(id)) 294 | { 295 | tab.characters.splice(old_char_index + 1, 0, new_id); 296 | storage.set_cast_tab(tab.tab_id, tab); 297 | } 298 | }); 299 | } 300 | 301 | return new_character; 302 | }, 303 | 304 | // Return the ID of the newly created tab 305 | create_cast_tab: function(tab_name) 306 | { 307 | // Find the highest tab ID now 308 | var tab_id = this.generate_cast_tab_id(), sort_order, stored_tabs = $.parseJSON(localStorage.cast_tabs); 309 | 310 | sort_order = stored_tabs.length + 1; 311 | 312 | stored_tabs.push({ 313 | tab_id: tab_id, 314 | name: tab_name, 315 | order: sort_order, 316 | characters: [] 317 | }); 318 | 319 | localStorage.cast_tabs = JSON.stringify(stored_tabs); 320 | 321 | return tab_id; 322 | }, 323 | 324 | // Get a specific setting 325 | // Note that actually changing settings is just left to the Settings tab 326 | setting: function(name) 327 | { 328 | if (localStorage.hasOwnProperty('setting_' + name)) 329 | { 330 | return localStorage['setting_' + name]; 331 | } 332 | else 333 | { 334 | return null; 335 | } 336 | } 337 | }; 338 | -------------------------------------------------------------------------------- /css/sr_tools.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | background-color: #383838; 4 | font-family: "Trebuchet MS", sans-serif; 5 | margin: 50px; 6 | } 7 | 8 | @font-face 9 | { 10 | font-family: ShadowrunNormal; 11 | src: url("fonts/shadowrun_regular.ttf"); 12 | } 13 | 14 | @font-face 15 | { 16 | font-family: ShadowrunItalics; 17 | src: url("fonts/shadowrun_italic.ttf"); 18 | } 19 | 20 | @font-face 21 | { 22 | font-family: ShadowrunBold; 23 | src: url("fonts/shadowrun_bold.ttf"); 24 | } 25 | 26 | 27 | 28 | .big_button 29 | { 30 | padding: 5px; 31 | } 32 | 33 | .big_button:hover 34 | { 35 | } 36 | 37 | .smaller_button 38 | { 39 | font-weight: normal; 40 | padding: 1px 3px; 41 | font-size: 0.7em; 42 | } 43 | 44 | .smaller_button:hover 45 | { 46 | font-weight: normal; 47 | } 48 | 49 | .tiny_button 50 | { 51 | font-weight: normal; 52 | padding: 0; 53 | font-size: 0.5em; 54 | } 55 | 56 | .tiny_button:hover 57 | { 58 | font-weight: normal; 59 | } 60 | 61 | .top_bar 62 | { 63 | position: absolute; 64 | top: 0; 65 | left: 0; 66 | right: 0; 67 | background-color: #383838; 68 | height: 90px; 69 | } 70 | 71 | .top_bar .title 72 | { 73 | cursor: pointer; 74 | font-size: 28px; 75 | font-weight: bold; 76 | font-style: normal; 77 | color: #fff; 78 | margin-top: 24px; 79 | margin-left: 24px; 80 | font-family: "ShadowrunNormal", Arial, sans-serif; 81 | } 82 | 83 | .top_bar .version 84 | { 85 | color: #fff; 86 | margin-left: 24px; 87 | } 88 | 89 | .top_bar .top_bar_roller 90 | { 91 | position: absolute; 92 | top: 5px; 93 | right: 5px; 94 | } 95 | 96 | .top_bar .top_bar_roller > div 97 | { 98 | display: inline-block; 99 | vertical-align: top; 100 | } 101 | 102 | .top_bar .top_bar_roller .edging 103 | { 104 | color: #fff; 105 | margin-top: 24px; 106 | } 107 | 108 | .top_bar .top_bar_roller #roll_results 109 | { 110 | background-color: #383838; 111 | color: #fff; 112 | height: 77px; 113 | width: 450px; 114 | } 115 | 116 | .menu 117 | { 118 | position: absolute; 119 | top: 90px; 120 | left: 0; 121 | bottom: 0; 122 | width: 250px; 123 | background-color: #383838; 124 | } 125 | 126 | .menu button 127 | { 128 | width: 200px; 129 | margin-left: 24px; 130 | margin-top: 10px; 131 | } 132 | 133 | .main_content 134 | { 135 | position: absolute; 136 | top: 90px; 137 | bottom: 0; 138 | left: 250px; 139 | right: 0; 140 | background-color: #383838; 141 | color: #fff; 142 | } 143 | 144 | .main_content .intro_screen 145 | { 146 | padding-right: 25%; 147 | padding-bottom: 20px; 148 | font-family: "ShadowrunNormal", Arial, sans-serif; 149 | } 150 | 151 | .main_content .intro_screen .legalese 152 | { 153 | color: #ddd; 154 | font-style: italic; 155 | margin-top: 40px; 156 | } 157 | 158 | div[template_holder] 159 | { 160 | display: none; 161 | } 162 | 163 | .entry_form 164 | { 165 | 166 | } 167 | 168 | .entry_form > .input_row 169 | { 170 | padding-bottom: 2px; 171 | } 172 | 173 | .entry_form > .input_row label 174 | { 175 | display: inline-block; 176 | } 177 | .entry_form > .input_row select 178 | { 179 | width: 200px; 180 | } 181 | 182 | .spacer_5 183 | { 184 | height: 5px; 185 | } 186 | 187 | .spacer_10 188 | { 189 | height: 10px; 190 | } 191 | 192 | .spacer_15 193 | { 194 | height: 15px; 195 | } 196 | 197 | .spacer_20 198 | { 199 | height: 20px; 200 | } 201 | 202 | .spacer_25 203 | { 204 | height: 25px; 205 | } 206 | 207 | .spacer_30 208 | { 209 | height: 30px; 210 | } 211 | 212 | .spacer_35 213 | { 214 | height: 35px; 215 | } 216 | 217 | .spacer_40 218 | { 219 | height: 40px; 220 | } 221 | 222 | .spacer_45 223 | { 224 | height: 45px; 225 | } 226 | 227 | .minion_generator_section 228 | { 229 | padding-right: 25px; 230 | } 231 | 232 | .minion_generator_section #overview .intro:not(:first-child) 233 | { 234 | margin-top: 10px; 235 | } 236 | 237 | #minion_generator 238 | { 239 | 240 | } 241 | 242 | #minion_generator .entry_form 243 | { 244 | 245 | } 246 | 247 | #minion_generator .entry_form #generated_results 248 | { 249 | width: 650px; 250 | margin-top: 20px; 251 | margin-bottom: 20px; 252 | } 253 | 254 | #mob_generator .entry_form .input_row > label[equalize] 255 | { 256 | vertical-align: top; 257 | } 258 | 259 | #mob_generator .entry_form #generated_results 260 | { 261 | width: 650px; 262 | margin-top: 20px; 263 | margin-bottom: 20px; 264 | } 265 | 266 | #mob_generator .entry_form #generated_results .mob_entry:not(:first-child) 267 | { 268 | margin-top: 10px; 269 | } 270 | 271 | #mob_generator .entry_form .add_special_types 272 | { 273 | display: inline-block; 274 | } 275 | 276 | #mob_generator .entry_form .add_special_types label 277 | { 278 | font-size: 0.75em; 279 | display: block; 280 | text-align: left; 281 | } 282 | 283 | .display_npc_wrapper 284 | { 285 | border: 1px solid #ccee00; 286 | font-size: 0.9em; 287 | } 288 | 289 | .display_npc_wrapper .controls 290 | { 291 | float: right; 292 | font-size: 0; 293 | margin-top: 2px; 294 | margin-right: 2px; 295 | } 296 | 297 | .display_npc_wrapper .controls button 298 | { 299 | margin-left: 1px; 300 | margin-right: 1px; 301 | } 302 | 303 | .display_npc_wrapper .npc_name, 304 | .display_npc_wrapper .npc_description, 305 | .display_npc_wrapper .npc_notes 306 | { 307 | border-bottom: 1px solid #ccee00; 308 | padding: 2px 2px 2px 4px; 309 | } 310 | .display_npc_wrapper .attribute_names 311 | { 312 | border-bottom: 1px solid #ccee00; 313 | display: flex; 314 | font-weight: bold; 315 | font-size: 1.1em; 316 | } 317 | .display_npc_wrapper .attribute_values 318 | { 319 | border-bottom: 1px solid #ccee00; 320 | display: flex; 321 | } 322 | 323 | .display_npc_wrapper .attribute_names .attribute_name, 324 | .display_npc_wrapper .attribute_values .attribute_value 325 | { 326 | text-align: center; 327 | flex-grow: 1; 328 | flex-basis: 10%; 329 | } 330 | 331 | .display_npc_wrapper .information > div 332 | { 333 | background-color: #383838; 334 | display: table; 335 | width: 100% 336 | } 337 | 338 | .display_npc_wrapper .information > div > label 339 | { 340 | padding: 2px; 341 | width: 160px; 342 | display: table-cell; 343 | vertical-align: top; 344 | color: #ccee00; 345 | background-color: #000; 346 | font-weight: bold; 347 | } 348 | 349 | .display_npc_wrapper .information > div > div, 350 | .display_npc_wrapper .information > div.skills > div > div 351 | { 352 | padding: 2px; 353 | display: flex; 354 | vertical-align: top; 355 | } 356 | 357 | .display_npc_wrapper .information > div.gear > div, 358 | .display_npc_wrapper .information > div.skills > div 359 | { 360 | display: table-cell; 361 | } 362 | 363 | .display_npc_wrapper .information > div.skills .skill 364 | { 365 | flex: 3 0 120px; 366 | border-bottom: 1px dotted #666; 367 | } 368 | 369 | .display_npc_wrapper .information > div.skills button 370 | { 371 | margin-left: 4px; 372 | margin-right: 4px; 373 | } 374 | 375 | .display_npc_wrapper .information > div.skills .result 376 | { 377 | flex: 2 0 40px; 378 | } 379 | 380 | .action_npc_wrapper .information .rollable 381 | { 382 | display: flex; 383 | } 384 | 385 | .action_npc_wrapper .information .rollable > div:not(.result) 386 | { 387 | flex: 3 0 120px; 388 | } 389 | 390 | .action_npc_wrapper .information .rollable > button 391 | { 392 | margin-left: 4px; 393 | margin-right: 4px; 394 | } 395 | 396 | .action_npc_wrapper .information .rollable > .result 397 | { 398 | flex: 2 0 40px; 399 | } 400 | 401 | .action_npc_wrapper .information > div > div > button 402 | { 403 | margin-left: 5px; 404 | margin-right: 5px; 405 | } 406 | 407 | .action_npc_wrapper .information > div > div .result 408 | { 409 | color: #000; 410 | background-color: #ccc; 411 | min-width: 40px; 412 | padding-left: 4px; 413 | padding-right: 4px; 414 | text-align: center; 415 | } 416 | 417 | .action_npc_wrapper .information > .gear > .value > div 418 | { 419 | display: flex; 420 | padding: 2px; 421 | } 422 | 423 | .action_npc_wrapper .information > .gear > .value > div .stats 424 | { 425 | flex: 3 0 120px; 426 | border-bottom: 1px dotted #666; 427 | } 428 | 429 | .action_npc_wrapper .information > .gear > .value > div button 430 | { 431 | margin-left: 5px; 432 | margin-right: 5px; 433 | } 434 | 435 | .action_npc_wrapper .information > .gear > .value > div .result 436 | { 437 | flex: 0 0 40px; 438 | } 439 | 440 | .action_npc_wrapper .information .condition_monitor_combined, 441 | .action_npc_wrapper .information .condition_monitor_separate 442 | { 443 | width: 100%; 444 | display: flex; 445 | } 446 | 447 | .action_npc_wrapper .information .condition_monitor_combined .monitor, 448 | .action_npc_wrapper .information .condition_monitor_separate .monitor 449 | { 450 | border: 1px solid #999; 451 | flex: 3 0 1px; 452 | margin-left: 4px; 453 | } 454 | 455 | .action_npc_wrapper .information .condition_monitor_combined .monitor .boxes, 456 | .action_npc_wrapper .information .condition_monitor_separate .monitor .boxes 457 | { 458 | display: flex; 459 | } 460 | 461 | .action_npc_wrapper .information .condition_monitor_combined .monitor .boxes > div, 462 | .action_npc_wrapper .information .condition_monitor_separate .monitor .boxes > div 463 | { 464 | background-color: #222; 465 | border: 1px solid #333; 466 | color: #666; 467 | flex: 1 1 1px; 468 | text-align: center; 469 | } 470 | 471 | .action_npc_wrapper .information .condition_monitor_combined .monitor .markers, 472 | .action_npc_wrapper .information .condition_monitor_separate .monitor .markers 473 | { 474 | display: flex; 475 | } 476 | 477 | .action_npc_wrapper .information .condition_monitor_combined .monitor .markers > div, 478 | .action_npc_wrapper .information .condition_monitor_separate .monitor .markers > div 479 | { 480 | flex: 1 1 1px; 481 | } 482 | 483 | .action_npc_wrapper .information .condition_monitor_combined .penalty, 484 | .action_npc_wrapper .information .condition_monitor_separate .penalty 485 | { 486 | flex: 2 0 1px; 487 | padding-left: 4px; 488 | padding-right: 4px; 489 | } 490 | 491 | .edit_npc_wrapper .npc_notes textarea 492 | { 493 | margin: 2px 2px 2px 4px; 494 | width: calc(100% - 16px); 495 | } 496 | 497 | .edit_npc_wrapper .npc_notes label, 498 | .edit_npc_wrapper .other_information > div > label 499 | { 500 | display: block; 501 | padding-left: 2px; 502 | color: #ccee00; 503 | font-weight: bold; 504 | } 505 | 506 | .edit_npc_wrapper .other_information .condition_monitor > div, 507 | .edit_npc_wrapper .other_information .wound_penalty > div 508 | { 509 | padding-left: 5px; 510 | } 511 | 512 | .edit_npc_wrapper .other_information .skills 513 | { 514 | 515 | } 516 | 517 | .edit_npc_wrapper .other_information .skills .value 518 | { 519 | padding-left: 5px; 520 | } 521 | 522 | .edit_npc_wrapper .other_information .skills .value .skill 523 | { 524 | width: 40%; 525 | display: inline-block; 526 | } 527 | 528 | .edit_npc_wrapper .other_information .skills .value div.skill_rating 529 | { 530 | width: 7%; 531 | display: inline-block; 532 | } 533 | 534 | .edit_npc_wrapper .other_information .skills .value button 535 | { 536 | vertical-align: text-bottom; 537 | } 538 | 539 | .edit_npc_wrapper .other_information .qualities div.quality 540 | { 541 | padding-left: 5px; 542 | } 543 | 544 | .edit_npc_wrapper .other_information .qualities div.quality span.quality 545 | { 546 | width: 40%; 547 | display: inline-block; 548 | } 549 | 550 | .edit_npc_wrapper .other_information .augments .value 551 | { 552 | padding-left: 5px; 553 | } 554 | 555 | .edit_npc_wrapper .other_information .augments .value .augmentation 556 | { 557 | width: 40%; 558 | display: inline-block; 559 | } 560 | 561 | .edit_npc_wrapper .other_information .augments .value div.augmentation_rating 562 | { 563 | width: 7%; 564 | display: inline-block; 565 | } 566 | 567 | .edit_npc_wrapper .other_information .augments .value button 568 | { 569 | vertical-align: text-bottom; 570 | } 571 | 572 | .cast_wrapper 573 | { 574 | padding-right: 25px; 575 | } 576 | 577 | .cast_of_shadows 578 | { 579 | 580 | } 581 | 582 | .cast_of_shadows .add_tab_wrapper 583 | { 584 | 585 | } 586 | 587 | .cast_of_shadows .add_tab_wrapper button 588 | { 589 | vertical-align: bottom; 590 | } 591 | 592 | .cast_of_shadows .edit_tab_wrapper button 593 | { 594 | vertical-align: bottom; 595 | } 596 | 597 | .cast_of_shadows .cast_tabs 598 | { 599 | 600 | } 601 | 602 | .cast_of_shadows .cast__full_list 603 | { 604 | 605 | } 606 | 607 | .cast_of_shadows .cast__full_list > div[list] 608 | { 609 | 610 | } 611 | 612 | .cast_of_shadows .cast__full_list > div[list] > div 613 | { 614 | display: flex; 615 | margin-bottom: 10px; 616 | } 617 | 618 | .cast_of_shadows .cast__full_list > div[list] > div > .entry 619 | { 620 | flex: 1 0 500px; 621 | } 622 | 623 | .cast_of_shadows .cast__full_list > div[list] > div > .tools 624 | { 625 | flex: 2 0 200px; 626 | padding-left: 10px; 627 | } 628 | 629 | .cast_of_shadows .cast__full_list > div[list] > div > .tools .add_to_tab 630 | { 631 | padding-left: 4px; 632 | } 633 | 634 | .cast_of_shadows .cast__full_list > div[list] > div > .tools .add_to_tab > div 635 | { 636 | padding: 6px 0; 637 | } 638 | 639 | .cast_of_shadows .cast__tab_list > div[list] > div 640 | { 641 | display: flex; 642 | margin-bottom: 10px; 643 | } 644 | 645 | .cast_of_shadows .cast__tab_list > div[list] > div > .entry 646 | { 647 | flex: 1 0 500px; 648 | } 649 | 650 | .cast_of_shadows .cast__tab_list > div[list] > div > .tools 651 | { 652 | flex: 2 0 200px; 653 | padding-left: 10px; 654 | } 655 | -------------------------------------------------------------------------------- /js/sr_tools_gen.js: -------------------------------------------------------------------------------- 1 | var gen = { 2 | type_options: ['civilian', 'thug', 'ganger', 'corpsec', 'police', 'cultist', 'htr', 'specops', 'mob'], 3 | 4 | random_type: function() 5 | { 6 | return this.type_options[roll.dval(this.type_options.length - 1)]; 7 | }, 8 | 9 | _merge_adjustments: function(base, adjust) 10 | { 11 | var i, attributes = ['body', 'agility', 'reaction', 'strength', 'will', 'logic', 'intuition', 'charisma']; 12 | 13 | if (!base.hasOwnProperty('professional_description') && adjust.hasOwnProperty('professional_description')) 14 | { 15 | base.professional_description = adjust.professional_description; 16 | } 17 | 18 | if (adjust.hasOwnProperty('attributes')) 19 | { 20 | var racial = false; 21 | 22 | if (base.hasOwnProperty('race')) 23 | racial = db.get_metatype_adjustment(base.race); 24 | 25 | attributes.forEach(function(att) 26 | { 27 | if (adjust.attributes.hasOwnProperty(att)) 28 | { 29 | base.attributes[att] += adjust.attributes[att]; 30 | 31 | if (racial) 32 | { 33 | // Racial Minimum 34 | base.attributes[att] = Math.max(racial.min_attributes[att], base.attributes[att]); 35 | 36 | // Racial Maximum 37 | base.attributes[att] = Math.min(racial.max_attributes[att], base.attributes[att]); 38 | } 39 | } 40 | }); 41 | } 42 | 43 | if (adjust.hasOwnProperty('skills')) 44 | { 45 | for (i in adjust.skills) 46 | { 47 | if (i && base.skills.hasOwnProperty(i)) 48 | { 49 | base.skills[i] = Math.max(base.skills[i], adjust.skills[i]); 50 | } 51 | else 52 | { 53 | base.skills[i] = adjust.skills[i]; 54 | } 55 | } 56 | } 57 | 58 | if (adjust.hasOwnProperty('qualities')) 59 | { 60 | adjust.qualities.positive.forEach(function (item) { 61 | if (typeof item === 'object' || !base.qualities.positive.includes(item)) 62 | { 63 | base.qualities.positive.push(item); 64 | } 65 | }); 66 | 67 | adjust.qualities.negative.forEach(function (item) { 68 | if (typeof item === 'object' || !base.qualities.negative.includes(item)) 69 | { 70 | base.qualities.negative.push(item); 71 | } 72 | }); 73 | } 74 | 75 | if (adjust.hasOwnProperty('armor')) 76 | { 77 | base.armor = adjust.armor; 78 | } 79 | 80 | if (adjust.hasOwnProperty('weapons')) 81 | { 82 | adjust.weapons.forEach(function (item) { 83 | if (typeof item === 'object' || !base.weapons.includes(item)) 84 | { 85 | base.weapons.push(item); 86 | } 87 | }); 88 | } 89 | 90 | if (adjust.hasOwnProperty('augmentations')) 91 | { 92 | adjust.augmentations.forEach(function (item) { 93 | if (typeof item === 'object' || !base.augmentations.includes(item)) 94 | { 95 | base.augmentations.push(item); 96 | } 97 | }); 98 | } 99 | 100 | if (adjust.hasOwnProperty('gear')) 101 | { 102 | adjust.gear.forEach(function (item) { 103 | if (typeof item === 'object' || !base.gear.includes(item)) 104 | { 105 | base.gear.push(item); 106 | } 107 | }); 108 | } 109 | 110 | if (adjust.hasOwnProperty('special')) 111 | { 112 | base.special = $.extend({}, base.special, adjust.special); 113 | // If we have a magic rating, remove any augmentations 114 | if (adjust.special.hasOwnProperty('Magic')) 115 | { 116 | base.augmentations = []; 117 | } 118 | } 119 | 120 | if (adjust.hasOwnProperty('commlink')) 121 | { 122 | if (adjust.commlink > base.commlink) 123 | { 124 | base.commlink = adjust.commlink; 125 | } 126 | } 127 | 128 | return base; 129 | }, 130 | 131 | mob: function(options) 132 | { 133 | if (options === undefined) 134 | { 135 | options = {}; 136 | } 137 | 138 | options = $.extend({}, { 139 | size: roll.dval(10), 140 | professional_rating: roll.dval(5) - 1, 141 | professional_type: this.type_options[roll.dval(this.type_options.length - 1)], 142 | all_race: false, 143 | include_special: (roll.dval(10) > 6), 144 | include_lt: false, 145 | include_adept: false, 146 | include_mage: false, 147 | include_decker: false 148 | }, options); 149 | 150 | var mob = [], mob_options = { 151 | professional_rating: options.professional_rating, 152 | professional_type: options.professional_type 153 | }; 154 | 155 | if (options.all_race !== false) 156 | { 157 | mob_options.race = options.all_race; 158 | } 159 | 160 | // If we want to include one of the specials, but haven't set which one, choose one at random 161 | var mook_count = options.size; 162 | 163 | if (options.include_special) 164 | { 165 | mook_count = options.size = 1; 166 | 167 | if (!options.include_lt && !options.include_adept && !options.include_mage && !options.include_decker) 168 | { 169 | var i = roll.dval(10); 170 | 171 | switch (true) 172 | { 173 | case (i < 6): 174 | options.include_lt = true; 175 | break; 176 | case (i < 8): 177 | options.include_decker = true; 178 | break; 179 | case (i < 10): 180 | options.include_adept = true; 181 | break; 182 | default: 183 | options.include_mage = true; 184 | break; 185 | } 186 | } 187 | 188 | var this_special = $.clone(mob_options); 189 | 190 | if (options.include_lt) 191 | { 192 | this_special.is_lt = true; 193 | } 194 | else if (options.include_decker) 195 | { 196 | this_special.is_decker = true; 197 | } 198 | else if (options.include_adept) 199 | { 200 | this_special.is_adept = true; 201 | } 202 | else if (options.include_mage) 203 | { 204 | this_special.is_mage = true; 205 | } 206 | 207 | mob.push(this.mook(this_special)); 208 | } 209 | 210 | for (mook_count; mook_count > 0; mook_count--) 211 | { 212 | mob.push(this.mook(mob_options)); 213 | } 214 | 215 | return mob; 216 | }, 217 | 218 | mook: function(options) 219 | { 220 | if (options === undefined) 221 | { 222 | options = {}; 223 | } 224 | 225 | options = $.extend({}, { 226 | name: 'Mook #' + roll.dval(10) + roll.dval(10) + roll.dval(10), 227 | gender: false, // false for random 228 | race: false, 229 | professional_rating: -1, 230 | professional_type: false, 231 | is_lt: false, 232 | is_adept: false, 233 | is_mage: false, 234 | is_decker: false, 235 | is_johnson: false, 236 | is_gunbunny: false, 237 | is_samurai: false, 238 | is_tank: false, 239 | is_shaman: false, 240 | is_contact: false, 241 | contact: false, // {connection rating, loyalty rating, type} || false 242 | notes: null 243 | }, options); 244 | 245 | var mook = { 246 | name: options.name, 247 | attributes: {body: 0, agility: 0, reaction: 0, strength: 0, will: 0, logic: 0, intuition: 0, charisma: 0}, 248 | skills: {}, 249 | knowledge_skills: {}, 250 | qualities: { 251 | positive: [], 252 | negative: [] 253 | }, 254 | weapons: [], 255 | armor: [], 256 | gear: [], 257 | augmentations: [], 258 | special: {}, 259 | commlink: 1, 260 | created: new Date().toJSON(), 261 | professional_type: options.professional_type 262 | }; 263 | 264 | // Pull in copies from global settings 265 | mook.condition_monitor = storage.setting('condition_monitor'); 266 | mook.wound_penalty = storage.setting('wound_penalty'); 267 | 268 | // If we don't have a gender, assign a binary gender. 269 | // Will limiting gender to a binary decision piss off some people? Probably yes. 270 | // However, the author is not spending time developing a fully politically correct gender-determination system at this time. 271 | // If you really want to hear how the author feels about the situation, buy him a beer 272 | if (options.gender !== 'Male' && options.gender !== 'Female') 273 | { 274 | if (options.is_contact) 275 | { 276 | // Even split 277 | if (roll.dval(2) === 2) 278 | { 279 | mook.gender = 'Female'; 280 | } 281 | else 282 | { 283 | mook.gender = 'Male'; 284 | } 285 | } 286 | else 287 | { 288 | // Probably not so even 289 | if (roll.dval(10) >= 9) 290 | { 291 | mook.gender = 'Female'; 292 | } 293 | else 294 | { 295 | mook.gender = 'Male'; 296 | } 297 | } 298 | } 299 | else 300 | { 301 | mook.gender = options.gender; 302 | } 303 | 304 | // If we don't have a professional rating, then generate a random one from 0-4 305 | if (options.professional_rating === -1) 306 | { 307 | options.professional_rating = roll.dval(5) - 1; 308 | } 309 | 310 | mook.professional_rating = options.professional_rating; 311 | 312 | var rating_baseline = db.get_base_attributes(options.professional_rating); 313 | 314 | this._merge_adjustments(mook, rating_baseline); 315 | 316 | // If we don't have a race, generate one 317 | if (options.race === false) 318 | { 319 | options.race = db.gen_race(); 320 | } 321 | 322 | mook.race = options.race; 323 | // Get the attribute adjustments from race and apply them 324 | var racial_baseline = db.get_metatype_adjustment(options.race); 325 | 326 | this._merge_adjustments(mook, racial_baseline); 327 | 328 | // If we don't have a professional type and we aren't a contact, then generate one 329 | if (options.professional_type === false) 330 | { 331 | if (options.is_contact === false) 332 | { 333 | options.professional_type = this.type_options[roll.dval(this.type_options.length - 1)]; 334 | } 335 | } 336 | 337 | if (options.is_contact === false) 338 | { 339 | mook.professional_type = options.professional_type; 340 | 341 | switch (mook.professional_type) 342 | { 343 | case 'civilian': 344 | mook.professional_description = 'Civilian'; 345 | break; 346 | case 'thug': 347 | mook.professional_description = 'Thug'; 348 | break; 349 | case 'ganger': 350 | mook.professional_description = 'Gang Member'; 351 | break; 352 | case 'corpsec': 353 | mook.professional_description = 'Corporate Security'; 354 | break; 355 | case 'police': 356 | mook.professional_description = 'Law Enforcement'; 357 | break; 358 | case 'cultist': 359 | mook.professional_description = 'Cultist'; 360 | break; 361 | case 'htr': 362 | mook.professional_description = 'High Threat Response'; 363 | break; 364 | case 'specops': 365 | mook.professional_description = 'Special Operations'; 366 | break; 367 | case 'mob': 368 | mook.professional_description = 'Organized Crime'; 369 | break; 370 | } 371 | } 372 | 373 | // Contacts do not have type adjustments, but everyone else does 374 | if (options.is_contact) 375 | { 376 | // TODO I need to deal with generating contacts! 377 | // This needs to include some stat adjustments for their rating, plus their type of contact-ness and other helpful things. 378 | } 379 | else 380 | { 381 | var type_adjustments = db.get_type_adjustments(options.professional_type, options.professional_rating); 382 | this._merge_adjustments(mook, type_adjustments); 383 | } 384 | 385 | // Is this a special type? [LT, adept, mage, decker] 386 | var adjustments; 387 | 388 | if (options.is_lt) 389 | { 390 | adjustments = db.get_special_adjustments('LT', options); 391 | this._merge_adjustments(mook, adjustments); 392 | mook.special.is_lt = true; 393 | } 394 | 395 | if (options.is_decker) 396 | { 397 | adjustments = db.get_special_adjustments('Decker', options); 398 | this._merge_adjustments(mook, adjustments); 399 | mook.special.is_decker = true; 400 | } 401 | 402 | if (options.is_adept) 403 | { 404 | adjustments = db.get_special_adjustments('Adept', options); 405 | this._merge_adjustments(mook, adjustments); 406 | mook.special.is_adept = true; 407 | } 408 | 409 | if (options.is_mage) 410 | { 411 | adjustments = db.get_special_adjustments('Mage', options); 412 | this._merge_adjustments(mook, adjustments); 413 | mook.special.is_mage = true; 414 | } 415 | 416 | if (options.is_shaman) 417 | { 418 | adjustments = db.get_special_adjustments('Shaman', options); 419 | this._merge_adjustments(mook, adjustments); 420 | mook.special.is_tank = true; 421 | } 422 | 423 | if (options.is_tank) 424 | { 425 | adjustments = db.get_special_adjustments('Tank', options); 426 | this._merge_adjustments(mook, adjustments); 427 | mook.special.is_tank = true; 428 | } 429 | 430 | if (options.is_samurai) 431 | { 432 | adjustments = db.get_special_adjustments('Samurai', options); 433 | this._merge_adjustments(mook, adjustments); 434 | mook.special.is_tank = true; 435 | } 436 | 437 | if (options.is_gunbunny) 438 | { 439 | adjustments = db.get_special_adjustments('Gunbunny', options); 440 | this._merge_adjustments(mook, adjustments); 441 | mook.special.is_tank = true; 442 | } 443 | 444 | if (options.is_johnson) 445 | { 446 | adjustments = db.get_special_adjustments('Johnson', options); 447 | this._merge_adjustments(mook, adjustments); 448 | mook.special.is_tank = true; 449 | } 450 | 451 | // If this is a Troll who has certain augmentations, they need to lose the Troll Dermal Deposits 452 | if (mook.race === 'Troll') 453 | { 454 | var skin_augments = ['Dermal Plating', 'Orthoskin']; 455 | 456 | var augment = mook.augmentations.filter(function (aug) 457 | { 458 | return skin_augments.includes(aug.name); 459 | }); 460 | 461 | if (augment.length > 0) 462 | { 463 | augment = mook.augmentations.filter(function (aug) 464 | { 465 | return aug.name !== 'Troll Dermal Deposits'; 466 | }); 467 | mook.augmentations = augment; 468 | } 469 | } 470 | 471 | return mook; 472 | }, 473 | 474 | matrix_host: function(options) 475 | { 476 | if (options === undefined) 477 | { 478 | options = {}; 479 | } 480 | 481 | options = $.extend({}, { 482 | // matrix_host: false, // False, or a host rating 483 | // matrix_host_mode: 'Secure', // Ratings order; Secure: FADS, Data: DFAS, Hidden: SFAD 484 | notes: null 485 | }, options); 486 | 487 | // TODO make the rest of this useful later 488 | }, 489 | 490 | _corp_name_combine_word: [ 491 | 'aim', 492 | 'arm', 493 | 'ash', 494 | 'auto', 495 | 'block', 496 | 'bright', 497 | 'caption', 498 | 'com', 499 | 'down', 500 | 'dream', 501 | 'fund', 502 | 'gate', 503 | 'green', 504 | 'hydro', 505 | 'lion', 506 | 'mark', 507 | 'max', 508 | 'motor', 509 | 'plastic', 510 | 'point', 511 | 'scope', 512 | 'strong', 513 | 'sun', 514 | 'thermo', 515 | 'wood', 516 | 'works' 517 | ], 518 | 519 | _corp_name_modifier_word: [ 520 | 'Ace', 521 | 'Action', 522 | 'Advanced', 523 | 'Anchor', 524 | 'Apparel', 525 | 'Aquatic', 526 | 'Atlas', 527 | 'Boulder', 528 | 'Broadcasting', 529 | 'Collective', 530 | 'Construction', 531 | 'Eagle', 532 | 'Electric', 533 | 'Entertainment', 534 | 'Financial', 535 | 'Financial', 536 | 'Global', 537 | 'Gold', 538 | 'Golden', 539 | 'Green', 540 | 'International', 541 | 'Investigative', 542 | 'Lender', 543 | 'Machine', 544 | 'Master', 545 | 'Media', 546 | 'Network', 547 | 'New World', 548 | 'Old World', 549 | 'Research', 550 | 'Robotic', 551 | 'Stone', 552 | 'Visual', 553 | 'Wireless' 554 | ], 555 | 556 | _corp_name_final_word: [ 557 | 'Agriculture', 558 | 'Analysis', 559 | 'Analytics', 560 | 'Collective', 561 | 'Construction', 562 | 'Consumables', 563 | 'Corporation', 564 | 'Entertainment', 565 | 'Financial', 566 | 'Global', 567 | 'Group', 568 | 'Holdings', 569 | 'Incorporated', 570 | 'Industries', 571 | 'International', 572 | 'Investigations', 573 | 'Network', 574 | 'Press', 575 | 'Processing', 576 | 'Productions', 577 | 'Reporting', 578 | 'Research', 579 | 'Robotics', 580 | 'Security', 581 | 'Systems', 582 | 'Technologies', 583 | 'Works' 584 | ], 585 | 586 | initials: function(count) 587 | { 588 | if (count === undefined) 589 | { 590 | count = roll.dval(3); 591 | } 592 | 593 | var l = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 594 | var a = '.-/+'; 595 | var ret = []; 596 | 597 | for (var i = 1; i <= count; i++) 598 | { 599 | ret.push(l[roll.dval(l.length) - 1]) 600 | } 601 | 602 | if (roll.dval(5) === 5) 603 | { 604 | i = a[roll.dval(a.length) - 1]; 605 | return ret.join(i); 606 | } 607 | else 608 | { 609 | return ret.join(''); 610 | } 611 | }, 612 | 613 | combine_words: function() 614 | { 615 | var i, j, k; 616 | 617 | do { 618 | i = roll.dval(this._corp_name_combine_word.length); 619 | j = roll.dval(this._corp_name_combine_word.length); 620 | } while (i === j); 621 | 622 | k = this._corp_name_combine_word[i - 1] + this._corp_name_combine_word[j - 1]; 623 | return k[0].toUpperCase() + k.slice(1); 624 | }, 625 | 626 | corp_name: function(options) 627 | { 628 | if (options === undefined) 629 | { 630 | options = {}; 631 | } 632 | 633 | options = $.extend({}, { 634 | format: null, 635 | notes: null, 636 | subsidiary: null // Subsidiary of '___' 637 | }, options); 638 | 639 | var corp = {name: ''}, name_parts = []; 640 | var formats = ['double', 'initials', 'triple', 'initials double']; 641 | 642 | if (options.format === null) 643 | { 644 | options.format = formats[roll.dval(formats.length) - 1]; 645 | } 646 | 647 | switch(options.format) 648 | { 649 | case 'double': 650 | if (roll.dval(2) === 2) 651 | { 652 | name_parts.push(this._corp_name_modifier_word[roll.dval(this._corp_name_modifier_word.length) - 1]); 653 | } 654 | else 655 | { 656 | name_parts.push(this.combine_words()); 657 | } 658 | break; 659 | 660 | case 'initials': 661 | name_parts.push(this.initials()); 662 | break; 663 | 664 | case 'triple': 665 | if (roll.dval(2) === 2) 666 | { 667 | name_parts.push(this.combine_words()); 668 | name_parts.push(this._corp_name_modifier_word[roll.dval(this._corp_name_modifier_word.length) - 1]); 669 | } 670 | else 671 | { 672 | name_parts.push(this._corp_name_modifier_word[roll.dval(this._corp_name_modifier_word.length) - 1]); 673 | name_parts.push(this.combine_words()); 674 | } 675 | break; 676 | 677 | case 'initials double': 678 | name_parts.push(this.initials()); 679 | name_parts.push(this._corp_name_modifier_word[roll.dval(this._corp_name_modifier_word.length) - 1]); 680 | break; 681 | 682 | default: 683 | console.log('ERROR: corp_name() with unknown format', options.format); 684 | return; 685 | } 686 | 687 | // Generate the final word 688 | name_parts.push(this._corp_name_final_word[roll.dval(this._corp_name_final_word.length) - 1]); 689 | 690 | corp.name = name_parts.join(' '); 691 | 692 | return corp; 693 | } 694 | }; 695 | -------------------------------------------------------------------------------- /js/sr_tools.js: -------------------------------------------------------------------------------- 1 | function view_cast(show_intro) 2 | { 3 | var $container = $('.main_content').empty(); 4 | var $template = render.get_template('cast_of_shadows'); 5 | var tabs_added = 0, tab_index_to_show; 6 | 7 | $('
').addClass('cast_wrapper').append($template).appendTo($container); 8 | 9 | var tabs = storage.get_cast_tabs(); 10 | 11 | tabs.forEach(function(tab) 12 | { 13 | tabs_added++; 14 | 15 | var $href = $('' + tab.name + '').attr('href', '#' + tab.href).attr('tab_id', tab.tab_id); 16 | 17 | var $li = $('
  • ').attr('tab_id', tab.tab_id).append($href); 18 | 19 | $template.find('ul.cast_tabs').append($li); 20 | 21 | $('
    ', {id: tab.href}).appendTo($template.find('.cast_of_shadows')).attr('tab_id', tab.tab_id); 22 | 23 | // Add a row for editing this tab to the introduction edit area 24 | var $row_template = render.get_template('edit_tab_row'); 25 | 26 | $row_template.appendTo($template.find('.edit_tab_wrapper')); 27 | 28 | $row_template.find('#tab_name').val(tab.name); 29 | 30 | // Check will save the name 31 | var save_tab_data = function () 32 | { 33 | var tab_name = $row_template.find('#tab_name').val().replace(/\W/g, ' ').trim(); 34 | 35 | if (tab.name !== tab_name && tab_name !== '') 36 | { 37 | storage.set_cast_tab(tab.tab_id, {name: tab_name}); 38 | storage.set_current_cast_tab(tab.tab_id); 39 | view_cast(); 40 | } 41 | }; 42 | 43 | $row_template.find('button.tab_edit').button().click(save_tab_data); 44 | $row_template.find('#tab_name').on('keyup', function (e) 45 | { 46 | if (e.keyCode === 13) 47 | save_tab_data(); 48 | }); 49 | 50 | // Up arrow moves tab up 51 | $row_template.find('button.tab_up').button(); 52 | 53 | if (tabs_added > 1) 54 | { 55 | $row_template.find('button.tab_up').click(function () 56 | { 57 | storage.set_cast_tab(tab.tab_id, {order: (tab.order - 1)}); 58 | view_cast(); 59 | }); 60 | } 61 | else 62 | { 63 | $row_template.find('button.tab_up').button('disable'); 64 | } 65 | 66 | // Down moves down 67 | $row_template.find('button.tab_down').button(); 68 | 69 | if (tabs_added < tabs.length) 70 | { 71 | $row_template.find('button.tab_down').click(function () 72 | { 73 | storage.set_cast_tab(tab.tab_id, {order: (tab.order + 1)}); 74 | view_cast(); 75 | }); 76 | } 77 | else 78 | { 79 | $row_template.find('button.tab_down').button('disable'); 80 | } 81 | 82 | // Don't allow the main tab to be deleted 83 | $row_template.find('button.delete_tab').button(); 84 | 85 | if (tab.tab_id !== 1) 86 | { 87 | $row_template.find('button.delete_tab').click(function () 88 | { 89 | storage.delete_cast_tab(tab.tab_id); 90 | storage.set_current_cast_tab(1); 91 | view_cast(true); 92 | }); 93 | } 94 | else 95 | { 96 | $row_template.find('button.delete_tab').button('disable'); 97 | } 98 | }); 99 | 100 | var redraw_full_cast = function($tab) 101 | { 102 | $tab.empty().append(render.get_template('cast__full_list')); 103 | 104 | var full_cast = storage.get_characters(); 105 | 106 | if (full_cast.length > 0) 107 | { 108 | $tab.find('.empty_message').detach(); 109 | } 110 | else 111 | { 112 | $tab.find('.cast_message').detach(); 113 | } 114 | 115 | full_cast.forEach(function(cast) 116 | { 117 | var $char_template = render.get_template('cast__full_list_entry').appendTo($tab.find('[list]')); 118 | 119 | render.mook_for_action($char_template.find('.entry'), cast); 120 | 121 | $char_template.find('.tab_delete_dialog').detach(); 122 | var $deletion_dialog = $char_template.find('.delete_dialog').dialog({ 123 | autoOpen: false, 124 | modal: true, 125 | title: 'Remove Cast Member', 126 | width: 450, 127 | buttons: [ 128 | { 129 | text: "Ok", 130 | click: function() { 131 | storage.delete_character(cast.character_id); 132 | $(this).dialog("close"); 133 | view_cast(); 134 | } 135 | }, 136 | { 137 | text: "Cancel", 138 | click: function() { 139 | $(this).dialog("close"); 140 | } 141 | } 142 | ] 143 | }); 144 | 145 | $char_template.find('.delete_cast_member').button().click(function () 146 | { 147 | $deletion_dialog.dialog('open'); 148 | }); 149 | 150 | $char_template.find('.tab_clone_dialog').detach(); 151 | var $clone_dialog = $char_template.find('.clone_dialog').dialog({ 152 | autoOpen: false, 153 | modal: true, 154 | title: 'Clone Cast Member', 155 | width: 450, 156 | buttons: [ 157 | { 158 | text: "Ok", 159 | click: function() { 160 | storage.clone_character(cast.character_id); 161 | $(this).dialog("close"); 162 | view_cast(); 163 | } 164 | }, 165 | { 166 | text: "Cancel", 167 | click: function() { 168 | $(this).dialog("close"); 169 | } 170 | } 171 | ] 172 | }); 173 | 174 | $char_template.find('.clone_cast_member').button().click(function () 175 | { 176 | $clone_dialog.dialog('open'); 177 | }); 178 | 179 | $char_template.find('.created_date').html('Created: ' + render.format_string_date(cast.created)); 180 | 181 | if (cast.edited) 182 | $char_template.find('.edited_date').html('Last Edited: ' + render.format_string_date(cast.edited)); 183 | else 184 | $char_template.find('.edited_date').hide(); 185 | 186 | // Add the npc to a given tab it isn't already in 187 | var tabs = storage.get_cast_tabs(); 188 | 189 | if (tabs.length > 1) 190 | { 191 | $char_template.find('button.add_npc').button().off('click').click(function() 192 | { 193 | var tab_id = parseInt($char_template.find('select[name="tab_name"]').val()); 194 | var this_tab = storage.get_cast_tab(tab_id); 195 | 196 | if (!this_tab.characters.includes(cast.character_id)) 197 | { 198 | this_tab.characters.push(cast.character_id); 199 | storage.set_cast_tab(tab_id, this_tab); 200 | } 201 | }); 202 | 203 | $char_template.find('button.add_npc_and_switch').button().off('click').click(function() 204 | { 205 | var tab_id = parseInt($char_template.find('select[name="tab_name"]').val()); 206 | var this_tab = storage.get_cast_tab(tab_id); 207 | 208 | if (!this_tab.characters.includes(cast.character_id)) 209 | { 210 | this_tab.characters.push(cast.character_id); 211 | storage.set_cast_tab(tab_id, this_tab); 212 | } 213 | 214 | $template.find('.cast_tabs li[tab_id] a[tab_id="' + tab_id + '"]').click(); 215 | }); 216 | 217 | var tabs_available = false; 218 | tabs.forEach(function(tab) 219 | { 220 | if (!tab.characters.includes(cast.character_id) && tab.tab_id !== 1) 221 | { 222 | $char_template.find('.add_to_tab select[name="tab_name"]').append($('