| 
 | 1 | +// Github:   https://github.com/shdwjk/Roll20API/blob/master/MonsterHitDice/MonsterHitDice.js  | 
 | 2 | +// By:       The Aaron, Arcane Scriptomancer  | 
 | 3 | +// Contact:  https://app.roll20.net/users/104025/the-aaron  | 
 | 4 | +var API_Meta = API_Meta||{}; //eslint-disable-line no-var  | 
 | 5 | +API_Meta.MonsterHitDice={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};  | 
 | 6 | +{try{throw new Error('');}catch(e){API_Meta.MonsterHitDice.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-6);}}  | 
 | 7 | + | 
 | 8 | +var globalconfig = globalconfig || undefined;  //eslint-disable-line no-var  | 
 | 9 | +const MonsterHitDice = (() => { // eslint-disable-line no-unused-vars  | 
 | 10 | + | 
 | 11 | +    const version = '0.3.13';  | 
 | 12 | +    API_Meta.MonsterHitDice.version = version;  | 
 | 13 | +    const lastUpdate = 1748794696;  | 
 | 14 | +    const schemaVersion = 0.3;  | 
 | 15 | + | 
 | 16 | +    let tokenIds = [];  | 
 | 17 | + | 
 | 18 | +    const checkGlobalConfig = () => {  | 
 | 19 | +        const s=state.MonsterHitDice;  | 
 | 20 | +        const g = (globalconfig && globalconfig.monsterhitdice);  | 
 | 21 | +        if(g && g.lastsaved && g.lastsaved > s.globalconfigCache.lastsaved){  | 
 | 22 | +           log('  > Updating from Global Config <  ['+(new Date(g.lastsaved*1000))+']');  | 
 | 23 | +           s.config.bar = parseInt(g.Bar.match(/\d+/)[0]);  | 
 | 24 | +            switch(g.Sheet){  | 
 | 25 | +                case "DnD 5e - Community Contributed":  | 
 | 26 | +                    s.config.hitDiceAttribute = 'npc_HP_hit_dice';  | 
 | 27 | +                    s.config.findSRDFormula = false;  | 
 | 28 | +                    s.config.HDis1eD8s = false;  | 
 | 29 | +                    s.config.useConBonus = true;  | 
 | 30 | +                    s.config.conBonusAttribute = 'npc_constitution';  | 
 | 31 | +                    s.config.conBonusIsStat = true;  | 
 | 32 | +                    break;  | 
 | 33 | + | 
 | 34 | +                case "DnD 5e - Shaped v2":  | 
 | 35 | +                    s.config.hitDiceAttribute = 'npc_hpformula';  | 
 | 36 | +                    s.config.findSRDFormula = true;  | 
 | 37 | +                    s.config.HDis1eD8s = false;  | 
 | 38 | +                    s.config.useConBonus = false;  | 
 | 39 | +                    s.config.conBonusAttribute = '';  | 
 | 40 | +                    s.config.conBonusIsStat = false;  | 
 | 41 | +                    break;  | 
 | 42 | + | 
 | 43 | +                case "DnD 5e - OGL by roll20":  | 
 | 44 | +                    s.config.hitDiceAttribute = 'npc_hpformula';  | 
 | 45 | +                    s.config.findSRDFormula = false;  | 
 | 46 | +                    s.config.HDis1eD8s = false;  | 
 | 47 | +                    s.config.useConBonus = false;  | 
 | 48 | +                    s.config.conBonusAttribute = '';  | 
 | 49 | +                    s.config.conBonusIsStat = false;  | 
 | 50 | +                    break;  | 
 | 51 | + | 
 | 52 | +                case "Advanced 1st Edition":  | 
 | 53 | +                    s.config.hitDiceAttribute = 'hitdice';  | 
 | 54 | +                    s.config.findSRDFormula = false;  | 
 | 55 | +                    s.config.HDis1eD8s = true;  | 
 | 56 | +                    s.config.useConBonus = false;  | 
 | 57 | +                    s.config.conBonusAttribute = '';  | 
 | 58 | +                    s.config.conBonusIsStat = false;  | 
 | 59 | +                    break;  | 
 | 60 | + | 
 | 61 | + | 
 | 62 | +                case "Custom - (Use settings below)":  | 
 | 63 | +                    s.config.hitDiceAttribute = g['HitDice Attribute'];  | 
 | 64 | +                    s.config.findSRDFormula = 'findSRDFormula' === g['SRD Embedded Formula'];  | 
 | 65 | +                    s.config.HDis1eD8s = 'HDis1eD8s' === g['1st Edition HD in Attribute (d8)'];  | 
 | 66 | +                    s.config.useConBonus = 'useSeparateCon' === g['Separate Constitution Modifier'];  | 
 | 67 | +                    s.config.conBonusAttribute = g['Constitution Attribute'];  | 
 | 68 | +                    s.config.conBonusIsStat = 'conIsStat' === g['Constitution is Stat'];  | 
 | 69 | +                    break;  | 
 | 70 | +            }  | 
 | 71 | +            state.MonsterHitDice.globalconfigCache=globalconfig.monsterhitdice;  | 
 | 72 | +        }  | 
 | 73 | +    };  | 
 | 74 | + | 
 | 75 | +    const checkInstall = () => {  | 
 | 76 | +        log('-=> MonsterHitDice v'+version+' <=-  ['+(new Date(lastUpdate*1000))+']');  | 
 | 77 | + | 
 | 78 | +        if( ! _.has(state,'MonsterHitDice') || state.MonsterHitDice.version !== schemaVersion) {  | 
 | 79 | +            log('  > Updating Schema to v'+schemaVersion+' <');  | 
 | 80 | +            switch(state.MonsterHitDice && state.MonsterHitDice.version) {  | 
 | 81 | + | 
 | 82 | +                case 0.1:  | 
 | 83 | +                  delete state.MonsterHitDice.globalConfigCache;  | 
 | 84 | +                  state.MonsterHitDice.globalconfigCache = {lastsaved:0};  | 
 | 85 | + | 
 | 86 | +                /* falls through */  | 
 | 87 | +                case 0.2:  | 
 | 88 | +                  state.MonsterHitDice.config.findSRDFormula = state.MonsterHitDice.findSRDFormula;  | 
 | 89 | +                  delete state.MonsterHitDice.findSRDFormula;  | 
 | 90 | + | 
 | 91 | +                /* falls through */  | 
 | 92 | +                case 'UpdateSchemaVersion':  | 
 | 93 | +                    state.MonsterHitDice.version = schemaVersion;  | 
 | 94 | +                    break;  | 
 | 95 | + | 
 | 96 | +                default:  | 
 | 97 | +                    state.MonsterHitDice = {  | 
 | 98 | +                        version: schemaVersion,  | 
 | 99 | +                        globalconfigCache: {lastsaved:0},  | 
 | 100 | +                        config: {  | 
 | 101 | +                            bar: 3,  | 
 | 102 | +                            hitDiceAttribute: 'npc_hpformula',  | 
 | 103 | +                            findSRDFormula: false,  | 
 | 104 | +                            HDis1eD8s: false,  | 
 | 105 | +                            useConBonus: false,  | 
 | 106 | +                            conBonusAttribute: '',  | 
 | 107 | +                            conBonusIsStat: false  | 
 | 108 | +                        }  | 
 | 109 | +                    };  | 
 | 110 | +            }  | 
 | 111 | +        }  | 
 | 112 | +        checkGlobalConfig();  | 
 | 113 | +    };  | 
 | 114 | + | 
 | 115 | +    const handleInput = (msg) => {  | 
 | 116 | + | 
 | 117 | +        if (msg.type === "api" && /^!mhd(\b|$)/i.test(msg.content) && playerIsGM(msg.playerid) ) {  | 
 | 118 | +            let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');  | 
 | 119 | +            let count = 0;  | 
 | 120 | +            (msg.selected || [])  | 
 | 121 | +                .map(o=>getObj('graphic',o._id))  | 
 | 122 | +                .filter(g=>undefined !== g)  | 
 | 123 | +                .forEach( o => {  | 
 | 124 | +                    ++count;  | 
 | 125 | +                    tokenIds.push(o.id);  | 
 | 126 | +                    rollHitDice(o);  | 
 | 127 | +                })  | 
 | 128 | +                ;  | 
 | 129 | +            sendChat('',`/w "${who}" Rolling hit dice for ${count} token(s).`);  | 
 | 130 | +        }  | 
 | 131 | +    };  | 
 | 132 | + | 
 | 133 | +    const findSRDRoll = (txt) => {  | 
 | 134 | +        return txt.match(/\d+d\d+(\+\d+)?/)[0]||0;  | 
 | 135 | +    };  | 
 | 136 | + | 
 | 137 | +    const buildHD1eExpression = (exp) => {  | 
 | 138 | +      let m = `${exp}`.match(/(\d+)([+-]\d+)?/);  | 
 | 139 | +      if(m){  | 
 | 140 | +        return `${m[1]}d8${ m[2] ? m[2] : ''}`;  | 
 | 141 | +      }  | 
 | 142 | +      return 0;  | 
 | 143 | +    };  | 
 | 144 | + | 
 | 145 | +    const rollHitDice = (obj) => {  | 
 | 146 | +        let sets = {};  | 
 | 147 | +        const bar = 'bar'+state.MonsterHitDice.config.bar;  | 
 | 148 | +        let hdAttrib;  | 
 | 149 | +        let conAttrib;  | 
 | 150 | +        let hdExpression = 0;  | 
 | 151 | +        let conExpression = 0;  | 
 | 152 | +        let bonus = 0;  | 
 | 153 | + | 
 | 154 | +        if(_.contains(tokenIds,obj.id)){  | 
 | 155 | +            tokenIds=_.without(tokenIds,obj.id);  | 
 | 156 | + | 
 | 157 | +            if( 'graphic' === obj.get('type') &&  | 
 | 158 | +            'token'   === obj.get('subtype') &&  | 
 | 159 | +            ''        !== obj.get('represents')  | 
 | 160 | +            ) {  | 
 | 161 | +                if( obj && '' === obj.get(bar+'_link') ) {  | 
 | 162 | +                    hdAttrib = findObjs({  | 
 | 163 | +                        type: 'attribute',   | 
 | 164 | +                        characterid: obj.get('represents'),  | 
 | 165 | +                        name: state.MonsterHitDice.config.hitDiceAttribute  | 
 | 166 | +                    })[0];  | 
 | 167 | +                    conAttrib = findObjs({  | 
 | 168 | +                        _type: 'attribute',   | 
 | 169 | +                        _characterid:obj.get('represents'),  | 
 | 170 | +                        name: state.MonsterHitDice.config.conBonusAttribute  | 
 | 171 | +                    })[0];  | 
 | 172 | + | 
 | 173 | +                    if( hdAttrib ) {  | 
 | 174 | +                        hdExpression = state.MonsterHitDice.config.findSRDFormula  | 
 | 175 | +                          ? findSRDRoll(hdAttrib.get('current'))  | 
 | 176 | +                          : hdAttrib.get('current')  | 
 | 177 | +                          ;  | 
 | 178 | + | 
 | 179 | +                        hdExpression = state.MonsterHitDice.config.HDis1eD8s  | 
 | 180 | +                          ? buildHD1eExpression(hdExpression)  | 
 | 181 | +                          : hdExpression  | 
 | 182 | +                          ;  | 
 | 183 | +                        if( state.MonsterHitDice.config.useConBonus && conAttrib ) {  | 
 | 184 | +                            conExpression = state.MonsterHitDice.config.conBonusIsStat  | 
 | 185 | +                              ? Math.round((conAttrib.get('current')-10)/2)  | 
 | 186 | +                              : conAttrib.get('current')  | 
 | 187 | +                              ;  | 
 | 188 | + | 
 | 189 | +                            bonus = conExpression * _.reduce(hdExpression.match(/(\d+)d\d+/g),(m,die) => {  | 
 | 190 | +                                m+=parseInt(die.match(/(\d+)d\d+/)[1],10);  | 
 | 191 | +                                return m;  | 
 | 192 | +                            },0);  | 
 | 193 | +                        }  | 
 | 194 | + | 
 | 195 | +                        sendChat('','/r '+hdExpression+'+'+bonus,(r) => {  | 
 | 196 | +                            let hp=0;  | 
 | 197 | +                            _.each(r,(subr) => {  | 
 | 198 | +                                let val=JSON.parse(subr.content);  | 
 | 199 | +                                if(_.has(val,'total'))  | 
 | 200 | +                                {  | 
 | 201 | +                                    hp+=val.total;  | 
 | 202 | +                                }  | 
 | 203 | +                            });  | 
 | 204 | +                            sets[bar+"_value"] = Math.max(hp,1);  | 
 | 205 | +                            sets[bar+"_max"] = Math.max(hp,1);  | 
 | 206 | +                            obj.set(sets);  | 
 | 207 | +                        });  | 
 | 208 | +                    }  | 
 | 209 | +                }  | 
 | 210 | +            }  | 
 | 211 | +        }  | 
 | 212 | +    };  | 
 | 213 | + | 
 | 214 | +    const saveTokenId = (obj) => {  | 
 | 215 | +        tokenIds.push(obj.id);  | 
 | 216 | + | 
 | 217 | +        setTimeout(((id) => {  | 
 | 218 | +            return () => {  | 
 | 219 | +                let token=getObj('graphic',id);  | 
 | 220 | +                if(token){  | 
 | 221 | +                    rollHitDice(token);  | 
 | 222 | +                }  | 
 | 223 | +            };  | 
 | 224 | +        })(obj.id),100);  | 
 | 225 | +    };  | 
 | 226 | + | 
 | 227 | + | 
 | 228 | +    const registerEventHandlers = () =>  {  | 
 | 229 | +        on('chat:message', handleInput);  | 
 | 230 | +        on('add:graphic', saveTokenId);  | 
 | 231 | +        on('change:graphic', rollHitDice);  | 
 | 232 | +    };  | 
 | 233 | + | 
 | 234 | + | 
 | 235 | +    on('ready',() => {  | 
 | 236 | +        checkInstall();  | 
 | 237 | +        registerEventHandlers();  | 
 | 238 | +    });  | 
 | 239 | + | 
 | 240 | + | 
 | 241 | + | 
 | 242 | +    return {  | 
 | 243 | +      /* public interface */  | 
 | 244 | +    };  | 
 | 245 | +      | 
 | 246 | +})();  | 
 | 247 | + | 
 | 248 | +{try{throw new Error('');}catch(e){API_Meta.MonsterHitDice.lineCount=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-API_Meta.MonsterHitDice.offset);}}  | 
0 commit comments