├── .gitignore ├── 5e ├── all_tokens_passive_perception.js ├── animate_tiny_weapons.js ├── apply_damage.js ├── auto_sort_creatures_by_type.js ├── bane.js ├── bardic_insperation.js ├── bless.js ├── divine_smite.js ├── guidance.js ├── heavy_armor_feat_workaround.js ├── initiative_with_disadvantage.js ├── lay_on_hands.js ├── monk_ki.js ├── npcs_to_coin_piles.js ├── rage.js ├── random_cutting_words.js ├── random_inspiration.js ├── random_mockeries.js ├── show_token_actions.js ├── show_token_actions_fix.js ├── show_tokens_resistances.js ├── stealth_check.js ├── tool_proficiency.js └── wild_magic.js ├── LICENSE.md ├── README.md ├── age └── readme.md ├── archived ├── 5e │ ├── fighter_second_wind.js │ ├── great_weapon_master.js │ ├── reroll_bad_d20_with_modifiers.js │ ├── sharpshooter.js │ ├── sneak_attack.js │ └── speed_factor_initiative.js ├── age │ ├── breather.js │ ├── clear_conditions.js │ ├── clear_cover.js │ ├── ding_fries_are_done.js │ ├── first_aid.js │ └── take_cover.js ├── misc │ ├── award_party_xp.js │ ├── close_all_doors.js │ ├── combat_tracker_ac_hp.js │ ├── convert_compendium_to_table.js │ ├── create_chat_message.js │ ├── create_merchant_stock.js │ ├── folder_permission.js │ ├── hide_video_boxes.js │ ├── journal_update_id_with_name.js │ ├── scene_border_walls.js │ └── share_image_via_url.js ├── module-specific │ ├── gm_secret_skill_check.js │ ├── hex.js │ ├── hit_die.js │ ├── hunters-mark-mark_attack_wrapper.js │ ├── hunters_mark-cast_mark.js │ ├── hunters_mark-gm_conditions.js │ ├── open_beyond_sheet_player.js │ ├── pearl_of_power.js │ ├── resolve_surprise.js │ └── token_vision_config_about_time.js ├── pf1e │ ├── award_xp.js │ ├── channel_positive_healing.js │ ├── roll_knowledge_skill_check.js │ └── wand_of_cure_light_wounds.js ├── pf2e │ ├── action_climb.js │ ├── action_conceal_object.js │ ├── action_demoralize.js │ ├── action_disable_device.js │ ├── action_disarm.js │ ├── action_force_open.js │ ├── action_grapple.js │ ├── action_high_jump.js │ ├── action_long_jump.js │ ├── action_pick_lock.js │ ├── action_sense_motive.js │ ├── action_shove.js │ ├── action_stand_up.js │ ├── action_steal.js │ ├── action_trip.js │ ├── alchemist_bomb.js │ ├── animal_rage.js │ ├── apply_condition.js │ ├── apply_spell_effect.js │ ├── blood_magic_proc.js │ ├── consume_arrow.js │ ├── consume_bolt.js │ ├── consume_bullets.js │ ├── craft_item.js │ ├── double_slice.js │ ├── double_slice_ac_shown_in_roll.js │ ├── double_slice_giff.js │ ├── dreams.js │ ├── dueling_cape.js │ ├── dueling_parry.js │ ├── earn_income.js │ ├── fury_instinct_rage.js │ ├── gm_craft_something.js │ ├── gozreh_sickness.js │ ├── hunted_shot.js │ ├── inspire_courage.js │ ├── oracular_curse.js │ ├── parry.js │ ├── persistent_damage_check.js │ ├── premonition_of_avoidance.js │ ├── print_spell_dc.js │ ├── rage_effect.js │ ├── rain_of_embers_stance.js │ ├── raise_shield.js │ ├── repair_item.js │ ├── rest_for_the_night.js │ ├── specialty_crafting.js │ ├── spell_attack.js │ ├── spell_damage.js │ ├── strike_attack.js │ ├── strike_damage.js │ ├── sweep_attack.js │ ├── take_a_breather.js │ ├── terrifying_resistance.js │ ├── treat_wounds.js │ ├── treat_wounds_(fancy).js │ └── treat_wounds_(improved).js ├── roll │ ├── mass_roll_check.js │ └── token_hp.js ├── token │ ├── selective_select.js │ ├── show_token_artwork.js │ └── token_multi_select.js └── wfrp4e │ └── shopify_actor.js ├── dsa5e └── 3d20_skill_roll.js ├── misc ├── actor_to_combat.js ├── ambient_light_quick_edit.js ├── announce_round_number.js ├── auto_script.js ├── circling_lights.js ├── compendium_set_lock_and_visibility.js ├── create_ambient_light.js ├── delete_all_templates.js ├── delete_items_not_in_folders.js ├── draw-walls-around-drawing.js ├── equip_unequip_shield.js ├── find_lights_by_color.js ├── find_selected_wall_ids.js ├── fine_tile_control.js ├── folder_to_compendium.js ├── folder_to_rollable_table.js ├── format_all_scene_notes.js ├── hex_crawl_helper.js ├── import_from_compendium.js ├── jukebox.js ├── lightning_lights_effect.js ├── lock_all_doors.js ├── lock_and_unlock_players.js ├── log_troubleshooting_msg_to_console.js ├── max_npc_hp.js ├── move_walls.js ├── polygon_drawing_to_walls.js ├── rebind_token_actors.js ├── rolltables.js ├── scale_grid_size_to_inches.js ├── show_modules.js ├── tile_toggle_hidden_status.js ├── tile_toggle_locked_status.js ├── tile_xy_adjust.js ├── toggle_playlist.js └── whisper_players.js ├── module-specific ├── readme.md └── token_favorite_action.js ├── module.json ├── packs ├── macros-5e.db ├── macros-age.db ├── macros-dsa5e.db ├── macros-misc.db ├── macros-module-specific.db ├── macros-pf1e.db ├── macros-pf2e.db ├── macros-roll.db ├── macros-token.db └── macros-wfrp4e.db ├── pf1e └── readme.md ├── pf2e ├── change_initiative_skill.js ├── fall_damage.js └── vision_configuration.js ├── roll ├── average_roll_results.js ├── character_stat_roller.js ├── chartopia_roller.js ├── roll_ammunition_die.js ├── roll_die.js ├── roll_initiatives.js ├── roll_table.js └── wandering_monsters.js ├── token ├── actor_selector.js ├── apply_prototype.js ├── bulk_change_initiative.js ├── change_disposition.js ├── clone_token_actor.js ├── end_turn.js ├── light_picker.js ├── light_picker_color.js ├── link_token_to_actor.js ├── mirror_token_image.js ├── randomize_wildcard_tokens.js ├── remove_conditions.js ├── rename_token_and_actor.js ├── select_token_in_stack.js ├── set_name_and_bars.js ├── shrink_or_enlarge.js ├── switch_images.js ├── token_behavior.js ├── token_reiconize.js ├── token_vision_config.js └── unlink_tokens_from_actor.js └── wfrp4e └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | .Spotlight-V100 7 | .Trashes 8 | ehthumbs.db 9 | Thumbs.db -------------------------------------------------------------------------------- /5e/all_tokens_passive_perception.js: -------------------------------------------------------------------------------- 1 | // Pull the passive perception of each token in the current scene and whisper the results to the GM. 2 | // Only tested with the 5e System in Foundry. 3 | // Author: @Drunemeton#7955. Based on the original macro by author @Erogroth#7134. 4 | 5 | // Initalize variables. 6 | let pcArray = []; 7 | let npcArray = []; 8 | let messageContentPC = ""; 9 | let messageContentNPC = ""; 10 | let messageHeaderPC = "PC Passive Perception
"; 11 | let messageHeaderNPC = "NPC Passive Perception
"; 12 | 13 | // Gather tokens in the current scene into an array. 14 | let tokens = canvas.tokens.placeables.filter((token) => token.data && token.actor); 15 | 16 | // From the tokens array sort into PC and NPC arrays. 17 | for (let count of tokens) { 18 | let tokenType = count.actor.data.type; 19 | let tokenName = count.data.name; 20 | let tokenPassive = count.actor.data.data.skills.prc.passive; 21 | 22 | if(tokenType === "character") { 23 | pcArray.push({ name: tokenName, passive: tokenPassive }); 24 | } 25 | if(tokenType === "npc") { 26 | npcArray.push({ name: tokenName, passive: tokenPassive }); 27 | } 28 | } 29 | 30 | // Sort each array. 31 | sortArray(pcArray); 32 | sortArray(npcArray); 33 | 34 | // Build chat message, with PCs first, then NPCs. 35 | for (let numPC of pcArray) { 36 | messageContentPC += `${numPC.name}: ${numPC.passive}
`; 37 | } 38 | for (let numNPC of npcArray) { 39 | messageContentNPC += `${numNPC.name}: ${numNPC.passive}
`; 40 | } 41 | 42 | let chatMessage = (messageHeaderPC + messageContentPC + `
` + messageHeaderNPC + messageContentNPC); 43 | 44 | let chatData = { 45 | user: game.user._id, 46 | speaker: ChatMessage.getSpeaker(), 47 | content: chatMessage, 48 | whisper: game.users.filter((u) => u.isGM).map((u) => u._id), 49 | }; 50 | 51 | // Display chat message. 52 | ChatMessage.create(chatData, {}); 53 | 54 | // Sort each array by Name. 55 | function sortArray(checkArray) { 56 | checkArray.sort(function (a, b) { 57 | var nameA = a.name.toUpperCase(); // ignore upper and lowercase 58 | var nameB = b.name.toUpperCase(); // ignore upper and lowercase 59 | if (nameA < nameB) { 60 | return -1; 61 | } 62 | if (nameA > nameB) { 63 | return 1; 64 | } 65 | // names must be equal 66 | return 0; 67 | }); 68 | 69 | // Sort array by Passive Perception. 70 | checkArray.sort(function (a, b) { 71 | return b.passive - a.passive; 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /5e/apply_damage.js: -------------------------------------------------------------------------------- 1 | // Displays a prompt which asks for an amount of damage to inflict 2 | // Inflicts the input damage amount to all selected tokens 3 | 4 | let content = ` 5 |
6 |
7 | 8 | 9 |
10 |
` 11 | 12 | new Dialog({ 13 | title: 'How much damage should be applied (negative for healing)?', 14 | content: content, 15 | buttons:{ 16 | yes: { 17 | icon: "", 18 | label: `Apply Damage` 19 | } 20 | }, 21 | 22 | default:'yes', 23 | 24 | close: html => { 25 | let result = html.find('input[name=\'inputField\']'); 26 | if (result.val() !== '') { 27 | let damage = result.val(); 28 | let allSelected = canvas.tokens.controlled 29 | 30 | allSelected.forEach(selected => { 31 | let actor = selected.actor 32 | let hp = actor.data.data.attributes.hp.value 33 | let maxHp = actor.data.data.attributes.hp.max 34 | 35 | let updatedHp = damage > hp ? 0 : hp - damage 36 | 37 | actor.update({'data.attributes.hp.value': updatedHp > maxHp ? maxHp : updatedHp}) 38 | 39 | console.log(actor) 40 | }) 41 | } 42 | } 43 | }).render(true); 44 | 45 | (async () => { 46 | await new Promise(resolve => setTimeout(resolve, 20)); 47 | let input = $('#damage-amount').focus(); 48 | })(); 49 | -------------------------------------------------------------------------------- /5e/auto_sort_creatures_by_type.js: -------------------------------------------------------------------------------- 1 | /** ##################################################################################### * 2 | * This macro loops over the existing creature actors in the actor directory and creates * 3 | * folders corresponding to the creature type. It then sorts the creatures to the * 4 | * corresponding folder to make them quiet a bit more manageable, especially when you've * 5 | * created/imported a lot of creatures. * 6 | * I would recommend using the "Compendium Folders" module in combination to keep your * 7 | * initial load as fast as possible when you have a lot of actors. * 8 | * ##################################################################################### * 9 | * Credits to ZetaDracon#7558 and Freeze#2689 * 10 | * ##################################################################################### */ 11 | const folderData = { 12 | color: "", 13 | parent: "", 14 | sorting: "a", 15 | type: "Actor" 16 | }; 17 | // lets make the folders. 18 | for(let actor of game.actors) { 19 | const type = actor.data.data.details.type?.value; 20 | if(!type) continue; // so player characters get filtered out. 21 | const folder = game.folders.find(f => f.name.toLowerCase() === type && f.type === "Actor"); 22 | if(!folder) await Folder.create(mergeObject({name: type}, folderData)); 23 | } 24 | // lets update the actors. 25 | const updates = game.actors.reduce((acc, a) => { 26 | const type = a.data.data.details.type?.value; 27 | if(!type) return acc; // so player characters get filtered out. 28 | let folderId = game.folders.find(f => f.name.toLowerCase() === type && f.type === "Actor").id; 29 | acc.push({_id: a.id, folder: folderId}); 30 | return acc; 31 | }, []); 32 | await Actor.updateDocuments(updates); -------------------------------------------------------------------------------- /5e/bane.js: -------------------------------------------------------------------------------- 1 | // new build for Bane macro by Penguin#0949 with help from Kotetsushin#7680 2 | // version beta 4.2.0 3 | 4 | // user notes 5 | // this macro is inteded for use by the recipient of the Bane spell in D&D 5e on Forge VTT 6 | // N.B. every recipient will need to use this macro independantly on their own Actor/token. 7 | 8 | //user modifiable declarations CHANGE AT YOUR OWN RISK 9 | const baneIconPath = 'icons/svg/degen.svg'; 10 | let baneMsg = ' is Baned!'; 11 | let endbaneMsg = ' is no longer Baned.'; 12 | 13 | //fixed declarations DO NOT MODIFY 14 | let macroActor = token.actor; 15 | let chatMsg = ''; 16 | let Baned = macroActor.effects.find(i => i.data.label === "Baned") 17 | let bane = { 18 | changes: [ 19 | { 20 | key: "data.bonuses.mwak.attack", 21 | mode: 2, 22 | priority: 20, 23 | value: "-1d4", 24 | }, 25 | { 26 | key: "data.bonuses.rwak.attack", 27 | mode: 2, 28 | priority: 20, 29 | value: "-1d4", 30 | }, 31 | { 32 | key: "data.bonuses.msak.attack", 33 | mode: 2, 34 | priority: 20, 35 | value: "-1d4", 36 | }, 37 | { 38 | key: "mdata.bonuses.rsak.attack", 39 | mode: 2, 40 | priority: 20, 41 | value: "-1d4", 42 | }, 43 | { 44 | key: "data.bonuses.abilities.save", 45 | mode: 2, 46 | priority: 20, 47 | value: "-1d4", 48 | }, 49 | ], 50 | duration: { 51 | seconds: 60, 52 | }, 53 | icon: baneIconPath, 54 | label: "Baned" 55 | } 56 | //identify token 57 | if (macroActor === undefined || macroActor === null) { 58 | ui.notifications.warn("Please select a token first."); 59 | } 60 | else { 61 | // If already bless 62 | if (Baned) { 63 | macroActor.deleteEmbeddedDocuments("ActiveEffect", [Baned.id]) 64 | // anounce to chat 65 | chatMsg = `${macroActor.name} ${endbaneMsg}`; 66 | } 67 | // if not already bless 68 | else { 69 | macroActor.createEmbeddedDocuments("ActiveEffect", [bane]) 70 | // anounce to chat 71 | chatMsg = `${macroActor.name} ${baneMsg}`; 72 | } 73 | // write to chat if needed: 74 | if (chatMsg !== '') { 75 | let chatData = { 76 | user: game.user._id, 77 | speaker: ChatMessage.getSpeaker(), 78 | content: chatMsg 79 | }; 80 | ChatMessage.create(chatData, {}); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /5e/bardic_insperation.js: -------------------------------------------------------------------------------- 1 | let bardinsp_data = null; 2 | 3 | //Check if have Bardic Insperation 4 | 5 | if (canvas.tokens.controlled.length == 1){ 6 | //console.log(canvas.tokens.controlled); 7 | let owner_actor = canvas.tokens.controlled[0].actor; 8 | 9 | for (let item in owner_actor.data.items){ 10 | if (item.name == "Bardic Inspiration"){ 11 | bardinsp_data = item; 12 | break; 13 | } 14 | } 15 | } 16 | 17 | //Get the Target of Bardic Insperation 18 | if (canvas.tokens._hover != null){ 19 | let bardinsp_token = canvas.tokens._hover; 20 | 21 | 22 | const effect = bardinsp_token.actor.effects.entries; 23 | 24 | bardinsp_token.toggleEffect("systems/dnd5e/icons/skills/yellow_08.jpg"); 25 | } -------------------------------------------------------------------------------- /5e/bless.js: -------------------------------------------------------------------------------- 1 | // new build for bless macro by Penguin#0949 with help from Kotetsushin#7680 2 | // version beta 4.2.0 3 | 4 | // user notes 5 | // this macro is inteded for use by the recipient of the bless spell in D&D 5e on Forge VTT 6 | // N.B. every recipient will need to use this macro independantly on their own Actor/token. 7 | 8 | //user modifiable declarations CHANGE AT YOUR OWN RISK 9 | const blessIconPath = 'icons/svg/regen.svg'; 10 | let blessMsg = ' is Blessed!'; 11 | let endblessMsg = ' is no longer Blessed'; 12 | 13 | //fixed declarations DO NOT MODIFY 14 | let macroActor = token.actor; 15 | let chatMsg = ''; 16 | let Blessd = macroActor.effects.find(i => i.data.label === "Blessed") 17 | let bless = { 18 | changes: [ 19 | { 20 | key: "data.bonuses.mwak.attack", 21 | mode: 2, 22 | priority: 20, 23 | value: "+1d4", 24 | }, 25 | { 26 | key: "data.bonuses.rwak.attack", 27 | mode: 2, 28 | priority: 20, 29 | value: "+1d4", 30 | }, 31 | { 32 | key: "data.bonuses.msak.attack", 33 | mode: 2, 34 | priority: 20, 35 | value: "+1d4", 36 | }, 37 | { 38 | key: "data.bonuses.rsak.attack", 39 | mode: 2, 40 | priority: 20, 41 | value: "+1d4", 42 | }, 43 | { 44 | key: "data.bonuses.abilities.save", 45 | mode: 2, 46 | priority: 20, 47 | value: "+1d4", 48 | }, 49 | ], 50 | duration: { 51 | seconds: 60, 52 | }, 53 | icon: blessIconPath, 54 | label: "Blessed" 55 | } 56 | //identify token 57 | if (macroActor === undefined || macroActor === null) { 58 | ui.notifications.warn("Please select a token first."); 59 | } 60 | else { 61 | // If already bless 62 | if (Blessd) { 63 | macroActor.deleteEmbeddedDocuments("ActiveEffect", [Blessd.id]) 64 | // anounce to chat 65 | chatMsg = `${macroActor.name} ${endblessMsg}`; 66 | } 67 | // if not already bless 68 | else { 69 | macroActor.createEmbeddedDocuments("ActiveEffect", [bless]) 70 | // anounce to chat 71 | chatMsg = `${macroActor.name} ${blessMsg}`; 72 | } 73 | // write to chat if needed: 74 | if (chatMsg !== '') { 75 | let chatData = { 76 | user: game.user._id, 77 | speaker: ChatMessage.getSpeaker(), 78 | content: chatMsg 79 | }; 80 | ChatMessage.create(chatData, {}); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /5e/guidance.js: -------------------------------------------------------------------------------- 1 | // new build for guidance macro by Mr.White and Penguin#0949 and with no help all of Kotetsushin#7680 trust me 2 | // version beta 4.2.0 3 | 4 | // user notes 5 | // this macro is inteded for use by the recipient of the bless spell in D&D 5e on Forge VTT 6 | // N.B. every recipient will need to use this macro independantly on their own Actor/token. 7 | 8 | //user modifiable declarations CHANGE AT YOUR OWN RISK 9 | const GuidIconPath = 'icons/svg/windmill.svg'; 10 | let GuideMsg = ' is guided!'; 11 | let endGuideMsg = ' is no longer guided.'; 12 | 13 | //fixed declarations DO NOT MODIFY 14 | let chatMsg = ''; 15 | let macroActor = token.actor; 16 | let Guided = macroActor.effects.find(i => i.data.label === "Guided") 17 | let Guide = { 18 | changes: [ 19 | { 20 | key: "data.bonuses.abilities.check", 21 | mode: 2, 22 | priority: 20, 23 | value: "+1d4", 24 | }, 25 | ], 26 | duration: { 27 | seconds: 60, 28 | }, 29 | icon: GuidIconPath, 30 | label: "Guided" 31 | } 32 | //identify token 33 | if (macroActor === undefined || macroActor === null) { 34 | ui.notifications.warn("Please select a token first."); 35 | } 36 | else { 37 | // If already guided 38 | if (Guided) { 39 | macroActor.deleteEmbeddedDocuments("ActiveEffect", [Guided.id]); 40 | // anounce to chat 41 | chatMsg = `${macroActor.name} ${endGuideMsg}`; 42 | } 43 | // if not already guided 44 | else { 45 | macroActor.createEmbeddedDocuments("ActiveEffect", [Guide]); 46 | // anounce to chat 47 | chatMsg = `${macroActor.name} ${GuideMsg}`; 48 | } 49 | // write to chat if needed: 50 | if (chatMsg !== '') { 51 | let chatData = { 52 | user: game.user._id, 53 | speaker: ChatMessage.getSpeaker(), 54 | content: chatMsg 55 | }; 56 | ChatMessage.create(chatData, {}); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /5e/heavy_armor_feat_workaround.js: -------------------------------------------------------------------------------- 1 | //Crude but effective way to simulate Heavy Armor Master. 2 | //Every time the player takes eligible damage, they can just click this macro with their token selected to "get their 3HP back." 3 | //Questions? Ask in #macro-polo on Discord. If absolutely needed, please ping Norc#5108. 4 | 5 | //Known minor limitation: Does not take into account temp HP AT ALL. 6 | 7 | function modifyHP(token, amount) { 8 | let hp_cur = token.actor.data.data.attributes.hp.value; 9 | let hp_max = token.actor.data.data.attributes.hp.max; 10 | let hp_min = token.actor.data.data.attributes.hp.min; 11 | hp_cur = (hp_cur+amount > hp_max) ? hp_max : hp_cur+amount; 12 | hp_cur = (hp_cur < hp_min) ? hp_min : hp_cur; 13 | token.actor.update({'data.attributes.hp.value': parseInt(hp_cur)}); 14 | return hp_cur; 15 | } 16 | 17 | if(token) { 18 | //Note: Just change the number after the comma to heal/receive other HP values. Negative numbers indicate damage. 19 | modifyHP(token,3); 20 | } else { 21 | ui.notifications.notify("Please select a token."); 22 | }6 23 | -------------------------------------------------------------------------------- /5e/initiative_with_disadvantage.js: -------------------------------------------------------------------------------- 1 | // Initiative with Disadvantage by Nulmas#9462 2 | // Thanks to Freeze#2689, vance#1935 and u/Azzu for the help. 3 | 4 | // This macro allows GMs and players to roll for Initiative with disadvantage when playing D&D 5e. Hopefully it won't be needed for long and the option for it will be added 5 | // to the system in a future release. 6 | 7 | // The macro will roll for all the selected tokens and add them to the combat if they aren't in it already. It will also check if you are using Dex as a tiebreaker and roll 8 | // accordingly. 9 | 10 | // BEWARE: If a token has already rolled for initiative and you use this macro with it selected, the new initiative will replace the old one. I considered changing this, but 11 | // decided it's worth keeping it this way in case a player or GM rolls for initiative without disadvantage by mistake. 12 | 13 | (async () => { 14 | if (canvas.tokens.controlled.length === 0) return ui.notifications.error("Choose tokens to roll for"); 15 | await canvas.tokens.toggleCombat(); 16 | let chosenTokens = canvas.tokens.controlled; 17 | let tieBreakerCheck = game.settings.get("dnd5e", "initiativeDexTiebreaker") ? 1 : 0; //Checks if Dex tiebreaker is being used 18 | let initiatives = chosenTokens.map(t => { 19 | let chosenActor = t.actor; 20 | let advantage = chosenActor.getFlag("dnd5e", "initiativeAdv") ? 1 : 0; 21 | let init = chosenActor.data.data.attributes.init.total; 22 | let tieBreaker = chosenActor.data.data.abilities.dex.value/100; 23 | let roll = new Roll(`${2 - advantage}d20kl + ${init} + ${tieBreaker * tieBreakerCheck}`).roll({async: false}); 24 | roll.toMessage({speaker: ChatMessage.getSpeaker({token: t.document})}); 25 | let combatantId = t.combatant.id; 26 | return{ 27 | _id: combatantId, 28 | initiative: roll.total, 29 | }; 30 | }); 31 | await game.combat.updateEmbeddedDocuments("Combatant", initiatives); 32 | })(); 33 | -------------------------------------------------------------------------------- /5e/random_cutting_words.js: -------------------------------------------------------------------------------- 1 | // Courtesy of @Zarek 2 | // Selected target receives a random cutting word from a table called "Mockeries" along with the roll reduction. 3 | // You can find a mockeries table in the community table module. 4 | 5 | let cuttingWords = async () => { 6 | // Setup variables 7 | let tableName = "mockeries"; 8 | let mockery = "Now go away or I shall taunt you a second time-a!"; // if table can't be found, use this. 9 | 10 | if (!actor) { 11 | ui.notifications.warn("You must have an actor selected."); 12 | return 13 | } 14 | 15 | let actorLevels = actor.data.data.levels || 1; 16 | let table = game.tables.contents.find(t => t.name == tableName); 17 | // Get Targets name 18 | const targetId = game.user.targets.ids[0]; 19 | const targetToken = canvas.tokens.get(targetId); 20 | if (!targetToken) { 21 | ui.notifications.warn("You must target a token."); 22 | return 23 | } 24 | const targetName = targetToken.name; 25 | 26 | // Roll the result, and mark it drawn 27 | if (table) { 28 | if (checkTable(table)) { 29 | let roll = await table.roll(); 30 | let result = roll.results[0]; 31 | mockery = result.data.text; 32 | await table.updateEmbeddedDocuments("TableResult", [{ 33 | _id: result.id, 34 | drawn: true 35 | }]); 36 | } 37 | } 38 | 39 | function checkTable(table) { 40 | let results = 0; 41 | for (let data of table.data.results) { 42 | if (!data.drawn) { 43 | results++; 44 | } 45 | } 46 | if (results < 1) { 47 | table.reset(); 48 | ui.notifications.notify("Table Reset") 49 | return false 50 | } 51 | return true 52 | } 53 | 54 | let dieType = 'd6'; 55 | if (actorLevels >= 15) { 56 | dieType = 'd12'; 57 | } else if (actorLevels >= 10) { 58 | dieType = 'd10'; 59 | } else if (actorLevels >= 5) { 60 | dieType = 'd8'; 61 | } 62 | 63 | let messageContent = `

${targetName} Reduce your roll by: [[1${dieType}]].

` 64 | messageContent += `

${token.name} exclaims "${mockery}"

` 65 | messageContent += `
Cutting Words 66 |

When a creature that you can see within 60 feet of you makes an Attack roll, an ability check, or a damage roll, you can use your Reaction to expend one of your uses of Bardic Inspiration, 67 | rolling a Bardic Inspiration die and subtracting the number rolled from the creature’s roll.

68 |

You can choose to use this feature after the creature makes its roll, but before the GM determines whether the Attack roll or ability check succeeds or fails, or before the creature deals its damage. 69 | The creature is immune if it can’t hear you or if it’s immune to being Charmed.

` 70 | 71 | // create the message 72 | if (messageContent !== '') { 73 | let chatData = { 74 | user: game.user.id, 75 | speaker: ChatMessage.getSpeaker(), 76 | content: messageContent, 77 | }; 78 | ChatMessage.create(chatData, {}); 79 | } 80 | }; 81 | 82 | cuttingWords(); 83 | -------------------------------------------------------------------------------- /5e/random_inspiration.js: -------------------------------------------------------------------------------- 1 | // Courtesy of @Zarek 2 | // Selected target receives a random inspiration from a table called "inspirations". 3 | // You can find a table called inspirations in the community tables module 4 | 5 | // Setup variables 6 | let tableName = "Inspirations"; 7 | 8 | let bardicInspiration = async() => { 9 | if (!actor) { 10 | ui.notifications.warn("You must have an actor selected."); 11 | return 12 | } 13 | 14 | // Get Targets name 15 | let actorLevels = actor.data.data.levels || 1; 16 | const targetId = game.user.targets.ids[0]; 17 | const targetToken = canvas.tokens.get(targetId); 18 | if (!targetToken) { 19 | ui.notifications.warn("You must target a token."); 20 | return 21 | } 22 | const targetName = targetToken.name; 23 | 24 | 25 | let table = game.tables.contents.find(t => t.name == tableName); 26 | 27 | //default inspiration if no table is found. 28 | //let inspiration = "Cowards die many times before their deaths; the valiant never taste death but once."; 29 | let inspiration = `I don't know what effect ${targetName} will have upon the enemy, but, by God, he terrifies me.`; 30 | 31 | // Roll the result, and mark it drawn 32 | if (table) { 33 | if (checkTable(table)) { 34 | // let result = table.roll()[1]; 35 | let roll = await table.roll(); 36 | let result = roll.results[0]; 37 | inspiration = result.data.text; 38 | await table.updateEmbeddedDocuments("TableResult", [{ 39 | _id: result.id, 40 | drawn: true 41 | }]); 42 | } 43 | } 44 | 45 | function checkTable(table) { 46 | let results = 0; 47 | for (let data of table.data.results) { 48 | if (!data.drawn) { 49 | results++; 50 | } 51 | } 52 | if (results < 1) { 53 | table.reset(); 54 | ui.notifications.notify("Table Reset") 55 | return false 56 | } 57 | return true 58 | } 59 | 60 | let dieType = 'd6'; 61 | if (actorLevels >= 15) { 62 | dieType = 'd12'; 63 | } else if (actorLevels >= 10) { 64 | dieType = 'd10'; 65 | } else if (actorLevels >= 5) { 66 | dieType = 'd8'; 67 | } 68 | 69 | let messageContent = ''; 70 | messageContent += `

${token.name} exclaims "${inspiration}"

` 71 | messageContent += `

${targetName} is inspired.

` 72 | messageContent += `
Bardic Inspiration

${targetName} gains one Bardic Inspiration die, a ${dieType}.
Once within the next 10 minutes, ${targetName} can roll the die and add the number rolled to one ability check, attack roll, or saving throw. ${targetName} can wait until after it rolls the d20 before deciding to use the Bardic Inspiration die, but must decide before the DM says whether the roll succeeds or fails. Once the Bardic Inspiration die is rolled, it is lost.

` 73 | 74 | // create the message 75 | if (messageContent !== '') { 76 | let chatData = { 77 | user: game.user.id, 78 | speaker: ChatMessage.getSpeaker(), 79 | content: messageContent, 80 | }; 81 | ChatMessage.create(chatData, {}); 82 | } 83 | }; 84 | bardicInspiration(); 85 | -------------------------------------------------------------------------------- /5e/random_mockeries.js: -------------------------------------------------------------------------------- 1 | // Courtesy of @Zarek 2 | // Selected target receives a random mockery from a table called "mockeries" along with the DC and damage. 3 | // You can find a table called mockeries in the community tables module. 4 | 5 | 6 | let tableName = "mockeries"; 7 | // default mockery if no table found. 8 | let mockery = "Now go away or I shall taunt you a second time-a!"; 9 | 10 | let viciousMockeries = async () => { 11 | if (!actor) { 12 | ui.notifications.warn("You must have an actor selected."); 13 | return 14 | } 15 | 16 | let actorLevels = actor.data.data.levels || 1; 17 | let table = game.tables.contents.find(t => t.name == tableName); 18 | 19 | // Get Targets name 20 | const targetId = game.user.targets.ids[0]; 21 | const targetToken = canvas.tokens.get(targetId); 22 | if (!targetToken) { 23 | ui.notifications.warn("You must target a token."); 24 | return 25 | } 26 | const targetName = targetToken.name; 27 | 28 | // Roll the result, and mark it drawn 29 | if (table) { 30 | if (checkTable(table)) { 31 | let roll = await table.roll(); 32 | let result = roll.results[0]; 33 | mockery = result.data.text; 34 | await table.updateEmbeddedDocuments("TableResult", [{ 35 | _id: result.id, 36 | drawn: true 37 | }]); 38 | } 39 | } 40 | 41 | function checkTable(table) { 42 | let results = 0; 43 | for (let data of table.data.results) { 44 | if (!data.drawn) { 45 | results++; 46 | } 47 | } 48 | if (results < 1) { 49 | table.reset(); 50 | ui.notifications.notify("Table Reset") 51 | return false 52 | } 53 | return true 54 | } 55 | 56 | // Add a message with damage roll 57 | let numDie = 1; 58 | if (actorLevels >= 17) { 59 | numDie = 4; 60 | } else if (actorLevels >= 11) { 61 | numDie = 3; 62 | } else if (actorLevels >= 5) { 63 | numDie = 2; 64 | } 65 | 66 | let messageContent = `

${targetName} Roll WIS save DC [[8+${actor.data.data.abilities.cha.mod}+@attributes.prof]] or take [[${numDie}d4]] damage and have disadvantage.

` 67 | messageContent += `

${token.name} exclaims "${mockery}"

` 68 | messageContent += `
Vicious Mockery

You unleash a string of insults laced with subtle enchantments at a creature you can see within range. If the target can hear you (though it need not understand you), it must succeed on a Wisdom saving throw or take 1d4 psychic damage and have disadvantage on the next attack roll it makes before the end of its next turn.

69 |

This spell’s damage increases by 1d4 when you reach 5th level ([[/r 2d4]]), 11th level ([[/r 3d4]]), and 17th level ([[/r 4d4]]).

` 70 | 71 | // create the message 72 | if (messageContent !== '') { 73 | let chatData = { 74 | user: game.user.id, 75 | speaker: ChatMessage.getSpeaker(), 76 | content: messageContent, 77 | }; 78 | ChatMessage.create(chatData, {}); 79 | } 80 | }; 81 | 82 | viciousMockeries(); 83 | -------------------------------------------------------------------------------- /5e/show_tokens_resistances.js: -------------------------------------------------------------------------------- 1 | // Prints the condition immunities, damage immunities, damage resistances and damage vulnerabilities of the currently selected token(s). 2 | // Damage types that appeared in the previous chat message (e.g. due to a roll) are highlighted in red. 3 | // A DM can call this macro after a player rolled damage to quickly see if they need to apply full, half or double damage. 4 | // 5 | // Author: https://github.com/Nijin22 6 | // Licence: MIT, see https://choosealicense.com/licenses/mit/ 7 | 8 | const damageTypes = ["Acid", "Bludgeoning", "Cold", "Fire", "Force", "Lightning", "Necrotic", "Piercing", "Non-Magical Physical", 9 | "Piercing", "Poison", "Psychic", "Radiant", "Slashing", "Thunder", 10 | "Bludgeoning, Piercing, and Slashing from Nonmagical Attacks"]; 11 | 12 | let msg = ""; 13 | let previousMessage; 14 | try { 15 | previousMessage = game.messages.entries[game.messages.entries.length-1].data.content; 16 | } catch (e) { 17 | // No previous message in log. Default to an empty string. 18 | previousMessage = ""; 19 | } 20 | 21 | // Enable case-insensitive replacements 22 | // Source: https://stackoverflow.com/a/7313467 23 | String.prototype.replaceAllCaseInsensitive = function(strReplace, strWith) { 24 | // See http://stackoverflow.com/a/3561711/556609 25 | var esc = strReplace.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 26 | var reg = new RegExp(esc, 'ig'); 27 | return this.replace(reg, strWith); 28 | }; 29 | 30 | 31 | // Get traits 32 | const traits = new Map(); 33 | traits.set("ci", "Condition Immunities"); 34 | traits.set("di", "Damage Immunities (No damage)"); 35 | traits.set("dr", "Damage Resistances (Half damage)"); 36 | traits.set("dv", "Damage Vulnerabilities (Double dmg)"); 37 | canvas.tokens.controlled.forEach(token => { 38 | let name = token.actor.name; 39 | msg += `

${name}

`; 40 | 41 | traits.forEach((traitDescr, traitId, map) => { 42 | // Clone 'default' 5e trait array 43 | var allTraits = [...token.actor.data.data.traits[traitId].value]; 44 | 45 | // Custom traits 46 | allTraits = allTraits.concat(token.actor.data.data.traits[traitId].custom.split(";").map(x => x.trim())); 47 | 48 | var printableTraits = allTraits.join("; "); 49 | if (printableTraits.length == 0) { 50 | printableTraits = "-"; 51 | } 52 | msg += `

${traitDescr}

${printableTraits}

`; 53 | }); 54 | }); 55 | 56 | // highlight words from previous message 57 | let damageTypesOfPrevousMsg = []; 58 | damageTypes.forEach(damageType => { 59 | if (previousMessage.toLowerCase().indexOf(damageType.toLowerCase()) != -1){ 60 | damageTypesOfPrevousMsg.push(damageType); 61 | } 62 | }); 63 | damageTypesOfPrevousMsg.forEach(damageType => { 64 | msg = msg.replaceAllCaseInsensitive(damageType, `${damageType}`); 65 | }); 66 | 67 | 68 | if (msg.length === 0) { 69 | msg = "No tokens selected."; 70 | } 71 | 72 | ui.notifications.info(msg); 73 | 74 | // Post message to self 75 | /*ChatMessage.create({ 76 | content: msg, 77 | whisper: [game.user._id] 78 | });*/ 79 | -------------------------------------------------------------------------------- /5e/stealth_check.js: -------------------------------------------------------------------------------- 1 | // Grabs selected tokens and rolls a stealth check against all other tokens passive perception on the map. Then returns the result. 2 | 3 | // getting all actors of selected tokens 4 | let actors = canvas.tokens.controlled.map(({ actor }) => actor); 5 | 6 | // if there are no selected tokens, roll for the player's character. 7 | if (actors.length < 1 && game.user.character) { 8 | actors = [game.user.character]; 9 | } 10 | const validActors = actors.filter(actor => actor != null); 11 | 12 | let messageContent; 13 | if (validActors.length) { 14 | messageContent = 'pp = passive perception
'; 15 | } else { 16 | messageContent = 'No tokens selected, please select at least one.'; 17 | } 18 | // roll for every actor 19 | for (const selectedActor of validActors) { 20 | const stealthMod = selectedActor.data.data.skills.ste.total; // stealth roll 21 | const result = await new Roll(`1d20+${stealthMod}`).roll({async: true}); 22 | const stealth = result.total; // rolling the formula 23 | messageContent += `

${selectedActor.name} stealth roll was a ${stealth} (1d20 + ${stealthMod}).

`; // creating the output string 24 | 25 | // grab a list of unique tokens then check their passive perception against the rolled stealth. 26 | const uniqueActor = {}; 27 | const caughtBy = canvas.tokens.placeables 28 | .filter(token => !!token.actor) 29 | .filter(({ actor }) => actor.data.data.attributes.hp.value > 0) // filter out dead creatures. 30 | .filter(({ actor }) => { // filter out duplicate token names. ie: we assume all goblins have the same passive perception 31 | if (uniqueActor[actor.name]) { 32 | return false; 33 | } 34 | uniqueActor[actor.name] = true; 35 | return true; 36 | }) 37 | .filter(({ actor }) => { 38 | return selectedActor.id !== actor.id; // Don't check to see if the token sees himself. 39 | }) 40 | .filter(({ actor }) => actor.data.data.skills.prc.passive >= stealth); // check map tokens passives with roller stealth 41 | 42 | if (!caughtBy.length) { 43 | messageContent += 'Stealth successful!
'; 44 | } else { 45 | messageContent += 'Stealth questionable:
'; 46 | caughtBy.map(({ actor }) => { 47 | messageContent += `${actor.name} pp(${actor.data.data.skills.prc.passive}).
`; 48 | }); 49 | } 50 | } 51 | 52 | // create the message 53 | const chatData = { 54 | user: game.user._id, 55 | speaker: game.user, 56 | content: messageContent, 57 | whisper: game.users.filter((u) => u.isGM).map((u) => u._id), 58 | }; 59 | ChatMessage.create(chatData, {}); 60 | -------------------------------------------------------------------------------- /5e/wild_magic.js: -------------------------------------------------------------------------------- 1 | function printMessage(message){ 2 | let chatData = { 3 | user : game.user.id, 4 | content : message, 5 | blind: true, 6 | whisper : game.users.filter(u => u.isGM).map(u => u.id) 7 | }; 8 | 9 | ChatMessage.create(chatData,{}); 10 | } 11 | 12 | 13 | const roll = new Roll(`1d20`); 14 | let result = await roll.roll(); 15 | 16 | if (result.total == 1) { 17 | printMessage('

Wild magic has been triggered.

'); 18 | } 19 | else{ 20 | printMessage("Wild magic was not triggered on a " + result.total); 21 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foundry VTT Macros Repository 2 | 3 | ![Latest Version](https://img.shields.io/badge/dynamic/json.svg?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffoundry-vtt-community%2Fmacros%2Fmaster%2Fmodule.json&label=Latest%20Release&prefix=v&query=$.version&colorB=red&style=for-the-badge) 4 | [![Forge Installs](https://img.shields.io/badge/dynamic/json?label=Forge%20Installs&query=package.installs&suffix=%25&url=https%3A%2F%2Fforge-vtt.com%2Fapi%2Fbazaar%2Fpackage%2Ffoundry_community_macros&colorB=006400&style=for-the-badge)](https://forge-vtt.com/bazaar#package=foundry_community_macros) 5 | [![Foundry Hub Endorsements](https://img.shields.io/endpoint?logoColor=white&url=https%3A%2F%2Fwww.foundryvtt-hub.com%2Fwp-json%2Fhubapi%2Fv1%2Fpackage%2Ffoundry_community_macros%2Fshield%2Fendorsements&style=for-the-badge)](https://www.foundryvtt-hub.com/package/foundry_community_macros/) 6 | 7 | Foundry community-contributed macros are noted here and merged into the Foundry Community Macros module for ease of use. 8 | 9 | > Macros may cause unintended side effects, such as issues with performance. Please read the comments in each macro to understand how it works before running! 10 | 11 | ## Installation 12 | 13 | ### Usage as a Module 14 | 15 | You can now install this module automatically by specifying the following public module URL : 16 | 17 | `https://raw.githubusercontent.com/foundry-vtt-community/macros/main/module.json` 18 | 19 | As GM go to the **Manage Modules** options menu in your **World Settings** tab then enable the **Foundry Community Macros** module. 20 | 21 | This module adds all of the community macros as a Compendium Packs called **FVTT Community Macros**. 22 | 23 | You can open these packs, right click and click on import the macros you want. You can then add these macros to the **Macro Toolbar** at the bottom of the screen. 24 | 25 | ### Usage without the Module: 26 | 1. Download the `.js` file to your machine 27 | 2. Create a new macro in Foundry VTT 28 | 3. Give it a name and set the **Type** to "Script" 29 | 4. Use a text editor, such as Notepad or vscode, to open the download `.js` file and copy the contents 30 | 5. Paste the contents into the **Command** section of the new macro 31 | 6. Click "Save Macro" 32 | 7. Celebrate 🎉 33 | 34 | ## Development/Contributing 35 | 36 | To clone this repository, along with every macro in it, use the following command: 37 | 38 | ``` 39 | git clone https://github.com/foundry-vtt-community/macros.git 40 | ``` 41 | 42 | You can make pull requests to add or update macros here: [https://github.com/foundry-vtt-community/macros/pulls](https://github.com/foundry-vtt-community/macros/pulls) 43 | 44 | 1. Navigate to the directory where you wish to add your file 45 | 2. Click on add file at the top 46 | 3. Name your file with a `.js` ending and paste your content and save (add details in the description field of your `.js` file) 47 | 4. Save the file and then open a pull request (GitHub should walk you through these steps) 48 | -------------------------------------------------------------------------------- /age/readme.md: -------------------------------------------------------------------------------- 1 | age folder -------------------------------------------------------------------------------- /archived/5e/reroll_bad_d20_with_modifiers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rolls a d20. If the roll is below a 3, it rerolls the value. Adds perception total + 1 as well. 3 | */ 4 | 5 | let dice = new Roll('1d20 + @skills.prc.total + 1').roll(); 6 | if (dice.total <= (4 + actor.data.data.skills.prc.total)) dice.reroll(); 7 | dice.toMessage(); 8 | -------------------------------------------------------------------------------- /archived/5e/sharpshooter.js: -------------------------------------------------------------------------------- 1 | if (!Boolean(actor)) return ui.notifications.warn("Please select a token."); 2 | if (!Boolean(actor.items.find(i => i.name === 'Sharpshooter'))) return ui.notifications.warn("Please select a single token with the Sharpshooter feat."); 3 | 4 | let atkModifier = -5; 5 | let dmgModifier = 10; 6 | const isEnabled = Boolean(actor.data.flags.ssMacro?.isEnabled); 7 | 8 | const disableSharpshooter = (item) => item.update({ 9 | 'data.damage.parts': item.data.flags.ssMacro.oldDmg, 10 | 'data.attackBonus': item.data.flags.ssMacro.oldAtk ?? 0, 11 | 'flags.ssMacro': null 12 | }); 13 | 14 | const enableSharpshooter = (item) => { 15 | let oldDmg = JSON.parse(JSON.stringify(item.data.data.damage.parts)); 16 | item.data.data.damage.parts[0][0] = `${item.data.data.damage.parts[0][0]} + ${dmgModifier}`; 17 | 18 | item.update({ 19 | 'flags.ssMacro.oldDmg': oldDmg, 20 | 'flags.ssMacro.oldAtk': JSON.parse(JSON.stringify(item.data.data.attackBonus ?? 0)), 21 | 'data.damage.parts': item.data.data.damage.parts, 22 | 'data.attackBonus': `${+(item.data.data.attackBonus || 0) + atkModifier}` 23 | }); 24 | } 25 | 26 | actor.update({ 'flags.ssMacro.isEnabled': !isEnabled }); 27 | for (let item of actor.items) { 28 | let isRangedWeapon = getProperty(item, 'data.data.actionType') === 'rwak' && getProperty(item, 'data.type') === 'weapon'; 29 | 30 | if (isRangedWeapon && item.data.data.damage.parts.length > 0) 31 | isEnabled ? disableSharpshooter(item) : enableSharpshooter(item); 32 | } 33 | 34 | ChatMessage.create({ 35 | user: game.user._id, 36 | speaker: ChatMessage.getSpeaker(), 37 | content: isEnabled ? `${actor.name} is aiming normally now.` : `${actor.name} is aiming for vital areas.`, 38 | }, {}); 39 | -------------------------------------------------------------------------------- /archived/age/breather.js: -------------------------------------------------------------------------------- 1 | /* This macro is specific to the AGE System (unoffical) game system. 2 | * 3 | * In AGE system games such as Modern AGE, The Expanse, etc., a character 4 | * may restore 1d6 + level + CON health at the end of a scene by 5 | * "taking a breather". This macro automates that for players or GM 6 | * along with generating a friendly chat message to announce this 7 | * in the chat log. 8 | * 9 | * This macro requires that the game system be "age-system" so that 10 | * the actor will have the appropriate structure. 11 | * 12 | * Oringial macro code: vkdolea 13 | */ 14 | 15 | if (game.system.id === 'age-system') { 16 | let flavor = "Taking a breather here, boss..."; 17 | // Change the value between "" to change flavor on chat message 18 | 19 | // Make sure we've got an actor selected 20 | let ageSystemActor = null; 21 | if (speaker.token) ageSystemActor = game.actors.tokens[speaker.token]; 22 | if (!ageSystemActor ) ageSystemActor = game.actors.get(speaker.actor); 23 | if (ageSystemActor ) { 24 | // Collect the infor we'll need to perform the roll 25 | let rollData = {}; 26 | rollData.level = ageSystemActor .data.data.level; 27 | rollData.cons = ageSystemActor .data.data.abilities.cons.total; 28 | 29 | // Revise token chat if ancestry/origin === Belter 30 | if (ageSystemActor.data.data.ancestry === 'Belter') { 31 | flavor = "Mi leta-go wa bek xiya bosmang"; 32 | } 33 | 34 | // Configure the chat message to be sent 35 | const chatMessage = {flavor, speaker}; 36 | 37 | // Make the roll and send the message 38 | let roll = new Roll("1d6 + @level + @cons", rollData).roll(); 39 | roll.toMessage(chatMessage); 40 | 41 | // Apply the effect 42 | const healed = roll.total; 43 | const curHP = ageSystemActor.data.data.health.value; 44 | const maxHP = ageSystemActor.data.data.health.max; 45 | let newHP = 0; 46 | if ((healed + curHP) > maxHP) { 47 | newHP = maxHP; 48 | } else { 49 | newHP = ageSystemActor.data.data.health.value + healed; 50 | } 51 | ageSystemActor.update({"data.health.value": newHP}); 52 | } 53 | } -------------------------------------------------------------------------------- /archived/age/clear_conditions.js: -------------------------------------------------------------------------------- 1 | /* This macro is specific to the AGE System (unoffical) game system. 2 | * 3 | * This macro requires that the game system be "age-system" so that 4 | * the actor will have the appropriate structure. 5 | * 6 | * Author: schlosrat 7 | */ 8 | 9 | if (game.system.id === 'age-system') { 10 | 11 | // Get the list of all the selected tokens 12 | const selected = canvas.tokens.controlled; 13 | 14 | // For each selected token... 15 | selected.forEach(token => { 16 | 17 | // Get the actor for this token 18 | let ageSystemActor = token.actor; 19 | 20 | // Set the flavor to use in the chat message 21 | let flavor = "All set, boss!" 22 | if (ageSystemActor.data.data.ancestry === "Belter") flavor = "Kowl set, bosmang!"; 23 | // Get the abilities for this actor 24 | // let abilities = ageSystemActor.data.data.abilities; 25 | 26 | // Record the actor's current CON value 27 | let conValue = ageSystemActor.data.data.abilities.cons.value; 28 | 29 | // Check for a baseConValue flag 30 | let baseCon = ageSystemActor.getFlag("world", "baseConValue"); 31 | 32 | // If there is a baseConValue flag set... 33 | if (baseCon != undefined) { 34 | // And if the current CON is less than the baseConValue 35 | if (conValue < baseCon) { 36 | conValue = baseCon; 37 | } 38 | } 39 | 40 | // Do all the updates in a single call to minimize trips to the backend 41 | ageSystemActor.update({ 42 | "data": { 43 | "conditions.blinded": false, 44 | "conditions.deafened": false, 45 | "conditions.dying": false, 46 | "conditions.exhausted": false, 47 | "conditions.fatigued": false, 48 | "conditions.freefalling": false, 49 | "conditions.helpless": false, 50 | "conditions.hindered": false, 51 | "conditions.injured": false, 52 | "conditions.prone": false, 53 | "conditions.restrained": false, 54 | "conditions.unconscious": false, 55 | "conditions.wounded": false, 56 | "abilities.cons.value": conValue, 57 | } 58 | }); 59 | 60 | // Get the speaker for this token 61 | let this_speaker = ChatMessage.getSpeaker({token: token}); 62 | 63 | // Send a friendly chat message from this token 64 | ChatMessage.create({speaker: this_speaker, content: flavor}); // All set, boss! 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /archived/age/clear_cover.js: -------------------------------------------------------------------------------- 1 | /* This macro is specific to the AGE System (unoffical) game system. 2 | * 3 | * In AGE system games such as Modern AGE, The Expanse, etc., a 4 | * character can improve their defense score by "taking cover", 5 | * which can either happen if the character uses their move to 6 | * do so, or applies the "Take Cover" stunt. 7 | * 8 | * This macro makes it a one-button click to remove the cover 9 | * active effect from all selected tokens. 10 | * 11 | * This macro requires that the game system be "age-system" 12 | * since the effect applied is specific to that system. 13 | * 14 | * Author: schlosrat 15 | */ 16 | 17 | // define removeNamedEffect function 18 | async function removeNamedEffect(ageSystemActor, effectData) { 19 | // Look to see if there's already a Cover effect 20 | const item = ageSystemActor.data.effects.find(i =>i.label === effectData.label); 21 | if (item != undefined) { 22 | // Delete it if there is one 23 | const deleted = await ageSystemActor.deleteEmbeddedEntity("ActiveEffect", item._id); // Deletes one EmbeddedEntity 24 | } 25 | } 26 | 27 | async function clearCover () { 28 | if (game.system.id === 'age-system') { 29 | const effectData = { 30 | label : "Cover", 31 | icon : "icons/svg/shield.svg", 32 | duration: {rounds: 10}, 33 | changes: [{ 34 | "key": "data.defense.total", 35 | "mode": 2, // Mode 2 is for ADD. 36 | "value": 0, 37 | "priority": 0 38 | },{ 39 | "key": "data.defense.mod", 40 | "mode": 2, // Mode 2 is for ADD. 41 | "value": 0, 42 | "priority": 0 43 | }] 44 | }; 45 | const selected = canvas.tokens.controlled; 46 | // console.log(selected) 47 | selected.forEach(token => { 48 | removeNamedEffect(token.actor, effectData); 49 | }) 50 | } 51 | } 52 | 53 | clearCover(); -------------------------------------------------------------------------------- /archived/misc/award_party_xp.js: -------------------------------------------------------------------------------- 1 | function award_xp(type, amount) 2 | { 3 | let actors = game.actors.filter(e => e.type === 'character' && e.hasPlayerOwner); 4 | let isShared = type == "shared"; 5 | console.log(type + ' ' + amount); 6 | if (Number.isInteger(amount) && actors.length > 0) 7 | { 8 | let totalAmount = isShared ? amount : amount * actors.length; 9 | let individualAmount = isShared ? Math.floor(amount / actors.length) : amount 10 | 11 | let chatContent = ` 12 | ${totalAmount} Experience Awarded! 13 |
${individualAmount} added to: 14 | `; 15 | 16 | actors.forEach(actor => 17 | { 18 | chatContent += `
${actor.name}`; 19 | actor.update({ 20 | "data.details.xp.value": actor.data.data.details.xp.value + individualAmount 21 | }); 22 | }); 23 | 24 | let chatData = { 25 | user: game.user.id, 26 | speaker: ChatMessage.getSpeaker(), 27 | content: chatContent, 28 | type: CONST.CHAT_MESSAGE_TYPES.OTHER 29 | }; 30 | ChatMessage.create(chatData); 31 | } 32 | } 33 | 34 | new Dialog({ 35 | title: "Award Party XP", 36 | content: ` 37 |

Select a type and an amount. Individual xp will give or take a set amount to/from each party member, whereas shared will split an amount evenly.

38 |
39 |
40 | 41 | 45 |
46 |
47 | 48 | 49 |
50 |
51 | `, 52 | buttons: { 53 | one: { 54 | icon: '', 55 | label: "Confirm", 56 | callback: (html) => 57 | { 58 | let type = html.find('[id=xp-type]')[0].value; 59 | let amount = parseInt(html.find('[id=xp-amount]')[0].value); 60 | award_xp(type, amount); 61 | } 62 | }, 63 | two: { 64 | icon: '', 65 | label: "Cancel", 66 | } 67 | }, 68 | default: "Cancel" 69 | }).render(true); 70 | -------------------------------------------------------------------------------- /archived/misc/close_all_doors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Closes all doors on the canvas 3 | * Author: @Atropos#3814 4 | */ 5 | 6 | canvas.scene.updateEmbeddedDocuments("Wall", canvas.scene.data.walls.map(w => { 7 | return {_id: w.id, ds: w.data.ds === 1 ? 0 : w.data.ds}; 8 | })); 9 | -------------------------------------------------------------------------------- /archived/misc/combat_tracker_ac_hp.js: -------------------------------------------------------------------------------- 1 | // Adds the actor's AC to the combat tracker. Then toggles between HP and AC 2 | const a = "attributes.ac.value"; 3 | const b = "attributes.hp.value"; 4 | 5 | if (game.combat.settings.resource == a) { 6 | game.settings.set('core', 'combatTrackerConfig', {resource: b, skipDefeated: true}); 7 | } else { 8 | game.settings.set('core', 'combatTrackerConfig', {resource: a, skipDefeated: true}); 9 | } 10 | game.combat.update({active: true}, {diff: false}); 11 | -------------------------------------------------------------------------------- /archived/misc/convert_compendium_to_table.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Macro: GeekDad's Compendium to Table Script 3 | * Version: 1 for v9 4 | * Updated: 22-12-2021 by Freeze#2689 5 | * Description: A nice friendly UI that takes a compendium and appends it to a table. 6 | */ 7 | 8 | function getPackNames() { 9 | let packs = []; 10 | let keys = game.packs.keys(); 11 | let done = false; 12 | while (!done) { 13 | let key = keys.next(); 14 | done = key.done; 15 | if (!done) { 16 | let pack = game.packs.get(key.value); 17 | if (pack.documentName === "Item") { 18 | packs.push({ key: key.value, name: pack.metadata.label }); 19 | } 20 | } 21 | } 22 | return packs; 23 | } 24 | 25 | function getTableNames() { 26 | let tables = game.tables.reduce((acc,table) => { 27 | acc.push({ key: table.id, name: table.name }); 28 | return acc; 29 | },[]); 30 | 31 | return tables; 32 | } 33 | 34 | async function convertToTable(packKey, tableKey) { 35 | let pack = game.packs.get(packKey); 36 | let table = game.tables.get(tableKey); 37 | 38 | await pack.getIndex(); 39 | let range = 0; 40 | 41 | const results = pack.index.map(i => { 42 | range++; 43 | return { 44 | text: i.name, 45 | type: 2, 46 | collection: packKey, 47 | resultId: i._id, 48 | img: i.img, 49 | weight: 1, 50 | range: [range, range], 51 | drawn: false 52 | } 53 | }); 54 | 55 | await table.createEmbeddedDocuments("TableResult", results); 56 | 57 | await table.update({formula: "1d" + results.length}); 58 | } 59 | 60 | let itemPacks = getPackNames(); 61 | let tables = getTableNames(); 62 | 63 | let content = `
64 |

This script will append the selected compendium to the selected table. If you want to a new table created, create it an empty table prior to running this script.

65 | 66 |



` 79 | 80 | new Dialog({ 81 | title: `GeekDad's Compendium to Rolltable Converter`, 82 | content: content, 83 | buttons: { 84 | yes: { 85 | icon: "", 86 | label: "Convert", 87 | callback: (html) => { 88 | let packKey = html.find("select[name='output-targetPack']").val(); 89 | let tableKey = html.find("select[name='output-tableKey']").val(); 90 | convertToTable(packKey, tableKey); 91 | } 92 | }, 93 | no: { 94 | icon: "", 95 | label: 'Cancel' 96 | } 97 | }, 98 | default: "yes" 99 | }).render(true); 100 | -------------------------------------------------------------------------------- /archived/misc/create_chat_message.js: -------------------------------------------------------------------------------- 1 | // Courtesy of @errational 2 | // Creates a chat message. 3 | let controlledToken = canvas.tokens.controlled[0]; 4 | const content = `

Monster attacks ${controlledToken.name}

`; 5 | 6 | ChatMessage.create({ 7 | speaker: ChatMessage.getSpeaker(controlledToken), 8 | content: content, 9 | type: CONST.CHAT_MESSAGE_TYPES.OTHER 10 | }); 11 | -------------------------------------------------------------------------------- /archived/misc/folder_permission.js: -------------------------------------------------------------------------------- 1 | // Provides a prompt to set default permissions to all items within a folder. 2 | // Prompts the user for the folder name (case sensitive) and the permission level. 3 | 4 | const form = ` 5 |
Folder:
6 | 7 |
8 | 9 |
Folder Type:
10 | 20 |
21 | 22 |
Permission:
23 | 29 |
30 | 31 | 35 | `; 36 | 37 | const dialog = new Dialog({ 38 | title: 'Set desired permission', 39 | content: form, 40 | buttons: { 41 | use: { 42 | label: 'Apply permissions', 43 | callback: applyPermissions, 44 | }, 45 | }, 46 | }).render(true); 47 | 48 | /** 49 | * 50 | * @param {jQuery} html 51 | */ 52 | function applyPermissions(html) { 53 | // Get values from form 54 | const folderType = html.find('select#folderType')[0].value; 55 | const folderName = html.find(`input#folderName`)[0].value; 56 | const permission = html.find(`select#desiredPermission`)[0].value; 57 | const recurse = html.find(`input#recurse`)[0].checked; 58 | 59 | // Find folderName 60 | const folders = game.folders.filter( 61 | f => f.type === folderType && f.name === folderName 62 | ); 63 | 64 | if (folders.length === 0) 65 | ui.notifications.error( 66 | `Your world does not have any folders named '${folderName}'.` 67 | ); 68 | else if (folders.length > 1) 69 | ui.notifications.error( 70 | `Your world has more than one folder named ${folderName}` 71 | ); 72 | else { 73 | repermission(folders[0], permission, recurse); 74 | ui.notifications.notify( 75 | `Desired permissions were set successfully for '${folderName}' of type '${folderType}'.` 76 | ); 77 | } 78 | } 79 | 80 | /** 81 | * 82 | * @param {*} currentFolder 83 | * @param {String} desiredPermission 84 | * @param {Boolean} recurse 85 | * @returns {Boolean} 86 | */ 87 | async function repermission(currentFolder, desiredPermission, recurse) { 88 | console.info(`Repermissioning: ${currentFolder.name}`); 89 | 90 | if (currentFolder.content) { 91 | currentFolder.content.forEach(async doc => { 92 | const newPerms = duplicate(doc.data.permission); 93 | newPerms.default = Number(desiredPermission); 94 | await doc.update({ permission: newPerms }); 95 | }); 96 | } 97 | 98 | if (recurse && currentFolder.children) { 99 | currentFolder.children.forEach(folder => 100 | repermission(folder, desiredPermission, recurse) 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /archived/misc/hide_video_boxes.js: -------------------------------------------------------------------------------- 1 | // Hides the camera boxes. 2 | // Note: this has to be re-ran when the UI refreshes. 3 | 4 | let cameras = document.getElementById("camera-views"); 5 | cameras.style.display = cameras.style.display === "none" ? "flex" : "none"; 6 | -------------------------------------------------------------------------------- /archived/misc/journal_update_id_with_name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Replaces the reference id to other items/tables/journals/actors to use their name. 3 | * Useful for after importing journal records from a compendium that has references to actors/items/etc... 4 | * Author: @KrishMero#1792 5 | */ 6 | 7 | game.journal.forEach(entry => { 8 | let content = entry.data.content; 9 | let matches = content.match(/@\w*\[\w*\]/g); 10 | // now we have an array of things such as @Actor[5c8HWfrpvRV4XtZ1] 11 | let uniqueMatches = matches 12 | .filter((value, index, self) => self.indexOf(value) === index) //unique matches 13 | .forEach(str => { 14 | let arrayData = str.slice(1, -1).split('['); // cut off the @ and ] then make [0] the type and [1] the id. 15 | // since the reference may not match directly with the game entity type, lets look that up. 16 | let entityType = getEntityType(arrayData[0]); 17 | let id = arrayData[1]; 18 | // with the id and our entity type, look up the name of the entry. 19 | let name = game[entityType].get(id)?.name; 20 | if (!name) { 21 | return ui.notifications.error(`Could not find any record for the entity type ${entityType} with the id of ${id}`); 22 | } 23 | 24 | // replace the ID with the name. 25 | console.log(`updating ${id} with ${name}`); 26 | 27 | let regEx = new RegExp(id, 'g'); 28 | content.replace(regEx, name); 29 | }); 30 | entry.update({ content }); 31 | }); 32 | 33 | function getEntityType(entity) { 34 | switch (entity) { 35 | case 'JournalEntry': return 'journal'; 36 | case 'RollTable': return 'tables'; 37 | default: return entity.toLowerCase() + 's'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /archived/misc/scene_border_walls.js: -------------------------------------------------------------------------------- 1 | for (let scene of game.scenes){ 2 | //Goes through the array of scenes and finds the active one. 3 | if (scene._view) { 4 | //Height, Width, and Padding offsets. 5 | //scene data.height and scene.data.width give image size. 6 | //canvas dimensions give size including padding 7 | //xf, yf = x and y offsets. 8 | let h = scene.data.height; 9 | let w = scene.data.width; 10 | let xf = (canvas.dimensions.width - w)*0.5; 11 | let yf = (canvas.dimensions.height - h)*0.5; 12 | //Walls need two vertices: X Point 1, Y Point 1, X Point 2, Y Point 2 13 | //top wall, right wall, bottom wall, left wall 14 | let tw = [xf, yf, w + xf, yf]; 15 | let rw = [w + xf, yf, w + xf, h + yf]; 16 | let bw = [w + xf, h + yf, xf, h + yf]; 17 | let lw = [xf, h + yf, xf, yf]; 18 | //Creates walls. There is probably a cleaner way to do this. 19 | Wall.create({ 20 | c: tw 21 | }); 22 | Wall.create({ 23 | c: rw 24 | }); 25 | Wall.create({ 26 | c: bw 27 | }); 28 | Wall.create({ 29 | c: lw 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /archived/misc/share_image_via_url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Share an image to all players when you have an image URL 3 | * Author: @Krishmero#1792 4 | */ 5 | 6 | let imagePopup = (imageUrl) => { 7 | // Display the image popout and share it. 8 | const ip = new ImagePopout(imageUrl); 9 | ip.render(true); 10 | ip.shareImage(); 11 | }; 12 | 13 | let chatDialog = (imageUrl) => { 14 | ChatMessage.create({ 15 | user: game.user._id, 16 | content: ``, 17 | type: CONST.CHAT_MESSAGE_TYPES.OOC 18 | }); 19 | }; 20 | 21 | let selectOptions = game.user.isGM ? ` 22 |
23 | 24 | 29 |
30 |
31 | ` : ''; 32 | 33 | new Dialog({ 34 | title: `Share Image via URL`, 35 | content: ` 36 |
37 | ${selectOptions} 38 |
39 | 40 | 41 |
42 |
43 | `, 44 | buttons: { 45 | yes: { 46 | icon: "", 47 | label: `Share`, 48 | callback: (html) => { 49 | let imageUrl = html.find('#image-url').val(); 50 | let permission = html.find('select#output-options')[0]?.value || null; 51 | if (!imageUrl) { 52 | return ui.notifications.info("You did not provide a valid image."); 53 | } 54 | if (game.user.isGM && ['popup', 'both'].includes(permission)) { 55 | imagePopup(imageUrl); 56 | } 57 | if (!game.user.isGM || ['chat', 'both'].includes(permission)) { 58 | chatDialog(imageUrl); 59 | } 60 | } 61 | }, 62 | no: { 63 | icon: "", 64 | label: `Cancel` 65 | }, 66 | }, 67 | default: "yes" 68 | }).render(true) 69 | -------------------------------------------------------------------------------- /archived/module-specific/gm_secret_skill_check.js: -------------------------------------------------------------------------------- 1 | // This macro is for Pathfinder 2E 2 | // Makes an always-secret roll with a skill using the selected token, or the configured character if none are selected 3 | // Make a roll with a dialog to choose the skill 4 | game.PF2EToolbox.secretSkillRoll() 5 | 6 | // Roll acrobatics 7 | // game.PF2EToolbox.secretSkillRoll('acr') 8 | 9 | // Roll perception 10 | // game.PF2EToolbox.secretSkillRoll('perception') 11 | 12 | // Show available skills for the selected token 13 | // game.PF2EToolbox.secretSkillRoll('give me the skills!') 14 | -------------------------------------------------------------------------------- /archived/module-specific/hit_die.js: -------------------------------------------------------------------------------- 1 | // Requires furnace to work correctly. 2 | // args[0] === name of character 3 | // args[1] === "add","sub", or "use" 4 | // "add" : adds 1 hit die if able to 5 | // "sub" : removes 1 hit die if able to 6 | // "use" : removes 1 hit die and heals actor for rolled amount 7 | 8 | (async () => { 9 | const actor = game.actors.find(i => i.name === args[0]); 10 | if (!actor) return ui.notifications.warn(`No Actor by that name available.`); 11 | const classItems = actor.data.items.filter(it => it.type === "class") 12 | if (!classItems.length) return ui.notifications.warn(`Actor has no class!`); 13 | if (classItems.length > 1) return ui.notifications.warn(`Actor has multiple classes! This is not (yet) supported.`); 14 | const classItem = classItems[0]; 15 | 16 | if(args[1] === "add") 17 | { 18 | if (classItem.data.hitDiceUsed <= 0) return ui.notifications.warn(`You are at maximum Hitdie!`); 19 | 20 | const classItemUpdate = { 21 | _id: classItem._id, 22 | data: { 23 | hitDiceUsed: classItem.data.hitDiceUsed - 1, 24 | }, 25 | }; 26 | 27 | await actor.updateEmbeddedEntity("OwnedItem", classItemUpdate); 28 | } 29 | 30 | if(args[1] === "sub") 31 | { 32 | if (classItem.data.hitDiceUsed >= classItem.data.levels) return ui.notifications.warn(`You have no remaining hit dice to spend!`); 33 | 34 | const classItemUpdate = { 35 | _id: classItem._id, 36 | data: { 37 | hitDiceUsed: classItem.data.hitDiceUsed + 1, 38 | }, 39 | }; 40 | 41 | await actor.updateEmbeddedEntity("OwnedItem", classItemUpdate); 42 | } 43 | 44 | if(args[1] === "use") 45 | { 46 | if (classItem.data.hitDiceUsed >= classItem.data.levels) return ui.notifications.warn(`You have no remaining hit dice to spend!`); 47 | const classItemUpdate = { 48 | _id: classItem._id, 49 | data: { 50 | hitDiceUsed: classItem.data.hitDiceUsed + 1, 51 | }, 52 | }; 53 | await actor.updateEmbeddedEntity("OwnedItem", classItemUpdate); 54 | 55 | const hitDieRoll = new Roll(`1${classItem.data.hitDice} + ${actor.data.data.abilities.con.mod}`); 56 | hitDieRoll.roll(); 57 | hitDieRoll.toMessage({ 58 | user : game.user._id, 59 | speaker : speaker, 60 | flavor : "Roll Hit Dice" 61 | }); 62 | 63 | const actorUpdate = { 64 | data: { 65 | attributes: { 66 | hp: { 67 | value: Math.clamped( 68 | actor.data.data.attributes.hp.value + hitDieRoll.total, 69 | actor.data.data.attributes.hp.min, 70 | actor.data.data.attributes.hp.max 71 | ) 72 | }, 73 | }, 74 | }, 75 | }; 76 | await actor.update(actorUpdate); 77 | } 78 | })(); 79 | -------------------------------------------------------------------------------- /archived/module-specific/hunters-mark-mark_attack_wrapper.js: -------------------------------------------------------------------------------- 1 | //This marco is replacement for a rollItemMacro. Replace the name of the item 2 | //you wish to use to make the attack. This macro must be used with the 3 | //CastMark.json macro or it will just make a standard attack. 4 | 5 | // PUT ITEM MACRO HERE between quotes **************** 6 | const itemName = "Longbow"; 7 | // *************************************************** 8 | 9 | //parameters 10 | 11 | let myToken = token; 12 | const macroName = "world"; 13 | const markDmg = " + 1d6"; 14 | const target = game.user.targets.values().next().value; 15 | const bonuses = myToken.actor.data.data.bonuses; 16 | const actorId = myToken.actor._id + "_mark"; 17 | 18 | //Check to see if the mark flag is set else make attack 19 | 20 | function checkMark() { 21 | const flag = myToken.getFlag(macroName, actorId); 22 | 23 | if (flag) { 24 | if (flag.targetId == target.data._id) { 25 | markAttack(flag); 26 | } else { 27 | baseAttack(flag); 28 | } 29 | } else { 30 | game.dnd5e.rollItemMacro(itemName); 31 | } 32 | } 33 | 34 | //check if the mark damag is set and if not increase 35 | //increase global damage by 1d6 36 | 37 | function markAttack(flag) { 38 | if (!flag.isSet) { 39 | let obj = { 40 | "data.bonuses.mwak.damage": flag.meleeAtk + markDmg, 41 | "data.bonuses.rwak.damage": flag.rangeAtk + markDmg, 42 | "data.bonuses.msak.damage": flag.meleeSpell + markDmg, 43 | "data.bonuses.rsak.damage": flag.rangeSpell + markDmg 44 | }; 45 | updateActor(myToken, obj); 46 | flag.isSet = true; 47 | } 48 | game.dnd5e.rollItemMacro(itemName); 49 | token.setFlag(macroName, actorId, flag); 50 | } 51 | 52 | // check if the mark damage is set and if it is revert to base global damage 53 | 54 | function baseAttack(flag) { 55 | if (flag) { 56 | let obj = { 57 | "data.bonuses.mwak.damage": flag.meleeAtk, 58 | "data.bonuses.rwak.damage": flag.rangeAtk, 59 | "data.bonuses.msak.damage": flag.meleeSpell, 60 | "data.bonuses.rsak.damage": flag.rangeSpell 61 | }; 62 | updateActor(myToken, obj); 63 | flag.isSet = false; 64 | game.dnd5e.rollItemMacro(itemName); 65 | token.setFlag(macroName, actorId, flag); 66 | } else { 67 | game.dnd5e.rollItemMacro(itemName); 68 | } 69 | } 70 | 71 | async function updateActor(updateToken, obj) { 72 | await updateToken.actor.update(obj); 73 | } 74 | 75 | //Ensure target is set and then call check mark function 76 | 77 | if (!myToken) ui.notifications.error("Please select your token first."); 78 | else { 79 | checkMark(); 80 | } 81 | -------------------------------------------------------------------------------- /archived/module-specific/hunters_mark-gm_conditions.js: -------------------------------------------------------------------------------- 1 | //This macro must be called 'GMConditions' and be on the GM's 2 | //hot bar for the castMark.js macro to function correctly. 3 | //you must tick the option 'Excecute Macro As GM' to grant 4 | //players access to this macro. 5 | //To do this you must create a new macro in your game and use 6 | //the code below, dragging the macro from the compendium 7 | //will not work. 8 | 9 | 10 | const action = args[0] 11 | const condition = args[1] 12 | const targetId = args[2] 13 | const target = canvas.tokens.get(targetId); 14 | if (action == "apply"){ 15 | game.cub.addCondition(condition, target, {replaceExisting: true}) 16 | } 17 | else if (action == "remove"){ 18 | game.cub.removeCondition(condition, target, {warn: true}); 19 | } 20 | -------------------------------------------------------------------------------- /archived/module-specific/open_beyond_sheet_player.js: -------------------------------------------------------------------------------- 1 | // Here's one for your players if you are using Virtual Tabletop Assets - D&D Beyond Integration: 2 | // Requires https://www.vttassets.com/modules/vtta-dndbeyond or ... 3 | // https://foundryvtt.com/packages/ddb-importer/ with character sheets linked! 4 | 5 | let popup = () => { 6 | if (!game.user.character) 7 | return ui.notifications.error("You must first have a character assigned to your user!"); 8 | 9 | let char = game.user.character; 10 | 11 | let url = ""; 12 | if (char.data.flags.vtta && char.data.flags.vtta.dndbeyond && char.data.flags.vtta.dndbeyond.url) { 13 | url = char.data.flags.vtta.dndbeyond.url; 14 | } else if (char.data.flags.ddbimporter && char.data.flags.ddbimporter.dndbeyond && char.data.flags.ddbimporter.dndbeyond.url) { 15 | url = char.data.flags.ddbimporter.dndbeyond.url; 16 | } else { 17 | return ui.notifications.error("Character must be linked with a D&D Beyond sheet!"); 18 | } 19 | 20 | let ratio = window.innerWidth / window.innerHeight; 21 | let width = Math.round(window.innerWidth * 0.5); 22 | let height = Math.round(window.innerWidth * 0.5 * ratio); 23 | const dndBeyondPopup = window.open( 24 | url, 25 | "ddb_sheet_popup", 26 | `resizeable,scrollbars,location=no,width=${width},height=${height},toolbar=1` 27 | ); 28 | }; 29 | 30 | popup(); 31 | -------------------------------------------------------------------------------- /archived/pf1e/channel_positive_healing.js: -------------------------------------------------------------------------------- 1 | // CONFIGURATION 2 | // Leave casterName as null to channel positive as the currently-selected character 3 | // Example `const casterName = "Bob Bobbington";` 4 | const casterName = null; 5 | 6 | const tokens = canvas.tokens.controlled; 7 | let caster = tokens.map((o) => o.actor)[0]; 8 | if (!caster && !!casterName) { 9 | caster = game.actors.entities.filter((o) => o.name.includes(casterName))[0]; 10 | } 11 | 12 | function channelPositive() { 13 | if (!caster.data.data.classes.cleric) { 14 | ui.notifications.warn("You're not a cleric!"); 15 | return; 16 | } 17 | const clericLevel = caster.data.data.classes.cleric.level; 18 | const rollString = `${Math.floor((clericLevel + 1) / 2)}d6`; 19 | 20 | const roll = new Roll(rollString); 21 | roll.roll(); 22 | roll.toMessage({ 23 | flavor: "Channeling positive energy", 24 | }); 25 | } 26 | 27 | if (!caster || caster === undefined) { 28 | ui.notifications.warn("You need to be controlling someone to channel!") 29 | } else { 30 | channelPositive(); 31 | } -------------------------------------------------------------------------------- /archived/pf1e/roll_knowledge_skill_check.js: -------------------------------------------------------------------------------- 1 | const tokens = canvas.tokens.controlled; 2 | const caster = tokens[0]; 3 | 4 | if (tokens.length !== 1) { 5 | ui.notifications.warn("Please select a token"); 6 | } else { 7 | const knowledgeTypes = [ 8 | "Arcana", 9 | "Dungeoneering", 10 | "Engineering", 11 | "Geography", 12 | "History", 13 | "Local", 14 | "Nature", 15 | "Nobility", 16 | "Planes", 17 | "Religion", 18 | ]; 19 | 20 | const knowledgeData = []; 21 | knowledgeTypes.forEach((type) => { 22 | const knowledgeDatum = 23 | caster.actor.data.data.skills[`k${type.toLowerCase().substring(0, 2)}`]; 24 | knowledgeDatum.name = type; 25 | knowledgeData.push(knowledgeDatum); 26 | }); 27 | 28 | const knownKnowledge = knowledgeData.filter((datum) => datum.rank > 0); 29 | 30 | if (knownKnowledge.length < 1) { 31 | ui.notifications.warn("You know nothing."); 32 | } else { 33 | const buttons = {}; 34 | knownKnowledge.forEach((type) => { 35 | buttons[type.name] = { 36 | label: type.name, 37 | callback: () => { 38 | rollCheck(type.name, type.mod); 39 | }, 40 | }; 41 | }); 42 | 43 | new Dialog({ 44 | title: "Roll Knowledge!", 45 | content: `

Choose a knowledge skill

`, 46 | buttons: buttons, 47 | }).render(true); 48 | } 49 | } 50 | 51 | function rollCheck(name, mod) { 52 | const roll = new Roll(`1d20 + ${mod}`); 53 | roll.roll(); 54 | roll.toMessage({ 55 | flavor: `Knowledge ${name} check`, 56 | speaker: { alias: token.actor.data.name }, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /archived/pf1e/wand_of_cure_light_wounds.js: -------------------------------------------------------------------------------- 1 | // this script attempts to heal X points of damage by repeatedly using charges of wands of cure light wounds 2 | 3 | function hitTarget(target) { 4 | if (target > 250) { 5 | ui.notifications.warn( 6 | "Too much healing! No one needs that much healing! Max 250." 7 | ); 8 | return; 9 | } 10 | let current = 0; 11 | let chargesUsed; 12 | 13 | const rolls = []; 14 | for (chargesUsed = 0; current < target; chargesUsed += 1) { 15 | const roll = new Roll("1d8 + 1"); 16 | roll.roll(); 17 | current += roll.total; 18 | rolls.push({ roll: roll.total - 1 }); 19 | } 20 | 21 | const roll = new Roll(`${chargesUsed}d8 + ${chargesUsed}`); 22 | const msg = roll.toMessage( 23 | { flavor: `Casting cure light wounds ${chargesUsed} times` }, 24 | { create: false } 25 | ); 26 | 27 | const fakeRoll = { 28 | class: "Roll", 29 | formula: `${chargesUsed}d8 + ${chargesUsed}`, 30 | dice: [ 31 | { 32 | class: "Die", 33 | faces: 8, 34 | rolls: rolls, 35 | formula: `${chargesUsed}d8`, 36 | options: {}, 37 | }, 38 | ], 39 | parts: ["_d0", "+", `${chargesUsed}`], 40 | result: `${current - chargesUsed} + ${chargesUsed}`, 41 | total: current, 42 | }; 43 | 44 | msg.roll = JSON.stringify(fakeRoll); 45 | msg.content = String(current); 46 | 47 | const tokens = canvas.tokens.controlled; 48 | if (tokens.length !== 1) { 49 | ui.notifications.warn("Please select a token."); 50 | return; 51 | } 52 | const token = tokens[0]; 53 | msg.speaker = {alias: token.actor.data.name} 54 | 55 | ChatMessage.create(msg); 56 | } 57 | 58 | new Dialog({ 59 | title: "Cast until heal a set amount", 60 | content: 61 | "

Enter the amount you want to heal


", 62 | buttons: { 63 | submit: { 64 | label: "Heal", 65 | icon: '', 66 | callback: () => { 67 | const healTarget = parseInt( 68 | eval( 69 | $("#amountInput") 70 | .val() 71 | .match(/[0-9]*/g) 72 | ) 73 | ); 74 | hitTarget(healTarget); 75 | }, 76 | }, 77 | }, 78 | }).render(true); 79 | -------------------------------------------------------------------------------- /archived/pf2e/action_climb.js: -------------------------------------------------------------------------------- 1 | if (canvas.tokens.controlled == 0) { 2 | ui.notifications.warn("You must select a token."); 3 | return 4 | } 5 | 6 | actor.data.data.skills.ath.roll(event); 7 | 8 | ////////////// To chat message data ///////////////// 9 | 10 | let toChat = (content) => { 11 | let chatData = { 12 | user: game.user.id, 13 | content, 14 | speaker: ChatMessage.getSpeaker(), 15 | } 16 | 17 | ChatMessage.create(chatData, {}) 18 | } 19 | 20 | //////////// To chat message data ////////////////// 21 | 22 | 23 | toChat(`

Climb

24 |
25 | You move up, down, or across an incline. Unless it's particularly easy, you must attempt an Athletics check. The GM determines the DC based on the nature of the incline and environmental circumstances. You're @Compendium[pf2e.conditionitems.AJh5ex99aV6VTggg]{Flat-Footed} unless you have a climb Speed. 26 |
27 | 28 |
29 |

💥 Crit Success!

30 |
31 | You move up, across, or safely down the incline for 5 feet plus 5 feet per 20 feet of your land Speed (a total of 10 feet for most PCs). 32 |
33 | 34 |
35 |

✔️ Success!

36 |
37 | You move up, across, or safely down the incline for 5 feet per 20 feet of your land Speed (a total of 5 feet for most PCs, minimum 5 feet if your Speed is below 20 feet). 38 |
39 | 40 |
41 |

❌ Crit Fail!

42 |
43 | You fall. If you began the climb on stable ground, you fall and land @Compendium[pf2e.conditionitems.j91X7x0XSomq8d60]{Prone}. 44 |
45 | 46 |
47 |

Skill Rank Requirements!

48 |
49 | Untrained: ladder, steep slope, low-branched tree 50 | 51 |
Trained: rigging, rope, typical tree 52 | 53 |
Expert: a wall with small handholds and footholds 54 | 55 |
Master: ceiling with handholds and footholds, rock wall 56 | 57 |
Legendary: smooth surface 58 |
59 | 60 |
`) -------------------------------------------------------------------------------- /archived/pf2e/action_conceal_object.js: -------------------------------------------------------------------------------- 1 | if (canvas.tokens.controlled == 0) { 2 | ui.notifications.warn("You must select a token."); 3 | return 4 | } 5 | 6 | actor.data.data.skills.ste.roll(event); 7 | 8 | ////////////// To chat message data ///////////////// 9 | 10 | let toChat = (content) => { 11 | let chatData = { 12 | user: game.user.id, 13 | content, 14 | speaker: ChatMessage.getSpeaker(), 15 | } 16 | 17 | ChatMessage.create(chatData, {}) 18 | } 19 | 20 | //////////// To chat message data ////////////////// 21 | 22 | 23 | toChat(`

Conceal an Object

24 |
25 | You hide a small object on your person (such as a weapon of light Bulk). When you try to sneak a concealed object past someone who might notice it, the GM rolls your Stealth check and compares it to this passive observer's Perception DC. 26 |
You can also conceal an object somewhere other than your person, such as among undergrowth or in a secret compartment within a piece of furniture. In this case, characters Seeking in an area compare their Perception check results to your Stealth DC to determine whether they find the object. 27 |
28 | 29 |
30 |

✔️ Success!

31 |
32 | The object remains undetected. 33 |
34 | 35 |
36 |

❌ Fail!

37 |
38 | The searcher finds the object. 39 |
40 | 41 |
`) -------------------------------------------------------------------------------- /archived/pf2e/action_demoralize.js: -------------------------------------------------------------------------------- 1 | const handleCrits = (roll) => roll === 1 ? -10 : (roll === 20 ? 10 : 0); 2 | const options = ['all', 'skill-check', "intimidation", 'demoralize'] 3 | 4 | if (canvas.tokens.controlled == 0) { 5 | ui.notifications.warn("You must select a token."); 6 | return 7 | } 8 | 9 | if (game.user.targets.size == 0) { 10 | ui.notifications.warn("You must have a target."); 11 | return 12 | } 13 | 14 | 15 | ////////////// To chat message data ///////////////// 16 | 17 | let toChat = (content) => { 18 | let chatData = { 19 | user: game.user.id, 20 | content, 21 | speaker: ChatMessage.getSpeaker(), 22 | } 23 | 24 | ChatMessage.create(chatData, {}) 25 | } 26 | 27 | //////////// To chat message data ////////////////// 28 | 29 | game.user.targets.forEach(t => { 30 | 31 | token.actor.data.data.skills.itm.roll(event, options, (result) => { 32 | let roll = result._total; 33 | let crit = handleCrits(result.parts[0].rolls[0].result); 34 | let willDC = 10 + t.actor.data.data.saves.will.totalModifier 35 | 36 | if (roll + crit >= willDC + 10) { 37 | toChat(`

Demoralize

38 | 💥 Crit Success! 39 |
${t.name}: becomes Frightened 2! 40 |
41 | 42 |
`) 43 | addAlert(t, 2) 44 | } else if (roll + crit >= willDC) { 45 | toChat(`

Demoralize

46 | ✔️ Success! 47 |
${t.name}: becomes Frightened 1! 48 |
49 | 50 |
`) 51 | addAlert(t, 1) 52 | } else if (roll + crit < willDC - 10) { 53 | 54 | toChat(`

Demoralize

55 | ❌ Crit Fail! 56 |
${t.name}: unaffected 57 |
58 | 59 |
`) 60 | 61 | } else if (roll + crit < willDC) { 62 | 63 | toChat(`

Demoralize

64 | ❌ Fail! 65 |
${t.name}: unaffected 66 |
67 | 68 |
`) 69 | } 70 | }) 71 | 72 | }) -------------------------------------------------------------------------------- /archived/pf2e/action_disable_device.js: -------------------------------------------------------------------------------- 1 | if (canvas.tokens.controlled == 0) { 2 | ui.notifications.warn("You must select a token."); 3 | return 4 | } 5 | 6 | actor.data.data.skills.thi.roll(event); 7 | 8 | ////////////// To chat message data ///////////////// 9 | 10 | let toChat = (content) => { 11 | let chatData = { 12 | user: game.user.id, 13 | content, 14 | speaker: ChatMessage.getSpeaker(), 15 | } 16 | 17 | ChatMessage.create(chatData, {}) 18 | } 19 | 20 | //////////// To chat message data ////////////////// 21 | 22 | 23 | toChat(`

Disable Device

24 |
25 | This action allows you to disarm a trap or another complex device. Your Thievery check result determines how much progress you make. 26 |
27 | 28 |
29 |

💥 Crit Success!

30 |
31 | You disable the device, or you achieve one success toward disabling a complex device. 32 |
33 | 34 |
35 |

✔️ Success!

36 |
37 | You disable the device, or you achieve one success toward disabling a complex device. 38 |
39 | 40 |
41 |

❌ Fail!

42 |
43 | You make no progress towards disabling the device, but don't trigger it. 44 |
45 | 46 |
47 |

❌ Crit Fail!

48 |
49 | You trigger the device. 50 |
51 | 52 |
`) -------------------------------------------------------------------------------- /archived/pf2e/action_force_open.js: -------------------------------------------------------------------------------- 1 | if (canvas.tokens.controlled == 0) { 2 | ui.notifications.warn("You must select a token."); 3 | return 4 | } 5 | 6 | actor.data.data.skills.ath.roll(event); 7 | 8 | ////////////// To chat message data ///////////////// 9 | 10 | let toChat = (content) => { 11 | let chatData = { 12 | user: game.user.id, 13 | content, 14 | speaker: ChatMessage.getSpeaker(), 15 | } 16 | 17 | ChatMessage.create(chatData, {}) 18 | } 19 | 20 | //////////// To chat message data ////////////////// 21 | 22 | 23 | toChat(`

Force Open

24 |
25 | You attempt to forcefully open a door, window, container or heavy gate. With a high enough result, you can even smash through walls. Without a crowbar, prying something open takes a -2 item penalty to Athletics. 26 |
27 | 28 |
29 |

💥 Crit Success!

30 |
31 | You open the door, window, container, or gate and can avoid damaging it in the process. 32 |
33 | 34 |
35 |

✔️ Success!

36 |
37 | You break the door, window, container, or gate open, and it gains the broken condition. If it's especially sturdy, the GM might have it take damage but not be broken. 38 |
39 | 40 |
41 |

❌ Crit Fail!

42 |
43 | Your attempt jams the door, window, container, or gate shut, imposing a -2 circumstance penalty on future attempts to Force it Open. 44 |
45 | 46 |
47 |

Skill Rank Requirements!

48 |
49 | Untrained: fabric, flimsy glass
50 | Trained: ice, sturdy glass
51 | Expert: flimsy wooden door, wooden portcullis
52 | Master: sturdy wooden door, iron portcullis, metal bar
53 | Legendary: stone or iron door 54 |
55 | 56 |
`) -------------------------------------------------------------------------------- /archived/pf2e/action_high_jump.js: -------------------------------------------------------------------------------- 1 | if (canvas.tokens.controlled == 0) { 2 | ui.notifications.warn("You must select a token."); 3 | return 4 | } 5 | 6 | actor.data.data.skills.ath.roll(event); 7 | 8 | ////////////// To chat message data ///////////////// 9 | 10 | let toChat = (content) => { 11 | let chatData = { 12 | user: game.user.id, 13 | content, 14 | speaker: ChatMessage.getSpeaker(), 15 | } 16 | 17 | ChatMessage.create(chatData, {}) 18 | } 19 | 20 | //////////// To chat message data ////////////////// 21 | 22 | 23 | toChat(`

High Jump

24 |
25 | You Stride, then make a vertical Leap and attempt a DC 30 Athletics check to increase the height of your jump. If you didn't Stride at least 10 feet, you automatically fail your check. This DC might be increased or decreased due to the situation, as determined by the GM. 26 |
27 | 28 |
29 |

💥 Crit Success!

30 |
31 | Increase the maximum vertical distance to 8 feet, or increase the maximum vertical distance to 5 feet and maximum horizontal distance to 10 feet. 32 |
33 | 34 |
35 |

✔️ Success!

36 |
37 | Increase the maximum vertical distance to 5 feet. 38 |
39 | 40 |
41 |

❌ Fail!

42 |
43 | You Leap normally. 44 |
45 | 46 |
47 |

❌ Crit Fail!

48 |
49 | You don't Leap at all, and instead you fall @Compendium[pf2e.conditionitems.j91X7x0XSomq8d60]{Prone} in your space. 50 |
51 | 52 |
`) -------------------------------------------------------------------------------- /archived/pf2e/action_long_jump.js: -------------------------------------------------------------------------------- 1 | if (canvas.tokens.controlled == 0) { 2 | ui.notifications.warn("You must select a token."); 3 | return 4 | } 5 | 6 | actor.data.data.skills.ath.roll(event); 7 | 8 | ////////////// To chat message data ///////////////// 9 | 10 | let toChat = (content) => { 11 | let chatData = { 12 | user: game.user.id, 13 | content, 14 | speaker: ChatMessage.getSpeaker(), 15 | } 16 | 17 | ChatMessage.create(chatData, {}) 18 | } 19 | 20 | //////////// To chat message data ////////////////// 21 | 22 | 23 | toChat(`

Long Jump

24 |
25 | You Stride, then make a horizontal Leap and attempt an Athletics check to increase the length of your jump. The DC of the Athletics check is equal to the total distance in feet you're attempting to move during your Leap (so you'd need to succeed at a DC 20 check to Leap 20 feet). You can't Leap farther than your Speed.
26 | 27 |
If you didn't Stride at least 10 feet, or if you attempt to jump in a different direction than your Stride, you automatically fail your check. This DC might be increased or decreased due to the situation, as determined by the GM or a taken feat. 28 |
29 | 30 |
31 |

💥 Crit Success!

32 |
33 | Increase the maximum vertical distance to 8 feet, or increase the maximum vertical distance to 5 feet and maximum horizontal distance to 10 feet. 34 |
35 | 36 |
37 |

✔️ Success!

38 |
39 | Increase the maximum horizontal distance you @Compendium[pf2e.actionspf2e.d5I6018Mci2SWokk]{Leap} to the desired distance. 40 |
41 | 42 |
43 |

❌ Fail!

44 |
45 | You Leap normally. 46 |
47 | 48 |
49 |

❌ Crit Fail!

50 |
51 | You Leap normally, but then fall and land @Compendium[pf2e.conditionitems.j91X7x0XSomq8d60]{Prone}. 52 |
53 | 54 |
`) -------------------------------------------------------------------------------- /archived/pf2e/action_pick_lock.js: -------------------------------------------------------------------------------- 1 | if (canvas.tokens.controlled == 0) { 2 | ui.notifications.warn("You must select a token."); 3 | return 4 | } 5 | 6 | actor.data.data.skills.thi.roll(event); 7 | 8 | ////////////// To chat message data ///////////////// 9 | 10 | let toChat = (content) => { 11 | let chatData = { 12 | user: game.user.id, 13 | content, 14 | speaker: ChatMessage.getSpeaker(), 15 | } 16 | 17 | ChatMessage.create(chatData, {}) 18 | } 19 | 20 | //////////// To chat message data ////////////////// 21 | 22 | 23 | toChat(`

Pick a Lock

24 |
25 | Opening a lock without a key is very similar to Disabling a Device, but the DC of the check is determined by the complexity and construction of the lock you are attempting to pick (locks and their DCs are found in their description). 26 |
27 | 28 |
29 |

💥 Crit Success!

30 |
31 | You unlock the lock, or you achieve two successes toward opening a complex lock. You leave no trace of your tampering. 32 |
33 | 34 |
35 |

✔️ Success!

36 |
37 | You open the lock, or you achieve one success toward opening a complex lock. 38 |
39 | 40 |
41 |

❌ Crit Fail!

42 |
43 | You break your tools. Fixing them requires using Crafting to @Compendium[pf2e.actionspf2e.bT3skovyLUtP22ME]{Repair} them or else swapping in @Compendium[pf2e.equipment-srd.Sw7MBLASN3xK4Y44]{Replacement Picks}. 44 |
45 | 46 |
`) -------------------------------------------------------------------------------- /archived/pf2e/action_sense_motive.js: -------------------------------------------------------------------------------- 1 | if (canvas.tokens.controlled == 0) { 2 | ui.notifications.warn("You must select a token."); 3 | return 4 | } 5 | 6 | actor.data.data.attributes.perception.roll(event); 7 | 8 | ////////////// To chat message data ///////////////// 9 | 10 | let toChat = (content) => { 11 | let chatData = { 12 | user: game.user.id, 13 | content, 14 | speaker: ChatMessage.getSpeaker(), 15 | } 16 | 17 | ChatMessage.create(chatData, {}) 18 | } 19 | 20 | //////////// To chat message data ////////////////// 21 | 22 | 23 | toChat(`

Sense Motive

24 |
25 | You try to tell whether a creature's behavior is abnormal. Choose one creature, and assess it for odd body language, signs of nervousness, and other indicators that it might be trying to deceive someone. The GM attempts a single secret Perception check for you and compares the result to the Deception DC of the creature, the DC of a spell affecting the creature's mental state, or another appropriate DC determined by the GM. You typically can't try to Sense the Motive of the same creature again until the situation changes significantly. 26 |
27 | 28 |
29 |

💥 Crit Success!

30 |
31 | You determine the creature's true intentions and get a solid idea of any mental magic affecting it. 32 |
33 | 34 |
35 |

✔️ Success!

36 |
37 | You can tell whether the creature is behaving normally, but you don't know its exact intentions or what magic might be affecting it. 38 |
39 | 40 |
41 |

❌ Fail!

42 |
43 | You detect what a deceptive creature wants you to believe. If they're not being deceptive, you believe they're behaving normally. 44 |
45 | 46 |
47 |

❌ Crit Fail!

48 |
49 | You get a false sense of the creature's intentions. 50 |
51 | 52 |
`) -------------------------------------------------------------------------------- /archived/pf2e/action_stand_up.js: -------------------------------------------------------------------------------- 1 | ui.notifications.warn(`${token.name} stands up`); 2 | (async () => { 3 | if(actor.items.find(entry => (entry.name === "Prone"))){ 4 | let prone = actor.items.find(entry => (entry.name === "Prone" && entry.type === "condition")); 5 | await PF2eConditionManager.removeConditionFromToken(prone._id, token) 6 | } 7 | 8 | if (actor.items.find(entry => (entry.name === "Flat-Footed"))){ 9 | let flatfooted = actor.items.find(entry => (entry.name === "Flat-Footed" && entry.type === "condition")); 10 | await PF2eConditionManager.removeConditionFromToken(flatfooted._id, token) 11 | } 12 | })(); 13 | 14 | 15 | -------------------------------------------------------------------------------- /archived/pf2e/action_steal.js: -------------------------------------------------------------------------------- 1 | if (canvas.tokens.controlled == 0) { 2 | ui.notifications.warn("You must select a token."); 3 | return 4 | } 5 | 6 | actor.data.data.skills.thi.roll(event); 7 | 8 | ////////////// To chat message data ///////////////// 9 | 10 | let toChat = (content) => { 11 | let chatData = { 12 | user: game.user.id, 13 | content, 14 | speaker: ChatMessage.getSpeaker(), 15 | } 16 | 17 | ChatMessage.create(chatData, {}) 18 | } 19 | 20 | //////////// To chat message data ////////////////// 21 | 22 | 23 | toChat(`

Steal

24 |
25 | You try to take a small object from another creature without being noticed. Typically, you can Steal only an object of negligible Bulk, and you automatically fail if the creature who has the object is in combat or on guard.
26 | 27 |
Attempt a Thievery check to determine if you successfully Steal the object. The DC to Steal is usually the Perception DC of the creature wearing the object. This assumes the object is worn but not closely guarded (like a loosely carried pouch filled with coins, or an object within such a pouch). If the object is in a pocket or similarly protected, you take a -5 penalty to your Thievery check. The GM might increase the DC of your check if the nature of the object makes it harder to steal (such as a very small item in a large pack, or a sheet of parchment mixed in with other documents).
28 | 29 |
You might also need to compare your Thievery check result against the Perception DCs of observers other than the person wearing the object. The GM may increase the Perception DCs of these observers if they're distracted. 30 |
31 | 32 |
33 |

✔️ Success!

34 |
35 | You steal the item without the bearer noticing, or an observer doesn't see you take or attempt to take the item. 36 |
37 | 38 |
39 |

❌ Fail!

40 |
41 | The item's bearer notices your attempt before you can take the object, or an observer sees you take or attempt to take the item. The GM determines the response of any creature that notices your theft. 42 |
43 | 44 |
`) -------------------------------------------------------------------------------- /archived/pf2e/action_trip.js: -------------------------------------------------------------------------------- 1 | const handleCrits = (roll) => roll === 1 ? -10 : (roll === 20 ? 10 : 0); 2 | const options = ['all', 'skill-check', 'athletics', 'trip'] 3 | 4 | 5 | if (canvas.tokens.controlled == 0) { 6 | ui.notifications.warn("You must select a token."); 7 | return 8 | } 9 | 10 | if (game.user.targets.size == 0) { 11 | ui.notifications.warn("You must have a target."); 12 | return 13 | } 14 | 15 | 16 | ////////////// To chat message data ///////////////// 17 | 18 | let toChat = (content) => { 19 | let chatData = { 20 | user: game.user.id, 21 | content, 22 | speaker: ChatMessage.getSpeaker(), 23 | } 24 | 25 | ChatMessage.create(chatData, {}) 26 | } 27 | 28 | //////////// To chat message data ////////////////// 29 | 30 | game.user.targets.forEach(t => { 31 | 32 | token.actor.data.data.skills.ath.roll(event, options, (result) => { 33 | let roll = result._total; 34 | let crit = handleCrits(result.parts[0].rolls[0].result); 35 | let reflexDC = 10 + t.actor.data.data.saves.reflex.totalModifier 36 | 37 | if (roll + crit >= reflexDC + 10) { 38 | toChat(`

Trip

39 | 💥 Crit Success! 40 |
${t.name}: The target falls and lands prone and takes [[1d6]] bludgeoning damage. 41 | 42 |
43 | 44 |
`) 45 | 46 | } else if (roll + crit >= reflexDC) { 47 | toChat(`

Trip

48 | ✔️ Success! 49 |
${t.name}: The target falls and lands prone. 50 | 51 |
52 | 53 |
`) 54 | 55 | } else if (roll + crit < reflexDC - 10) { 56 | 57 | toChat(`

Trip

58 | ❌ Crit Fail! 59 |
${t.name}: You lose your balance and fall and land prone. 60 |
61 | 62 |
`) 63 | 64 | } else if (roll + crit < reflexDC) { 65 | 66 | toChat(`

Trip

67 | ❌ Fail! 68 |
${t.name}: Your action fails to trip the target. 69 |
70 | 71 |
`) 72 | } 73 | 74 | }) 75 | 76 | }) -------------------------------------------------------------------------------- /archived/pf2e/animal_rage.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for (let token of canvas.tokens.controlled) { 4 | if ( 5 | (token.actor.data.data.customModifiers["ac"] || []).some( 6 | (modifier) => modifier.name === "Animal Rage" 7 | ) 8 | ) { 9 | await actor.removeCustomModifier("ac", "Animal Rage"); 10 | /// Remove the line below if you do not wish for your character to lose all temp hp when toggled "off". 11 | await actor.update({ "data.attributes.hp.temp": 0 }); 12 | /// Remove the line above if you do not wish for your character to lose all temp hp when toggled "off". 13 | if ( 14 | token.data.effects.includes( 15 | "systems/pf2e/icons/features/classes/rage.jpg" 16 | ) 17 | ) { 18 | token.toggleEffect("systems/pf2e/icons/features/classes/rage.jpg"); 19 | } 20 | } else { 21 | const tmpHP = 22 | token.actor.data.data.details.level.value + 23 | token.actor.data.data.abilities.con.mod; 24 | if (token.actor.data.data.attributes.hp.temp < tmpHP) { 25 | await actor.update({ "data.attributes.hp.temp": tmpHP }); 26 | } 27 | await actor.addCustomModifier("ac", "Animal Rage", -1, "untyped"); 28 | if ( 29 | !token.data.effects.includes( 30 | "systems/pf2e/icons/features/classes/rage.jpg" 31 | ) 32 | ) { 33 | token.toggleEffect("systems/pf2e/icons/features/classes/rage.jpg"); 34 | } 35 | } 36 | } 37 | } else { 38 | ui.notifications.warn("You must have an actor selected."); 39 | } 40 | })(); -------------------------------------------------------------------------------- /archived/pf2e/apply_spell_effect.js: -------------------------------------------------------------------------------- 1 | //allows bulk adding and removing of Spell Effects to tokens 2 | 3 | let applyChanges = false; 4 | const compendiumName="pf2e.spell-effects"; 5 | const effectCompendium = game.packs.get(compendiumName); 6 | let effectList=[]; 7 | 8 | async function getEffectList(){ 9 | if(effectList.length<=0){ 10 | let effects = await effectCompendium.getContent(); 11 | for(let count=0;count'+ list[count].Name + ''; 23 | } 24 | return optionlist; 25 | } 26 | 27 | async function applyEffect(effectid) 28 | { 29 | let effectItem=effectList[effectid]; 30 | 31 | const item = await fromUuid(effectItem.UUID); 32 | for (const token of canvas.tokens.controlled) { 33 | let existing = token.actor.items.filter(i => i.type === item.type).find(e => e.name === item.name); 34 | if (existing) { 35 | await token.actor.deleteOwnedItem(existing._id); 36 | } else { 37 | let owneditemdata = await token.actor.createOwnedItem(item); 38 | owneditemdata.data.start.value=game.time.worldTime; 39 | } 40 | } 41 | } 42 | 43 | new Dialog({ 44 | title: `Apply Effect`, 45 | content: ` 46 |
47 |
48 | 49 | 51 |
52 |
53 | `, 54 | buttons: { 55 | yes: { 56 | icon: "", 57 | label: `Apply Changes`, 58 | callback: () => applyChanges = true 59 | }, 60 | no: { 61 | icon: "", 62 | label: `Cancel Changes` 63 | }, 64 | }, 65 | default: "yes", 66 | close: html => { 67 | if (applyChanges) { 68 | let effectid = html.find('[name="effectChoice"]')[0].value; 69 | //console.log(effectItem); 70 | applyEffect(effectid); 71 | 72 | } 73 | } 74 | }).render(true); 75 | -------------------------------------------------------------------------------- /archived/pf2e/blood_magic_proc.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for (let token of canvas.tokens.controlled) { 4 | let messageContent = ''; 5 | if ( 6 | (token.actor.data.data.customModifiers["ac"] || []).some( 7 | (modifier) => modifier.name === "Blood Magic" 8 | ) 9 | ) { 10 | await actor.removeCustomModifier("ac", "Blood Magic"); 11 | if ( 12 | token.data.effects.includes( 13 | "https://i.imgur.com/Jtub936.png" 14 | ) 15 | ) { 16 | token.toggleEffect("https://i.imgur.com/Jtub936.png"); 17 | } 18 | messageContent = 'The scales begin to fade away...' 19 | } else { 20 | await actor.addCustomModifier("ac", "Blood Magic", +1, "untyped"); 21 | if ( 22 | !token.data.effects.includes( 23 | "https://i.imgur.com/Jtub936.png" 24 | ) 25 | ) { 26 | token.toggleEffect("https://i.imgur.com/Jtub936.png"); 27 | } 28 | messageContent = 'Draconic scales strengthen your defenses.' 29 | }; 30 | // create the message 31 | 32 | if (messageContent !== '') { 33 | let chatData = { 34 | user: game.user._id, 35 | speaker: ChatMessage.getSpeaker(), 36 | content: messageContent, 37 | }; 38 | 39 | await ChatMessage.create(chatData, {}); 40 | } 41 | } 42 | } else { 43 | ui.notifications.warn("You must have an actor selected."); 44 | } 45 | })(); -------------------------------------------------------------------------------- /archived/pf2e/consume_arrow.js: -------------------------------------------------------------------------------- 1 | if (!actor) { 2 | ui.notifications.warn("You must select yourself."); 3 | } 4 | 5 | let updates = []; 6 | let consumed = ""; 7 | // Use Arrows 8 | (async () => { 9 | let item = actor.items.find(i=> i.name==="Arrows"); 10 | 11 | if(item === null) return 12 | 13 | if (item.data.data.quantity.value < 1) { 14 | 15 | ui.notifications.warn(`${game.user.name} not enough ${name} remaining`); 16 | } else { 17 | 18 | updates.push({"_id": item._id, "data.quantity.value": item.data.data.quantity.value - 1}); 19 | consumed += `${item.data.data.quantity.value - 1} arrows left
`; 20 | } 21 | 22 | if (updates.length > 0) { 23 | actor.updateEmbeddedEntity("OwnedItem", updates); 24 | } 25 | ChatMessage.create({ 26 | user: game.user._id, 27 | speaker: { actor: actor, alias: actor.name }, 28 | content: consumed, 29 | type: CONST.CHAT_MESSAGE_TYPES.OTHER 30 | }); 31 | })(); -------------------------------------------------------------------------------- /archived/pf2e/consume_bolt.js: -------------------------------------------------------------------------------- 1 | if (!actor) { 2 | ui.notifications.warn("You must select yourself."); 3 | } 4 | 5 | let updates = []; 6 | let consumed = ""; 7 | // Use Bolts 8 | (async () => { 9 | let item = actor.items.find(i=> i.name==="Bolts"); 10 | 11 | if(item === null) return 12 | 13 | if (item.data.data.quantity.value < 1) { 14 | 15 | ui.notifications.warn(`${game.user.name} not enough ${name} remaining`); 16 | } else { 17 | 18 | updates.push({"_id": item._id, "data.quantity.value": item.data.data.quantity.value - 1}); 19 | consumed += `${item.data.data.quantity.value - 1} arrows left
`; 20 | } 21 | 22 | if (updates.length > 0) { 23 | actor.updateEmbeddedEntity("OwnedItem", updates); 24 | } 25 | ChatMessage.create({ 26 | user: game.user._id, 27 | speaker: { actor: actor, alias: actor.name }, 28 | content: consumed, 29 | type: CONST.CHAT_MESSAGE_TYPES.OTHER 30 | }); 31 | })(); -------------------------------------------------------------------------------- /archived/pf2e/consume_bullets.js: -------------------------------------------------------------------------------- 1 | if (!actor) { 2 | ui.notifications.warn("You must select yourself."); 3 | } 4 | 5 | let updates = []; 6 | let consumed = ""; 7 | // Use Bullets 8 | (async () => { 9 | let item = actor.items.find(i=> i.name==="Bullets"); 10 | 11 | if(item === null) return 12 | 13 | if (item.data.data.quantity.value < 1) { 14 | 15 | ui.notifications.warn(`${game.user.name} not enough ${name} remaining`); 16 | } else { 17 | 18 | updates.push({"_id": item._id, "data.quantity.value": item.data.data.quantity.value - 1}); 19 | consumed += `${item.data.data.quantity.value - 1} bullets left
`; 20 | } 21 | 22 | if (updates.length > 0) { 23 | actor.updateEmbeddedEntity("OwnedItem", updates); 24 | } 25 | ChatMessage.create({ 26 | user: game.user._id, 27 | speaker: { actor: actor, alias: actor.name }, 28 | content: consumed, 29 | type: CONST.CHAT_MESSAGE_TYPES.OTHER 30 | }); 31 | })(); -------------------------------------------------------------------------------- /archived/pf2e/dueling_cape.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for (let token of canvas.tokens.controlled) { 4 | if ( 5 | (token.actor.data.data.customModifiers["ac"] || []).some( 6 | (modifier) => modifier.name === "Dueling Cape" 7 | ) 8 | ) { 9 | await actor.removeCustomModifier("ac", "Dueling Cape"); 10 | if ( 11 | token.data.effects.includes( 12 | "systems/pf2e/icons/equipment/adventuring-gear/dueling-cape.jpg" 13 | ) 14 | ) { 15 | token.toggleEffect("systems/pf2e/icons/equipment/adventuring-gear/dueling-cape.jpg"); 16 | } 17 | } else { 18 | await actor.addCustomModifier("ac", "Dueling Cape", 1, "circumstance"); 19 | if ( 20 | !token.data.effects.includes( 21 | "systems/pf2e/icons/spells/fire-shield.jpg" 22 | ) 23 | ) { 24 | token.toggleEffect("systems/pf2e/icons/equipment/adventuring-gear/dueling-cape.jpg"); 25 | } 26 | } 27 | } 28 | } else { 29 | ui.notifications.warn("You must have an actor selected."); 30 | } 31 | })(); -------------------------------------------------------------------------------- /archived/pf2e/dueling_parry.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for (let token of canvas.tokens.controlled) { 4 | if ( 5 | (token.actor.data.data.customModifiers["ac"] || []).some( 6 | (modifier) => modifier.name === "Dueling Parry" 7 | ) 8 | ) { 9 | await actor.removeCustomModifier("ac", "Dueling Parry"); 10 | if ( 11 | token.data.effects.includes( 12 | "https://i.imgur.com/QeRgfwS.png" 13 | ) 14 | ) { 15 | token.toggleEffect("https://i.imgur.com/QeRgfwS.png"); 16 | } 17 | } else { 18 | await actor.addCustomModifier("ac", "Dueling Parry", +1, "untyped"); 19 | if ( 20 | !token.data.effects.includes( 21 | "https://i.imgur.com/QeRgfwS.png" 22 | ) 23 | ) { 24 | token.toggleEffect("https://i.imgur.com/QeRgfwS.png"); 25 | } 26 | } 27 | } 28 | } else { 29 | ui.notifications.warn("You must have an actor selected."); 30 | } 31 | })(); -------------------------------------------------------------------------------- /archived/pf2e/fury_instinct_rage.js: -------------------------------------------------------------------------------- 1 | const RAGE_DAMAGE = 2; // increase for giant instinct or higher levels 2 | 3 | (async () => { 4 | if (actor) { 5 | for (let token of canvas.tokens.controlled) { 6 | if ( 7 | (token.actor.data.data.customModifiers["ac"] || []).some( 8 | (modifier) => modifier.name === "Rage" 9 | ) 10 | ) { 11 | await actor.removeCustomModifier("ac", "Rage"); 12 | await actor.removeCustomModifier("damage", "Rage"); 13 | /// Remove the line below if you do not wish for your character to lose all temp hp when toggled "off". 14 | await actor.update({ "data.attributes.hp.temp": 0 }); 15 | /// Remove the line above if you do not wish for your character to lose all temp hp when toggled "off". 16 | await actor.update({ "data.attributes.speed.value": 25 }); 17 | if ( 18 | token.data.effects.includes( 19 | "https://i.imgur.com/VFOwIY0.png" 20 | ) 21 | ) { 22 | token.toggleEffect("https://i.imgur.com/VFOwIY0.png"); 23 | } 24 | } else { 25 | const tmpHP = 26 | token.actor.data.data.details.level.value + 27 | token.actor.data.data.abilities.con.mod; 28 | if (token.actor.data.data.attributes.hp.temp < tmpHP) { 29 | await actor.update({ "data.attributes.hp.temp": tmpHP }); 30 | } 31 | await actor.addCustomModifier("ac", "Rage", -1, "untyped"); 32 | await actor.update({ "data.attributes.speed.value": 35 }); 33 | await actor.addCustomModifier("damage", "Rage", RAGE_DAMAGE, "status"); 34 | if ( 35 | !token.data.effects.includes( 36 | "https://i.imgur.com/VFOwIY0.png" 37 | ) 38 | ) { 39 | token.toggleEffect("https://i.imgur.com/VFOwIY0.png"); 40 | } 41 | } 42 | } 43 | } else { 44 | ui.notifications.warn("You must have an actor selected."); 45 | } 46 | })(); -------------------------------------------------------------------------------- /archived/pf2e/gozreh_sickness.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for ( let token of canvas.tokens.controlled ) { 4 | let messageContent = ''; 5 | if ((token.actor.data.data.customModifiers['attack'] || []).some(modifier => modifier.name === 'Gozreh Sickness')) { 6 | await token.actor.removeCustomModifier('attack', 'Gozreh Sickness'); 7 | await token.actor.removeCustomModifier('damage', 'Gozreh Sickness'); 8 | await actor.removeCustomModifier("ac", "Gozreh Sickness"); 9 | await actor.removeCustomModifier("fortitude", "Gozreh Sickness"); 10 | 11 | if (token.data.effects.includes("systems/pf2e/icons/spells/daze.jpg")) { 12 | await token.toggleEffect("systems/pf2e/icons/spells/daze.jpg") 13 | } 14 | 15 | messageContent = 'Is no longer Sickened by Gozreh!' 16 | } else { 17 | await token.actor.addCustomModifier('attack', 'Gozreh Sickness', -1, 'status'); 18 | await token.actor.addCustomModifier('damage', 'Gozreh Sickness', -1, 'status'); 19 | await actor.addCustomModifier("ac", "Gozreh Sickness", -1, "status"); 20 | await actor.addCustomModifier("fortitude", "Gozreh Sickness", -1, "status"); 21 | 22 | if (!token.data.effects.includes("systems/pf2e/icons/spells/daze.jpg")) { 23 | await token.toggleEffect("systems/pf2e/icons/spells/daze.jpg") 24 | } 25 | 26 | messageContent = 'Is Sickened by Gozreh!' 27 | }; 28 | // create the message 29 | 30 | if (messageContent !== '') { 31 | let chatData = { 32 | user: game.user._id, 33 | speaker: ChatMessage.getSpeaker(), 34 | content: messageContent, 35 | }; 36 | 37 | await ChatMessage.create(chatData, {}); 38 | } 39 | } 40 | } else { 41 | ui.notifications.warn("You must have an actor selected."); 42 | } 43 | })(); -------------------------------------------------------------------------------- /archived/pf2e/inspire_courage.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for ( let token of canvas.tokens.controlled ) { 4 | let messageContent = ''; 5 | if ((token.actor.data.data.customModifiers['attack'] || []).some(modifier => modifier.name === 'Inspire Courage')) { 6 | await token.actor.removeCustomModifier('attack', 'Inspire Courage'); 7 | await token.actor.removeCustomModifier('damage', 'Inspire Courage'); 8 | 9 | if (token.data.effects.includes("systems/pf2e/icons/conditions-2/status_hero.png")) { 10 | await token.toggleEffect("systems/pf2e/icons/conditions-2/status_hero.png") 11 | } 12 | 13 | messageContent = 'Is no longer Inspired.' 14 | } else { 15 | await token.actor.addCustomModifier('attack', 'Inspire Courage', 1, 'status'); 16 | await token.actor.addCustomModifier('damage', 'Inspire Courage', 1, 'status'); 17 | 18 | if (!token.data.effects.includes("systems/pf2e/icons/conditions-2/status_hero.png")) { 19 | await token.toggleEffect("systems/pf2e/icons/conditions-2/status_hero.png") 20 | } 21 | 22 | messageContent = 'Is Inspired!' 23 | }; 24 | // create the message 25 | 26 | if (messageContent !== '') { 27 | let chatData = { 28 | user: game.user._id, 29 | speaker: ChatMessage.getSpeaker(), 30 | content: messageContent, 31 | }; 32 | 33 | await ChatMessage.create(chatData, {}); 34 | } 35 | } 36 | } else { 37 | ui.notifications.warn("You must have an actor selected."); 38 | } 39 | })(); -------------------------------------------------------------------------------- /archived/pf2e/parry.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for (let token of canvas.tokens.controlled) { 4 | if ( 5 | (token.actor.data.data.customModifiers["ac"] || []).some( 6 | (modifier) => modifier.name === "Parry" 7 | ) 8 | ) { 9 | await actor.removeCustomModifier("ac", "Parry"); 10 | if ( 11 | token.data.effects.includes( 12 | "systems/pf2e/icons/equipment/weapons/main-gauche.png" 13 | ) 14 | ) { 15 | token.toggleEffect("systems/pf2e/icons/equipment/weapons/main-gauche.png"); 16 | } 17 | } else { 18 | await actor.addCustomModifier("ac", "Parry", 1, "circumstance"); 19 | if ( 20 | !token.data.effects.includes( 21 | "systems/pf2e/icons/spells/shield.jpg" 22 | ) 23 | ) { 24 | token.toggleEffect("systems/pf2e/icons/equipment/weapons/main-gauche.png"); 25 | } 26 | } 27 | } 28 | } else { 29 | ui.notifications.warn("You must have an actor selected."); 30 | } 31 | })(); -------------------------------------------------------------------------------- /archived/pf2e/premonition_of_avoidance.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for ( let token of canvas.tokens.controlled ) { 4 | let messageContent = ''; 5 | if ((token.actor.data.data.customModifiers['attack'] || []).some(modifier => modifier.name === 'Premonition Of Avoidance')) { 6 | await token.actor.removeCustomModifier('attack', 'Premonition Of Avoidance'); 7 | 8 | await actor.removeCustomModifier("fortitude", "Premonition Of Avoidance"); 9 | await actor.removeCustomModifier("reflex", "Premonition Of Avoidance"); 10 | await actor.removeCustomModifier("will", "Premonition Of Avoidance"); 11 | 12 | if (token.data.effects.includes("https://assets.forge-vtt.com/bazaar/systems/pf2e/assets/icons/equipment/adventuring-gear/hourglass.jpg")) { 13 | await token.toggleEffect("https://assets.forge-vtt.com/bazaar/systems/pf2e/assets/icons/equipment/adventuring-gear/hourglass.jpg") 14 | } 15 | 16 | messageContent = 'The blessing from Pharasma begins to fade...' 17 | 18 | } else { 19 | await token.actor.addCustomModifier('attack', 'Premonition Of Avoidance', 0, 'status'); 20 | 21 | await actor.addCustomModifier("fortitude", "Premonition Of Avoidance", +2, "status"); 22 | await actor.addCustomModifier("reflex", "Premonition Of Avoidance", +2, "status"); 23 | await actor.addCustomModifier("will", "Premonition Of Avoidance", +2, "status"); 24 | 25 | if (!token.data.effects.includes("https://assets.forge-vtt.com/bazaar/systems/pf2e/assets/icons/equipment/adventuring-gear/hourglass.jpg")) { 26 | await token.toggleEffect("https://assets.forge-vtt.com/bazaar/systems/pf2e/assets/icons/equipment/adventuring-gear/hourglass.jpg") 27 | } 28 | 29 | messageContent = 'Pharasma attempts to grant Bhelroth foresight!' 30 | }; 31 | // create the message 32 | 33 | if (messageContent !== '') { 34 | let chatData = { 35 | user: game.user._id, 36 | speaker: ChatMessage.getSpeaker(), 37 | content: messageContent, 38 | }; 39 | 40 | await ChatMessage.create(chatData, {}); 41 | } 42 | } 43 | } else { 44 | ui.notifications.warn("You must have an actor selected."); 45 | } 46 | })(); -------------------------------------------------------------------------------- /archived/pf2e/print_spell_dc.js: -------------------------------------------------------------------------------- 1 | //Spell DC: This will print to the chat the spell DC. 2 | //Replace the name with your character's Spellcasting Entry. 3 | 4 | let name = 'Spontaneous Primal Spells'; 5 | 6 | let dc= (actor.data.items).find(item => item.name 7 | == name).data.spelldc.dc; 8 | 9 | let string = 'DC is ' + dc; 10 | 11 | ChatMessage.create({content: string, speaker: ChatMessage.getSpeaker({actor: actor})}); 12 | -------------------------------------------------------------------------------- /archived/pf2e/rage_effect.js: -------------------------------------------------------------------------------- 1 | // starts rage using the rage effect. With the new effect system in place other rage macros should be obsolete (including the one included in the core system 2 | 3 | const ITEM_UUID = 'Compendium.pf2e.feature-effects.z3uyCMBddrPK5umr'; // Effect: Rage 4 | 5 | (async () => { 6 | const item = await fromUuid(ITEM_UUID); 7 | for (const token of canvas.tokens.controlled) { 8 | let existing = token.actor.items.filter(i => i.type === item.type).find(e => e.name === item.name); 9 | if (existing) { 10 | await token.actor.deleteOwnedItem(existing._id); 11 | } else { 12 | let owneditemdata = await token.actor.createOwnedItem(item); 13 | owneditemdata.data.start.value=game.time.worldTime; 14 | } 15 | } 16 | })(); 17 | -------------------------------------------------------------------------------- /archived/pf2e/rain_of_embers_stance.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for (let token of canvas.tokens.controlled) { 4 | if ( 5 | (token.actor.data.data.customModifiers["ac"] || []).some( 6 | (modifier) => modifier.name === "Embers Stance" 7 | ) 8 | ) { 9 | await actor.removeCustomModifier("ac", "Embers Stance"); 10 | if ( 11 | token.data.effects.includes( 12 | "systems/pf2e/icons/spells/fire-shield.jpg" 13 | ) 14 | ) { 15 | token.toggleEffect("systems/pf2e/icons/spells/fire-shield.jpg"); 16 | } 17 | } else { 18 | await actor.addCustomModifier("ac", "Embers Stance", 1, "status"); 19 | if ( 20 | !token.data.effects.includes( 21 | "systems/pf2e/icons/spells/fire-shield.jpg" 22 | ) 23 | ) { 24 | token.toggleEffect("systems/pf2e/icons/spells/fire-shield.jpg"); 25 | } 26 | } 27 | } 28 | } else { 29 | ui.notifications.warn("You must have an actor selected."); 30 | } 31 | })(); -------------------------------------------------------------------------------- /archived/pf2e/raise_shield.js: -------------------------------------------------------------------------------- 1 | // 'Raise Shield' macro that will raised a shield the character has equipped 2 | let messageContent = '' 3 | if (!actor) { 4 | ui.notifications.warn("You must have an actor selected."); 5 | } 6 | 7 | (async () => { 8 | for (let token of canvas.tokens.controlled) { 9 | const shield = token.actor.data.items.filter(item => item.type === 'armor') 10 | .filter(armor => armor.data.armorType.value === 'shield') 11 | .find(shield => shield.data.equipped.value); 12 | if (shield) { 13 | if (token.data.effects.includes("systems/pf2e/icons/equipment/shields/steel-shield.jpg")) { 14 | actor.removeCustomModifier('ac', 'Raised Shield') 15 | token.toggleEffect("systems/pf2e/icons/equipment/shields/steel-shield.jpg") 16 | messageContent = 'Lowers their shield' 17 | } else { 18 | actor.addCustomModifier('ac', 'Raised Shield', Number(shield.data.armor.value), 'circumstance'); 19 | token.toggleEffect("systems/pf2e/icons/equipment/shields/steel-shield.jpg") 20 | messageContent = 'Raises their shield' 21 | }; 22 | 23 | 24 | } else ui.notifications.warn("You must have a shield equipped."); 25 | } 26 | })(); 27 | // create the message 28 | if (messageContent !== '') { 29 | let chatData = { 30 | user: game.user._id, 31 | speaker: ChatMessage.getSpeaker(), 32 | content: messageContent, 33 | }; 34 | ChatMessage.create(chatData, {}); 35 | } -------------------------------------------------------------------------------- /archived/pf2e/specialty_crafting.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for ( let token of canvas.tokens.controlled ) { 4 | let messageContent = ''; 5 | if ((token.actor.data.data.customModifiers['crafting'] || []).some(modifier => modifier.name === 'Specialty Crafting')) { 6 | await token.actor.removeCustomModifier('crafting', 'Specialty Crafting'); 7 | 8 | if (token.data.effects.includes("systems/pf2e/icons/equipment/adventuring-gear/hammer.jpg")) { 9 | await token.toggleEffect("systems/pf2e/icons/equipment/adventuring-gear/hammer.jpg") 10 | } 11 | 12 | } else { 13 | await token.actor.addCustomModifier('crafting', 'Specialty Crafting', +1, 'status'); 14 | 15 | if (!token.data.effects.includes("systems/pf2e/icons/equipment/adventuring-gear/hammer.jpg")) { 16 | await token.toggleEffect("systems/pf2e/icons/equipment/adventuring-gear/hammer.jpg") 17 | } 18 | 19 | }; 20 | // create the message 21 | 22 | if (messageContent !== '') { 23 | let chatData = { 24 | user: game.user._id, 25 | speaker: ChatMessage.getSpeaker(), 26 | content: messageContent, 27 | }; 28 | 29 | await ChatMessage.create(chatData, {}); 30 | } 31 | } 32 | } else { 33 | ui.notifications.warn("You must have an actor selected."); 34 | } 35 | })(); -------------------------------------------------------------------------------- /archived/pf2e/spell_attack.js: -------------------------------------------------------------------------------- 1 | //Spell Attack: This rolls the spell attack for any "Spontaneous Primal Spells". 2 | //Replace the name with whatever the name of your Spellcasting Entry is in your spellbook to roll that spell attack. 3 | 4 | let name = 'Spontaneous Primal Spells'; 5 | 6 | let modifier = (actor.data.items).find(item => item.name 7 | == name).data.spelldc.value; 8 | 9 | const roll = new Roll('1d20+' + modifier); 10 | 11 | roll.roll(); 12 | 13 | roll.toMessage({ flavor: "Spell Attack", speaker: ChatMessage.getSpeaker({actor: actor})}); 14 | -------------------------------------------------------------------------------- /archived/pf2e/spell_damage.js: -------------------------------------------------------------------------------- 1 | //Spell Damage: This will roll the damage of your spell. 2 | //Simply replace the name with whatever spell you want to roll damage for. 3 | //Also be sure to change the text in the message. 4 | 5 | let name= 'Acid Splash';const damage = (actor.data.items).find(item => item.name 6 | == name).data.damage.value; 7 | 8 | const roll = new Roll(damage); 9 | 10 | roll.roll(); 11 | 12 | roll.toMessage({ flavor: "Acid Splash Damage", speaker: ChatMessage.getSpeaker({actor: actor})}); 13 | -------------------------------------------------------------------------------- /archived/pf2e/strike_attack.js: -------------------------------------------------------------------------------- 1 | //Script for macro that rolls the attack of a "strike" 2 | //Must select a character with the associated "strike" for macro to work 3 | //Replace the weapon 'Throwing Knife' with the name of the strike you want to attack with 4 | //ex 'Fist' or 'Dagger' 5 | let weapon = 'Throwing Knife'; 6 | (actor.data.data.actions ?? []).filter(action => action.type === 'strike').find(strike => strike.name === weapon)?.attack(event); 7 | -------------------------------------------------------------------------------- /archived/pf2e/strike_damage.js: -------------------------------------------------------------------------------- 1 | //Script for macro that rolls the damage of a "strike" 2 | //Must select a character with the associated "strike" for macro to work 3 | //Replace the weapon 'Throwing Knife' with the name of the strike you want to attack with 4 | //ex 'Fist' or 'Dagger 5 | let weapon = 'Throwing Knife'; 6 | let bonusdice = ''; 7 | (actor.data.data.actions ?? []).filter(action => action.type === 'strike').find(strike => strike.name === weapon)?.damage(event, [bonusdice]); 8 | -------------------------------------------------------------------------------- /archived/pf2e/sweep_attack.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for ( let token of canvas.tokens.controlled ) { 4 | let messageContent = ''; 5 | if ((token.actor.data.data.customModifiers['attack'] || []).some(modifier => modifier.name === 'Sweep Attack')) { 6 | await token.actor.removeCustomModifier('attack', 'Sweep Attack'); 7 | 8 | if (token.data.effects.includes("systems/pf2e/icons/equipment/weapons/battle-axe.jpg")) { 9 | await token.toggleEffect("systems/pf2e/icons/equipment/weapons/battle-axe.jpg") 10 | } 11 | } else { 12 | await token.actor.addCustomModifier('attack', 'Sweep Attack', 1, 'status'); 13 | 14 | if (!token.data.effects.includes("systems/pf2e/icons/equipment/weapons/battle-axe.jpg")) { 15 | await token.toggleEffect("systems/pf2e/icons/equipment/weapons/battle-axe.jpg") 16 | } 17 | 18 | messageContent = 'Readies to Strike a new foe!' 19 | }; 20 | // create the message 21 | 22 | if (messageContent !== '') { 23 | let chatData = { 24 | user: game.user._id, 25 | speaker: ChatMessage.getSpeaker(), 26 | content: messageContent, 27 | }; 28 | 29 | await ChatMessage.create(chatData, {}); 30 | } 31 | } 32 | } else { 33 | ui.notifications.warn("You must have an actor selected."); 34 | } 35 | })(); -------------------------------------------------------------------------------- /archived/pf2e/take_a_breather.js: -------------------------------------------------------------------------------- 1 | let toChat = (content) => { 2 | let chatData = { 3 | user: game.user.id, 4 | content, 5 | speaker: ChatMessage.getSpeaker(), 6 | } 7 | ChatMessage.create(chatData, {}) 8 | } 9 | 10 | let applyChanges = false; 11 | new Dialog({ 12 | title: `Take a Breather`, 13 | content: ` 14 |
Rest for 10 minutes, spend a resolve point, and regain stamina?
15 | `, 16 | buttons: { 17 | yes: { 18 | icon: "", 19 | label: `Take a Breather`, 20 | callback: () => applyChanges = true 21 | }, 22 | no: { 23 | icon: "", 24 | label: `Cancel` 25 | }, 26 | }, 27 | default: "yes", 28 | close: html => { 29 | if (applyChanges) { 30 | for ( let token of canvas.tokens.controlled ) { 31 | const {name} = token; 32 | console.log(token); 33 | const {resolve, sp} = token.actor.data.data.attributes; 34 | console.log(resolve, sp); 35 | if (resolve.value > 0) { 36 | let oldSP = sp.value; 37 | toChat(`${name} has ${sp.value}/${sp.max} SP and spends a resolve point, taking a 10 minute breather. Stamina Refreshed.`); 38 | token.actor.update({ 39 | 'data.attributes.sp.value': sp.max, 40 | 'data.attributes.resolve.value': resolve.value-1 41 | }); 42 | } else { 43 | toChat(`${name} is tired and needs to go to bed! No resolve points remaining.`); 44 | } 45 | } 46 | } 47 | } 48 | }).render(true); -------------------------------------------------------------------------------- /archived/pf2e/terrifying_resistance.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | if (actor) { 3 | for ( let token of canvas.tokens.controlled ) { 4 | let messageContent = ''; 5 | if ((token.actor.data.data.customModifiers['attack'] || []).some(modifier => modifier.name === 'Terrifying Resistance')) { 6 | await token.actor.removeCustomModifier('attack', 'Terrifying Resistance'); 7 | 8 | await actor.removeCustomModifier("fortitude", "Terrifying Resistance"); 9 | await actor.removeCustomModifier("reflex", "Terrifying Resistance"); 10 | await actor.removeCustomModifier("will", "Terrifying Resistance"); 11 | 12 | if (token.data.effects.includes("systems/pf2e/icons/spells/dominate.jpg")) { 13 | await token.toggleEffect("systems/pf2e/icons/spells/dominate.jpg") 14 | } 15 | 16 | messageContent = 'The terrifying aura begins to fade...' 17 | 18 | } else { 19 | await token.actor.addCustomModifier('attack', 'Terrifying Resistance', 0, 'status'); 20 | 21 | await actor.addCustomModifier("fortitude", "Terrifying Resistance", +1, "status"); 22 | await actor.addCustomModifier("reflex", "Terrifying Resistance", +1, "status"); 23 | await actor.addCustomModifier("will", "Terrifying Resistance", +1, "status"); 24 | 25 | if (!token.data.effects.includes("systems/pf2e/icons/spells/dominate.jpg")) { 26 | await token.toggleEffect("systems/pf2e/icons/spells/dominate.jpg") 27 | } 28 | 29 | messageContent = 'Your presence eminates an aura of terror!' 30 | }; 31 | // create the message 32 | 33 | if (messageContent !== '') { 34 | let chatData = { 35 | user: game.user._id, 36 | speaker: ChatMessage.getSpeaker(), 37 | content: messageContent, 38 | }; 39 | 40 | await ChatMessage.create(chatData, {}); 41 | } 42 | } 43 | } else { 44 | ui.notifications.warn("You must have an actor selected."); 45 | } 46 | })(); -------------------------------------------------------------------------------- /archived/roll/token_hp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Roll/Reroll selected token HP 3 | * Author: Tielc#7191 4 | */ 5 | 6 | const tokens = canvas.tokens.controlled; 7 | let choice = 0; 8 | 9 | if (tokens.length > 0){ 10 | tokens.forEach(rollHP); 11 | } else { 12 | printMessage("No Tokens were selected"); 13 | } 14 | 15 | async function rollHP(token, index){ 16 | let actor = token.actor; 17 | let formula = actor.data.data.attributes.hp.formula; 18 | 19 | if (actor.data.type != "npc" || !formula) return; 20 | 21 | let hp = (await new Roll(formula).roll()).total; 22 | 23 | await actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); 24 | 25 | printMessage('

' + actor.data.name + '

HP: ' + actor.data.data.attributes.hp.value + '/' + actor.data.data.attributes.hp.max + '(' + token.data._id + ')'); 26 | } 27 | 28 | function printMessage(message){ 29 | let chatData = { 30 | user : game.user._id, 31 | content : message, 32 | blind: true, 33 | whisper : game.users.filter(u => u.isGM).map(u => u.id) 34 | }; 35 | 36 | ChatMessage.create(chatData,{}); 37 | } -------------------------------------------------------------------------------- /archived/token/selective_select.js: -------------------------------------------------------------------------------- 1 | // given a selection of tokens, allows you to select only players, npcs, or by disposition (useful when applying a spell or condition to a large group of enemies or allies 2 | 3 | async function selectOnly(selectChoice) 4 | { 5 | let release=false; 6 | for (const token of canvas.tokens.controlled) { 7 | switch(selectChoice){ 8 | case "playersOnly": 9 | if(!token.actor.hasPlayerOwner) 10 | release=true; 11 | break; 12 | case "nonHostiles": 13 | if(token.data.disposition<0) 14 | release=true; 15 | break; 16 | case "hostiles": 17 | if(token.data.disposition>=0) 18 | release=true; 19 | break; 20 | case "NPCs": 21 | if(token.actor.hasPlayerOwner) 22 | release=true; 23 | } 24 | if(release) 25 | token.release(); 26 | release=false; 27 | } 28 | } 29 | let applyChanges=false; 30 | new Dialog({ 31 | title: `Selective Select`, 32 | content: ` 33 |
34 |
35 | 41 |
42 |
43 | `, 44 | buttons: { 45 | yes: { 46 | icon: "", 47 | label: `Apply Changes`, 48 | callback: () => applyChanges = true 49 | }, 50 | no: { 51 | icon: "", 52 | label: `Cancel Changes` 53 | }, 54 | }, 55 | default: "yes", 56 | close: html => { 57 | if (applyChanges) { 58 | let applyType = html.find('[name="select-type"]')[0].value || null; 59 | if(applyType) 60 | selectOnly(applyType); 61 | } 62 | } 63 | }).render(true); 64 | -------------------------------------------------------------------------------- /archived/token/show_token_artwork.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Macro showing Character's Artwork for selected token 3 | * 4 | * @author Forien#2130 5 | * @url https://patreon.com/forien 6 | * @licence MIT 7 | */ 8 | 9 | if (token === undefined) { 10 | ui.notifications.warn("Please select token first."); 11 | } else { 12 | let tActor = token.actor; 13 | let ip = new ImagePopout(tActor.data.img, { 14 | title: tActor.name, 15 | shareable: true, 16 | uuid: tActor.uuid 17 | }).render(true); 18 | ip.shareImage(); 19 | } 20 | -------------------------------------------------------------------------------- /archived/token/token_multi_select.js: -------------------------------------------------------------------------------- 1 | /* 2 | Swap the selected token with another of similar name via a 3 | drop-down menu in a dialog box. 4 | 5 | Tokens for each character should be named similarly but end with 6 | '_walking.png', '_fighting.png', and '_sneaking.png'. For example, 7 | 'talion_walking.png', 'talion_fighting.png', and 'talion_sneaking.png' 8 | 9 | If a token does not exist, mystery man will be automatically selected. 10 | */ 11 | 12 | if (actor !== undefined && actor !== null) { 13 | let d = new Dialog({ 14 | title: 'Token Mogrifier', 15 | content: "

Select a new token

" + 16 | "", 22 | buttons: { 23 | ok: { 24 | icon: '', 25 | label: "Do it!", 26 | callback: () => 27 | token.document.update({ 28 | img: token.data.img.slice(0, token.data.img.lastIndexOf('_')) + document.getElementById("token").value 29 | }) 30 | }, 31 | cancel: { 32 | icon: '', 33 | label: "Nevermind", 34 | callback: () => {} 35 | } 36 | } 37 | }); 38 | d.render(true); 39 | } else { 40 | ui.notifications.warn("Please select a token."); 41 | } 42 | -------------------------------------------------------------------------------- /dsa5e/3d20_skill_roll.js: -------------------------------------------------------------------------------- 1 | //Create a roll term with flavor text and execute the roll. Wait for the result and make the roll async. 2 | // Note: Instead of "evaluate" --> "roll" can be used they seem identical 3 | let dies = await new Roll("1d20[black]+1d20[red]+1d20[yellow]").evaluate({async: true}); 4 | 5 | // Create a html based Chat message which will be outputed. 6 | let ChatData={ 7 | // let the roll take part as a roll of the actor. If speaker ist deleted then the roll takes place as the player. 8 | speaker: ChatMessage.getSpeaker({token: actor}), 9 | // This was asked for by the Dice so Nice API. Currently not clear what it exactly does 10 | type: CONST.CHAT_MESSAGE_TYPES.ROLL, 11 | // Rolls a pool of dice/diceterms. If more than one diceterm/dices are rolled then an array of objects needs to be defined --> [r1,r2,r3] 12 | rolls: [dies], 13 | // This was asked for by the Dice so Nice API. Currently not clear what it exactly does 14 | rollMode: game.settings.get("core", "rollMode"), 15 | // Create HTML template for Chat message output. Each Dice in the dice term is an object in an array thus we need to adress it with dies.dice[x]. Remember: arrays start with an index of 0 in java script 16 | content:` 17 |
18 |
19 |
20 |
21 |
22 |
    23 |
  1. ${dies.dice[0].results[0].result}
  2. 24 |
  3. ${dies.dice[1].results[0].result}
  4. 25 |
  5. ${dies.dice[2].results[0].result}
  6. 26 |
27 | 28 |
29 |
30 |
31 |

${dies.dice[0].results[0].result} / ${dies.dice[1].results[0].result} / ${dies.dice[2].results[0].result}

32 |
33 |
` 34 | } 35 | // Create the Chat message 36 | ChatMessage.create(ChatData); 37 | -------------------------------------------------------------------------------- /misc/actor_to_combat.js: -------------------------------------------------------------------------------- 1 | /* 2 | Display a prompt which allows you to select actors to add to the current combat encounter. 3 | This is especially useful if you are running an encounter without a scene or tokens. 4 | You can see it in action here: https://cdn.discordapp.com/attachments/960551198342139995/966856419787833384/Peek_2022-04-21_20-21.mp4 5 | */ 6 | 7 | Dialog.confirm({ 8 | title: "Who would you like to add to the current combat?", 9 | content: `
`, 10 | render: html => { 11 | const section = html[0].querySelector("section"); 12 | const template = html[0].querySelector("template"); 13 | 14 | for (const actor of game.actors) { 15 | const item = document.createElement("div"); 16 | item.style.margin = ".5em 0"; 17 | 18 | const name = document.createElement("span"); 19 | name.slot = "name"; 20 | name.textContent = actor.name; 21 | item.append(name); 22 | 23 | const id = document.createElement("code"); 24 | id.slot = "id"; 25 | id.textContent = actor.id; 26 | item.append(id); 27 | 28 | const shadowRoot = item.attachShadow({ mode: "open" }); 29 | shadowRoot.appendChild(template.content.cloneNode(true)); 30 | 31 | section.append(item); 32 | } 33 | }, 34 | yes: html => { 35 | const updates = []; 36 | html[0].querySelectorAll("template ~ section > div").forEach(el => { 37 | const id = el.querySelector("code").textContent; 38 | let quantity = el.shadowRoot.querySelector("input").value; 39 | while (quantity > 0) { 40 | updates.push({ "actorId": id }); 41 | quantity--; 42 | } 43 | }); 44 | game.combat?.createEmbeddedDocuments("Combatant", updates); 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /misc/announce_round_number.js: -------------------------------------------------------------------------------- 1 | let messageContent = `
ROUND ${game.combat.round}

` 2 | ChatMessage.create({content: messageContent}); 3 | -------------------------------------------------------------------------------- /misc/create_ambient_light.js: -------------------------------------------------------------------------------- 1 | // Create a (pre-configured) lightsource on the current scene. 2 | // This example is a blue light for "activating a stargate." 3 | 4 | canvas.scene.createEmbeddedDocuments("AmbientLight", [{ 5 | t: "l", // l for local. The other option is g for global. 6 | x: 1500, // horizontal positioning 7 | y: 1150, // vertical positioning 8 | rotation: 0, // the beam direction of the light in degrees (if its angle is less than 360 degrees.) 9 | config: { 10 | dim: 20.50, // the total radius of the light, including where it is dim. 11 | bright: 19.00, // the bright radius of the light 12 | angle: 360, // the coverage of the light. (Try 30 for a "spotlight" effect.) 13 | 14 | // Oddly, degrees are counted from the 6 o'clock position. 15 | color: "#0080FF", // Light coloring. 16 | alpha: 0.5 17 | } // Light opacity (or "brightness," depending on how you think about it.) 18 | }]); 19 | -------------------------------------------------------------------------------- /misc/delete_all_templates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Deletes all templates on the current scene 3 | */ 4 | 5 | // no dialog. Just delete all templates. 6 | canvas.scene.deleteEmbeddedDocuments("MeasuredTemplate", canvas.templates.placeables.map(o =>o.id)); 7 | 8 | // Get a dialog confirmation before deleting all templates on the scene: 9 | // canvas.templates.deleteAll() 10 | -------------------------------------------------------------------------------- /misc/delete_items_not_in_folders.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clears the actor Documents of any entries not in a folder. 3 | * Change 'actors' in game.actors.filter to items, macros, tables, journal etc. to get the entry not in a folder. 4 | * Change 'Actor' to another game Document such as RollTable, Item, Macro, JournalEntry, etc... to delete the correct Document type. 5 | * Author: Freeze#2689 6 | */ 7 | const deleteIds = game.actors.filter(e => e.folder === null).map(e => e.id); 8 | Actor.deleteDocuments(deleteIds); 9 | -------------------------------------------------------------------------------- /misc/equip_unequip_shield.js: -------------------------------------------------------------------------------- 1 | /** 2 | * D&D5e biased, paths may be wrong in other systems! 3 | * Equips/unequips an item. Make sure you change the variables at the top (as required). 4 | * This script will also error check to make sure items exist and tokens are select. 5 | * Chat and token icon display options can be set as desired. 6 | * Author: Zapgun, Freeze#2689 (fix for v9) 7 | */ 8 | 9 | const itemName = 'Shield'; // <--- Change this to the *exact* item name (capitals count!) 10 | const sendToChat = true; // <--- Change to 'true' or 'false' to display a chat message about equipping 11 | const displayIcon = true; // <--- Change to 'true' or 'false' to display an effect icon when equipped 12 | const effectIconPath = 'icons/svg/shield.svg'; // <--- Add the effect icon you want to appear when equipped 13 | 14 | let toggleResult = false; 15 | 16 | if (!actor) { 17 | ui.notifications.warn('You need to select a token before using this macro!'); 18 | } else { 19 | 20 | const myItem = actor.items.getName(itemName); 21 | if (myItem) { 22 | let item = actor.items.get(myItem.id); 23 | let attr = "data.equipped"; 24 | let equipped = getProperty(item.data, attr); 25 | if (sendToChat) { 26 | if (!equipped) { 27 | chatMessage(actor.name + ' equips their ' + ' ' + itemName + ''); 28 | } else { 29 | chatMessage(actor.name + ' un-equips their ' + ' ' + itemName + ''); 30 | } 31 | } 32 | item.update({ [attr]: !getProperty(item.data, attr) }); 33 | 34 | // mark/unmark character's token with an effect icon when displayToken is true 35 | (async () => { 36 | if (displayIcon) { 37 | toggleResult = await token.toggleEffect(effectIconPath, { active: !equipped }); 38 | // if (toggleResult == equipped) token.toggleEffect(effectIconPath, {active: equipped}); 39 | } 40 | })(); 41 | 42 | } else { 43 | ui.notifications.warn("No item named '" + itemName + "' found on character!"); 44 | } 45 | } 46 | 47 | function chatMessage(messageContent) { 48 | // create the message 49 | if (messageContent !== '') { 50 | let chatData = { 51 | user: game.user.id, 52 | speaker: ChatMessage.getSpeaker(), 53 | content: messageContent, 54 | }; 55 | ChatMessage.create(chatData, {}); 56 | } 57 | } -------------------------------------------------------------------------------- /misc/find_lights_by_color.js: -------------------------------------------------------------------------------- 1 | // Courtesy of @FloRad, Updated by scooper4711, updated for v9 by Freeze#2689, updated for v9.268 by Wim De Cat 2 | // This macro is intended to perform a batch update 3 | // of all lights in the current scene e.g. after 4 | // importing from Unversal Battle Map importer, 5 | // allowing you to set e.g. all wall torches identically. 6 | 7 | // Convert certain color lights to torch 8 | 9 | ;(async () => { 10 | let foundLights = []; 11 | let markingColor = "#ffff00" 12 | let newColor = "#dd0000" 13 | let scene = game.scenes.active; 14 | 15 | canvas.lighting.placeables.forEach(l => { if (l.data.config.color === markingColor && l.scene === scene) foundLights.push(l.id) }) 16 | 17 | const updates = [] 18 | foundLights.forEach(id => { 19 | updates.push({ _id: id, 20 | config: { 21 | color: newColor, 22 | dim: 16, 23 | bright: 8, 24 | animation: { 25 | type: 'torch', 26 | speed: 1, 27 | intensity: 3, 28 | reverse: false 29 | } 30 | } 31 | }); 32 | }) 33 | 34 | await scene.updateEmbeddedDocuments("AmbientLight", updates); 35 | 36 | console.log(foundLights) 37 | })() 38 | 39 | 40 | // Convert all lights to torch not touching the color 41 | 42 | ;(async () => { 43 | let foundLights = []; 44 | let scene = game.scenes.active; 45 | 46 | canvas.lighting.placeables.forEach(l => { if (l.scene === scene) foundLights.push(l.id) }) 47 | 48 | const updates = [] 49 | foundLights.forEach(id => { 50 | updates.push({ _id: id, 51 | config: { 52 | dim: 16, 53 | bright: 8, 54 | animation: { 55 | type: 'pulse', 56 | speed: 1, 57 | intensity: 3, 58 | reverse: false 59 | } 60 | } 61 | }); 62 | }) 63 | 64 | await scene.updateEmbeddedDocuments("AmbientLight", updates); 65 | 66 | console.log(foundLights) 67 | })() 68 | -------------------------------------------------------------------------------- /misc/find_selected_wall_ids.js: -------------------------------------------------------------------------------- 1 | for (const [i, wall] of canvas.walls.controlled.entries()) { 2 | const text = new PIXI.Text(`${i} - ${wall.id}`); 3 | text.anchor.set(0.5); 4 | text.x = (wall.data.c[0] + wall.data.c[2]) / 2; 5 | text.y = (wall.data.c[1] + wall.data.c[3]) / 2; 6 | text.name = wall.id; 7 | text.style = new PIXI.TextStyle({ fill: 0xffffff, dropShadow: true, fontSize: 20 }); 8 | wall.addChild(text); 9 | } 10 | 11 | const wallIds = canvas.walls.controlled.map((x) => x.id); 12 | const wallsToExport = new Set(wallIds); 13 | 14 | new Dialog({ 15 | title: "Selected Wall IDs", 16 | content: ` 17 | ${[...wallIds.entries()].map(([i, x]) => `
${i}: ${x}
`).join("")} 18 |

19 | 20 | 21 |

`, 22 | buttons: { close: { label: "Close" } }, 23 | close() { 24 | for (const wall of canvas.walls.placeables) { 25 | const child = wall.children.find((w) => w.name === wall.id); 26 | if (child) wall.removeChild(child); 27 | } 28 | }, 29 | render(html) { 30 | html.find(`input[type=checkbox]`).on("change", function () { 31 | const $this = $(this); 32 | const index = +$this.attr("data-i"); 33 | if ($this.prop("checked")) wallsToExport.add(wallIds[index]); 34 | else wallsToExport.delete(wallIds[index]); 35 | }); 36 | 37 | html.find(".button-copy-text").on("click", async () => { 38 | const toCopy = JSON.stringify(wallIds.filter((w) => wallsToExport.has(w))); 39 | await navigator.clipboard.writeText(toCopy); 40 | html.find(".span-copy-text").css("display", "unset"); 41 | }); 42 | }, 43 | }).render(true); 44 | -------------------------------------------------------------------------------- /misc/folder_to_rollable_table.js: -------------------------------------------------------------------------------- 1 | // Take the entries of a folder and turn it into a rollable table. 2 | // If the table exists it appends the results, if no table exists the table is created. Change the description to fit your needs. 3 | // Author: @Atropos#3814 edited for v9 by Freeze#2689 4 | 5 | const tableName = "some table name"; // name of table you will be appending, or creating. 6 | const folderName = "some folder"; // name of folder whos Documents you wish to push into a rollable table. 7 | 8 | const folder = game.folders.getName(folderName); 9 | const table = game.tables.getName(tableName); 10 | const results = folder.contents.map((i, count) => { 11 | return { 12 | text: i.name, 13 | type: CONST.TABLE_RESULT_TYPES.DOCUMENT, 14 | collection: folder.type, 15 | resultId: i.id, 16 | img: i.img, 17 | weight: 1, 18 | range: [count + 1, count + 1], 19 | drawn: false 20 | } 21 | }); 22 | if(table){ 23 | await table.createEmbeddedDocuments("TableResult", results); 24 | } 25 | else { 26 | table = await RollTable.createDocuments([{ 27 | name: tableName, 28 | description: "table description", 29 | results: results, 30 | formula: `1d${results.length}` 31 | }]); 32 | } 33 | -------------------------------------------------------------------------------- /misc/format_all_scene_notes.js: -------------------------------------------------------------------------------- 1 | const updates = canvas.notes.placeables.map(n => ({ 2 | _id: n.id, 3 | 4 | /* READ THIS MESSAGE!**format_all_scene_notes.js** 5 | const updates = canvas.notes.placeables.map(n => ({ 6 | _id: n.id, 7 | 8 | /* READ THIS MESSAGE! 9 | Define new note properties 10 | double right-click a map pin for a list of valid fonts, icons, etc. 11 | remove the // from front of the line to allow that setting to be updated. 12 | */ 13 | 14 | //fontFamily: "Signika", 15 | //fontSize: 48, 16 | //icon: "icons/svg/anchor.svg", // replace the name of the icon, for example, the anchor would be "icons/svg/anchor.svg" 17 | //iconSize: 40, 18 | //iconTint: null, // if not white hex code like: "#AB8345" 19 | //textAnchor: CONST.TEXT_ANCHOR_POINTS.CENTER, // textAnchor controls the location of the text in relation to the icon. other options are .BOTTOM, .TOP. LEFT . RIGHT 20 | //textColor: "#FFFFFF", 21 | //x: 2450, // absolute location on the canvas. 22 | //y: 1250, // absolute location on the canvas. 23 | })); 24 | canvas.scene.updateEmbeddedDocuments("Note", updates); 25 | Define new note properties 26 | double right-click a map pin for a list of valid fonts, icons, etc. 27 | remove the // from front of the line to allow that setting to be updated. 28 | */ 29 | 30 | //fontFamily: "Signika", 31 | //fontSize: 48, 32 | //icon: "icons/svg/anchor.svg", // replace the name of the icon, for example, the anchor would be "icons/svg/anchor.svg" 33 | //iconSize: 40, 34 | //iconTint: null, // if not white hex code like: "#AB8345" 35 | //textAnchor: CONST.TEXT_ANCHOR_POINTS.CENTER, // textAnchor controls the location of the text in relation to the icon. other options are .BOTTOM, .TOP. LEFT . RIGHT 36 | //textColor: "#FFFFFF", 37 | //x: 2450, // absolute location on the canvas. 38 | //y: 1250, // absolute location on the canvas. 39 | })); 40 | canvas.scene.updateEmbeddedDocuments("Note", updates); -------------------------------------------------------------------------------- /misc/import_from_compendium.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import all the entries from a compendium into the desired folder. 3 | * @Author: KrishMero#1702 4 | */ 5 | 6 | let packOptions = game.packs.map(pack => ``); 7 | const form = ` 8 |
Folder:
9 | 10 |
11 |
leave blank to create a folder after the compendium name
12 |
13 | 14 |
Compendium:
15 | 18 |
19 | 20 | 24 | `; 25 | 26 | const dialog = new Dialog({ 27 | title: "Import data from compendium", 28 | content: form, 29 | buttons: { 30 | use: { 31 | label: "Apply", 32 | callback: importCompendium 33 | } 34 | } 35 | }).render(true); 36 | 37 | async function importCompendium(html) { 38 | const folderName = html.find(`input#folderName`)[0].value; 39 | const packName = html.find(`select#destinationPack`)[0].value; 40 | const remove = html.find(`input#delete`)[0].checked; 41 | 42 | const pack = game.packs.get(packName); 43 | const doc = pack.documentName; 44 | let folder = folderName ? findFolder(folderName, doc) : await createFolder(pack, doc); 45 | 46 | if (!folder) return ui.notifications.error(`Your world does not have any ${doc} folders named '${folderName}'.`); 47 | console.log(folder.id) 48 | if (remove) removeDataFirst(folder.id, doc); 49 | if (folder) importPack(pack, doc, folder.id) 50 | } 51 | 52 | async function importPack(pack, doc, folderId) { 53 | const docClass = CONFIG[doc].documentClass; 54 | const content = await pack.getDocuments(); 55 | const createData = content.map(c => { 56 | let data = c.toObject(); 57 | data.folder = folderId; 58 | return data; 59 | }); 60 | docClass.createDocuments(createData); 61 | } 62 | 63 | async function removeDataFirst(folderId, doc) { 64 | let type = getDocType(doc); 65 | const removeableData = game[type].filter(t => t.data.folder === folderId); 66 | CONFIG[doc].documentClass.deleteDocuments(removeableData.map(e=>e.id)); 67 | // if (typeof removeableData.delete !== "undefined") { 68 | // removeableData.delete(); 69 | // } else { 70 | // removeableData.map(d => d.delete()); 71 | // } 72 | } 73 | 74 | async function createFolder(pack, type) { 75 | let name = pack.metadata.label; 76 | if(game.folders.getName(name)) return game.folders.getName(name); 77 | let folder = await Folder.createDocuments([{ name, type, parent: null}]); 78 | return folder[0]; 79 | } 80 | 81 | function findFolder(folderName, doc) 82 | { 83 | return game.folders.find(f => f.name === folderName && f.type === doc) 84 | } 85 | 86 | function getDocType(doc) { 87 | switch (doc) { 88 | case 'JournalEntry': return 'journal'; 89 | case 'RollTable': return 'tables'; 90 | default: return doc.toLowerCase() + 's'; 91 | } 92 | } -------------------------------------------------------------------------------- /misc/jukebox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Quickly play from a list of sound effects and set their audio level. (does not loop) 3 | * Author: Rockshow 4 | */ 5 | 6 | let playlist = { 7 | 'twigs breaking': 'audio/twigs_1.mp3', 8 | 'door opening': 'sounds/lock.wav', 9 | }; 10 | 11 | let optionList; 12 | for (let [key, value] of Object.entries(playlist)) { 13 | optionList += ``; 14 | } 15 | 16 | let applyChanges = false; 17 | new Dialog({ 18 | title: `Audio chosing form`, 19 | content: ` 20 |
21 |
22 | 23 | 26 |
27 | 28 |
29 | 30 |
31 | 32 | 1 33 |
34 |
35 |
36 | 45 | `, 46 | buttons: { 47 | no: { 48 | icon: "", 49 | label: `Cancel Changes` 50 | }, 51 | yes: { 52 | icon: "", 53 | label: `Apply Changes`, 54 | callback: () => applyChanges = true 55 | }, 56 | }, 57 | default: "yes", 58 | close: html => { 59 | if (applyChanges) { 60 | let canzone= html.find('[name="idcanzone"]')[0].value || "none"; 61 | let vol1= html.find('[name="vol"]')[0].value || "none"; 62 | AudioHelper.play({src: canzone, volume:vol1, autoplay: true, loop: false}, true); 63 | } 64 | } 65 | }).render(true); 66 | -------------------------------------------------------------------------------- /misc/lock_all_doors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Locks all closed doors on the canvas 3 | * Author: orcnog 4 | */ 5 | 6 | await canvas.walls.updateAll(w => ({ds: w.data.ds === CONST.WALL_DOOR_STATES.CLOSED ? CONST.WALL_DOOR_STATES.LOCKED : CONST.WALL_DOOR_STATES.CLOSED}), w => w.data.door === CONST.WALL_DOOR_TYPES.DOOR && (w.data.ds === CONST.WALL_DOOR_STATES.LOCKED || w.data.ds === CONST.WALL_DOOR_STATES.CLOSED)); -------------------------------------------------------------------------------- /misc/lock_and_unlock_players.js: -------------------------------------------------------------------------------- 1 | async function updateRoles(fromRole, toRole) { 2 | const updates = game.users.filter(u => u.role === fromRole).map(u => ({_id: u.id, role: toRole})) 3 | await User.updateDocuments(updates) 4 | } 5 | new Dialog({ 6 | title: `Lock or unlock all players?`, 7 | default: 'cancel', 8 | buttons: { 9 | unlock: { 10 | icon: '', 11 | label: 'Unlock', 12 | callback: () => updateRoles(CONST.USER_ROLES.NONE, CONST.USER_ROLES.PLAYER) 13 | }, 14 | lock: { 15 | icon: '', 16 | label: 'Lock', 17 | callback: () => updateRoles(CONST.USER_ROLES.PLAYER, CONST.USER_ROLES.NONE) 18 | }, 19 | cancel: { 20 | icon: '', 21 | label: 'Cancel', 22 | callback: () => {} 23 | } 24 | } 25 | }).render(true) -------------------------------------------------------------------------------- /misc/log_troubleshooting_msg_to_console.js: -------------------------------------------------------------------------------- 1 | // Courtesy of @fohswe 2 | // Logs a troubleshooting message to the browser console. 3 | // (You can usually view the browser console using F12.) 4 | 5 | const gl = canvas.app.renderer.context.gl; 6 | 7 | console.log( 8 | `=== Start of troubleshooting === 9 | Background image: ${canvas.dimensions.width}x${canvas.dimensions.height} 10 | Number of walls: ${canvas.scene.walls.size} 11 | Number of selected vision sources: ${canvas.sight.sources.size} 12 | Number of light sources: ${canvas.lighting.sources.size} 13 | WebGL MAX_TEXTURE_SIZE: ${gl.getParameter(gl.MAX_TEXTURE_SIZE)} 14 | ` 15 | ); -------------------------------------------------------------------------------- /misc/max_npc_hp.js: -------------------------------------------------------------------------------- 1 | // A simple macro for maximizing NPC HP 2 | 3 | // Choose one of the following to update by uncommenting one of the two lines below 4 | //const actors = game.actors; // update all actors in the sidebar 5 | const actors = canvas.tokens.controlled.map((t) => t.actor); // update all selected tokens 6 | 7 | actors 8 | .filter((actor) => actor.type === "npc") 9 | .forEach(async (actor) => { 10 | const formula = actor.data.data?.attributes?.hp?.formula; 11 | if (!formula) return; 12 | 13 | const roll = await new Roll(formula).roll({ maximize: true }); 14 | const data = { 15 | "data.attributes.hp.value": roll.total, 16 | "data.attributes.hp.max": roll.total, 17 | }; 18 | await actor.update(data); 19 | }); 20 | -------------------------------------------------------------------------------- /misc/move_walls.js: -------------------------------------------------------------------------------- 1 | /* From: @(Busy) Gen Kitty (she/her) 2 | To move each node on both axes, you need all 4 parameters listed. 3 | In this case, he wanted to move all the walls up and to the left and 4 | the foundry grid is sorta vertically flipped to what you'd expect, 5 | which is why all of the operators are "-=" If you wanted to move them 6 | in different directions it'd just be a matter of changing the operator 7 | next to the equals sign. 8 | 9 | Each argument is a node's X or Y position, and each wall segment has two nodes. 10 | 0 = Node 1 X 11 | 1 = Node 1 Y 12 | 2 = Node 2 X 13 | 3 = Node 2 Y 14 | */ 15 | 16 | let walls = canvas.scene.data.walls.map(w => { 17 | w = duplicate(w); 18 | w.c[0] -= 50; 19 | w.c[1] -= 50; 20 | w.c[2] -= 50; 21 | w.c[3] -= 50; 22 | return w; 23 | }); 24 | canvas.scene.update({walls: walls}); 25 | -------------------------------------------------------------------------------- /misc/polygon_drawing_to_walls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts selected polygon drawing to a wall 3 | * @Author: cole#9640 4 | */ 5 | 6 | let drawings = canvas.drawings.controlled; 7 | 8 | drawings = drawings.filter(drawing => { 9 | if (!drawing.isPolygon) { 10 | ui.notifications.warn(`Drawing "${drawing.document._id}" is not a polygon, skipping`); 11 | return false; 12 | } 13 | return true; 14 | }); 15 | 16 | if (drawings.length) { 17 | const newWalls = drawings.flatMap((drawing) => { 18 | const { x, y } = drawing.document; 19 | const { width, height } = drawing.document.shape; 20 | const xCenterOffset = width / 2; 21 | const yCenterOffset = height / 2; 22 | 23 | const o = Math.toRadians(drawing.document.rotation); 24 | const coso = Math.cos(o); 25 | const sino = Math.sin(o); 26 | 27 | pts = drawing.document.shape.points; 28 | points = []; 29 | for (i = 0; i < pts.length - 1; i += 2) { 30 | const offsetX = pts[i] - xCenterOffset; 31 | const offsetY = pts[i + 1] - yCenterOffset; 32 | const rotatedX = (offsetX * coso - offsetY * sino); 33 | const rotatedY = (offsetY * coso + offsetX * sino); 34 | points.push([rotatedX + x + xCenterOffset, rotatedY + y + yCenterOffset]); 35 | } 36 | return points.slice(0, points.length - 1) 37 | .map((point, i) => { 38 | return { c: point.concat(points[i + 1]) }; 39 | }); 40 | }); 41 | 42 | canvas.scene.createEmbeddedDocuments("Wall", newWalls).then(as => { canvas.walls.activate(); }); 43 | 44 | } else { 45 | ui.notifications.error("No polygon drawings selected!"); 46 | } 47 | -------------------------------------------------------------------------------- /misc/rebind_token_actors.js: -------------------------------------------------------------------------------- 1 | // Rebinds the actor for all selected tokens. 2 | // Requirements: 3 | // - the actor must be present in the "Actor Directory" 4 | // - actor name can't contain either '/' or '.' 5 | 6 | // Any Token with an altered name and an img path attached 7 | // will look up the default actor name via provided URL. 8 | let tname, results, str, arr; 9 | 10 | const dir = new ActorDirectory(); 11 | 12 | for (const token of canvas.tokens.controlled) { 13 | tname = token.name; 14 | results = dir.documents.filter(obj => {if(obj.data.name === tname){return obj;}}) 15 | if(results.length === 0){ 16 | if(token.data.img){ 17 | // Possible optimization: regEx look-up for any word character pre '.' and post '/' 18 | str = token.data.img; 19 | arr = str.split('/'); 20 | tname = arr[arr.length-1].split('.')[0]; 21 | results = dir.documents.filter(obj => {if(obj.data.name === tname){return obj;}}) 22 | } 23 | } 24 | if(results.length > 0){ 25 | await token.update({'actorId':results[0].data._id}); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /misc/scale_grid_size_to_inches.js: -------------------------------------------------------------------------------- 1 | /** 2 | Author: @stan#1549 (github.com/janssen-io) 3 | 4 | Description: 5 | The first time you click the macro, it will prompt you for your screen size (diagonal). 6 | Clicking the macro a second time will automatically scale to 1"/grid unit (square/hex). 7 | Shift-click the macro to update this value. 8 | 9 | Note: 10 | The screen size is saved per client. So opening the macro on another device for the first time, will make it prompt for the screen size again. 11 | */ 12 | 13 | function showDialog(inches, resolve) { 14 | new Dialog({ 15 | content: `Screen size (inches): `, 16 | default: 'scale', 17 | buttons: { 18 | scale: { 19 | label: 'Scale', 20 | callback: html => resolve(+html.find('input').val()) 21 | } 22 | } 23 | }).render(true); 24 | } 25 | 26 | function scale(screenSizeInches) { 27 | const diagonal = Math.sqrt(screen.width ** 2 + screen.height ** 2); 28 | const ppi = diagonal / screenSizeInches; 29 | console.log(`PPI: ${screenSizeInches}" screen | ${canvas.grid.size}px per grid unit | ${ppi}px per inch | Scaling to: ${ppi / canvas.grid.size}`); 30 | canvas.animatePan({ scale: ppi / canvas.grid.size }); 31 | } 32 | 33 | const key = 'stan#1549.scale.screenSize'; 34 | const storedScreenSize = localStorage.getItem(key); 35 | const shouldUpdatePPI = !storedScreenSize || event.shiftKey; 36 | 37 | const getScreenSize = new Promise(resolve => { 38 | if (shouldUpdatePPI) { 39 | showDialog(storedScreenSize || 42, resolve); 40 | } else { 41 | resolve(storedScreenSize); 42 | } 43 | }); 44 | 45 | getScreenSize.then(inches => { 46 | scale(inches); 47 | localStorage.setItem(key, inches); 48 | }); 49 | 50 | 51 | -------------------------------------------------------------------------------- /misc/show_modules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Show Modules - Shows currently installed modules in foundry. Added on behalf of @vance 3 | */ 4 | let mods = ''; 5 | game.modules.forEach(m => { 6 | let a = m.active ? 'Enabled' : 'Disabled'; 7 | mods = mods.concat(`${m.id}: ${a}\n`); 8 | }); 9 | 10 | let d = new Dialog({ 11 | title: `Enabled Mods`, 12 | content: ``, 13 | buttons: { 14 | copy: { 15 | label: `Copy to clipboard`, 16 | callback: () => { 17 | $("#modslist").select(); 18 | document.execCommand('copy'); 19 | } 20 | }, 21 | close: { 22 | icon: "", 23 | label: `Close` 24 | }, 25 | }, 26 | default: "close", 27 | close: () => {} 28 | }); 29 | 30 | d.render(true); -------------------------------------------------------------------------------- /misc/tile_toggle_hidden_status.js: -------------------------------------------------------------------------------- 1 | // Simple macro to loop through ALL SELECTED TILES and toggle whether or not they are hidden. 2 | // Uncomment line 8 or 9 to change behavior to hide / show all tiles instead of toggle 3 | const tiles = canvas.background.controlled.length ? canvas.background.controlled : canvas.foreground.controlled; 4 | const updates = tiles.map(tile => { 5 | let v; 6 | v = !tile.data.hidden; // Toggle visibility for each tile 7 | // v = false; // Hide all selected tiles 8 | // v = true; // Show all selected tiles 9 | return{ _id: tile.id, hidden: v }; 10 | }); 11 | canvas.scene.updateEmbeddedDocuments("Tile", updates); -------------------------------------------------------------------------------- /misc/tile_toggle_locked_status.js: -------------------------------------------------------------------------------- 1 | //Simple macro to loop through ALL SELECTED TILES and toggle their locked status. 2 | //In other words: 3 | //If an individual tile is unlocked, this macro will lock it. 4 | //If an individual tile is locked, this macro will unlock it. 5 | 6 | const tiles = canvas.background.controlled.length ? canvas.background.controlled : canvas.foreground.controlled; 7 | const updates = tiles.map(tile => ({ _id: tile.id, locked: !tile.data.locked })); 8 | canvas.scene.updateEmbeddedDocuments("Tile", updates); -------------------------------------------------------------------------------- /misc/tile_xy_adjust.js: -------------------------------------------------------------------------------- 1 | //Simple macro to loop through ALL SELECTED TILES and adjust their position by a set amount 2 | //Questions? Ask in Foundry VTT Discord #macro-polo channel. If absolutely needed, ping @Norc$5108 3 | 4 | async function adjustTilesXY(tiles, xAdjust, yAdjust ) { 5 | const updates = tiles.map(tile => ({ 6 | _id: tile.id, 7 | x: tile.x + xAdjust, 8 | y: tile.y + yAdjust, 9 | })); 10 | await canvas.scene.updateEmbeddedDocuments("Tile", updates) 11 | } 12 | 13 | const tiles = canvas.background.controlled.length ? canvas.background.controlled : canvas.foreground.controlled; 14 | if(!tiles.length) return ui.notifications.info("No tiles selected.") 15 | //loop through all selected tiles 16 | //REPLACE THE "1" VALUES BELOW AS NEEDED 17 | //The first number controls side-to-side position: 18 | //Positive values move tiles to the right 19 | //Negative values move tiles to the left 20 | //If you enter 0, tiles will not move side to side at all. 21 | //The second number controls up-and-down position: 22 | //Positive values move tiles down 23 | //Negative values move tiles up 24 | //If you enter 0, tiles will not move up or down at all. 25 | await adjustTilesXY(tiles, 1, 1); -------------------------------------------------------------------------------- /misc/toggle_playlist.js: -------------------------------------------------------------------------------- 1 | // Get all playlists from contents and prepare choices 2 | 3 | let optionsText = game.playlists.reduce((acc, e) => acc += ``, ``); 4 | let _applyChanges = false; 5 | 6 | let d = new Dialog({ 7 | title: "Playlist Toggle", 8 | content: ` 9 |
10 |
11 | 12 | 13 |
14 |
15 | `, 16 | buttons: { 17 | one: { 18 | icon: '', 19 | label: "Playlist Toggle", 20 | callback: () => _applyChanges = true 21 | }, 22 | two: { 23 | icon: '', 24 | label: "Cancel", 25 | callback: () => _applyChanges = false 26 | } 27 | }, 28 | default: "Cancel", 29 | close: html => { 30 | if (_applyChanges) { 31 | let _plId = html.find('[name=playlist-selection]')[0].value; 32 | let _pl = game.playlists.get(_plId); 33 | if(_pl) { 34 | if (_pl.playing) { 35 | // turn off 36 | _pl.stopAll(); 37 | } else { 38 | // turn on 39 | _pl.playAll(); 40 | } 41 | } 42 | else { 43 | ui.notifications.error(`No valid playlist selected.`); 44 | } 45 | } 46 | } 47 | }).render(true); -------------------------------------------------------------------------------- /misc/whisper_players.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides a dialog to whisper specific players. If you have tokens selected, it will automatically default to try and whisper those players. 3 | * @Author: Nelson#3570 4 | */ 5 | 6 | let applyChanges = false; 7 | 8 | let users = game.users.filter(user => user.active); 9 | let checkOptions = "" 10 | let playerTokenIds = users.map(u => u.character?.id).filter(id => id !== undefined); 11 | let selectedPlayerIds = canvas.tokens.controlled.map(token => { 12 | if (playerTokenIds.includes(token.actor.id)) return token.actor.id; 13 | }); 14 | 15 | // Build checkbox list for all active players 16 | users.forEach(user => { 17 | let checked = !!user.character && selectedPlayerIds.includes(user.character.id) && 'checked'; 18 | checkOptions+=` 19 |
20 | \n 21 | 22 | ` 23 | }); 24 | 25 | new Dialog({ 26 | title:"Whisper", 27 | content:`Whisper To: ${checkOptions}
28 | 29 |
`, 30 | buttons:{ 31 | whisper:{ 32 | label:"Whisper", 33 | callback: (html) => createMessage(html) 34 | } 35 | } 36 | }).render(true); 37 | 38 | function createMessage(html) { 39 | var targets = []; 40 | // build list of selected players ids for whispers target 41 | for ( let user of users ) { 42 | if (html.find('[name="'+user.id+'"]')[0].checked){ 43 | applyChanges=true; 44 | targets.push(user.id); 45 | } 46 | var messageText = html.find('[name="message"]')[0].value 47 | } 48 | if(!applyChanges)return; 49 | ChatMessage.create({ 50 | content: messageText, 51 | whisper: targets 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /module-specific/readme.md: -------------------------------------------------------------------------------- 1 | module-specific folder -------------------------------------------------------------------------------- /packs/macros-age.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundry-vtt-community/macros/f6bf3ea2e42b8d0510d313bee8cecdc80a68fb2e/packs/macros-age.db -------------------------------------------------------------------------------- /packs/macros-dsa5e.db: -------------------------------------------------------------------------------- 1 | {"name":"3d20 Skill Roll","type":"script","scope":"global","author":"W4eVeWm2pD6oM5bE","img":"icons/svg/dice-target.svg","command":"//Create a roll term with flavor text and execute the roll. Wait for the result and make the roll async.\n// Note: Instead of \"evaluate\" --> \"roll\" can be used they seem identical \nlet dies = await new Roll(\"1d20[black]+1d20[red]+1d20[yellow]\").evaluate({async: true});\n\n// Create a html based Chat message which will be outputed.\nlet ChatData={\n// let the roll take part as a roll of the actor. If speaker ist deleted then the roll takes place as the player.\nspeaker: ChatMessage.getSpeaker({token: actor}),\n// This was asked for by the Dice so Nice API. Currently not clear what it exactly does\ntype: CONST.CHAT_MESSAGE_TYPES.ROLL,\n// Rolls a pool of dice/diceterms. If more than one diceterm/dices are rolled then an array of objects needs to be defined --> [r1,r2,r3]\nrolls: [dies],\n// This was asked for by the Dice so Nice API. Currently not clear what it exactly does\nrollMode: game.settings.get(\"core\", \"rollMode\"),\n// Create HTML template for Chat message output. Each Dice in the dice term is an object in an array thus we need to adress it with dies.dice[x]. Remember: arrays start with an index of 0 in java script\ncontent:`\n
\n
\n
\n
\n
\n
    \n
  1. ${dies.dice[0].results[0].result}
  2. \n
  3. ${dies.dice[1].results[0].result}
  4. \n
  5. ${dies.dice[2].results[0].result}
  6. \n
\n\n
\n
\n
\n

${dies.dice[0].results[0].result} / ${dies.dice[1].results[0].result} / ${dies.dice[2].results[0].result}

\n
\n
`\n}\n// Create the Chat message\nChatMessage.create(ChatData);","ownership":{"default":0,"W4eVeWm2pD6oM5bE":3},"flags":{"core":{"sourceId":"Macro.uGLqwKSrw3SEiMaJ"}},"_stats":{"systemId":"dsa5","systemVersion":"4.1.20","coreVersion":"10.291","createdTime":1670488013537,"modifiedTime":1670488629780,"lastModifiedBy":"W4eVeWm2pD6oM5bE"},"folder":null,"sort":0,"_id":"krzN4mLTVo9LPJBh"} 2 | -------------------------------------------------------------------------------- /packs/macros-wfrp4e.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foundry-vtt-community/macros/f6bf3ea2e42b8d0510d313bee8cecdc80a68fb2e/packs/macros-wfrp4e.db -------------------------------------------------------------------------------- /pf1e/readme.md: -------------------------------------------------------------------------------- 1 | pf1e folder -------------------------------------------------------------------------------- /pf2e/change_initiative_skill.js: -------------------------------------------------------------------------------- 1 | // changes the default initiative skill for the selected tokens. Will even change monsters/NPCs even though the UI does not support this. 2 | // Useful when staging an ambush for example. 3 | // If the creature has an inherent bonus to initiative on top of their skill this will not be accounted for. 4 | 5 | async function setInitSkill(skillname) 6 | { 7 | canvas.tokens.controlled.forEach(async function(changetoken){ 8 | if(changetoken.actor.data.type!="character"){ 9 | let skillval=skillname=="perception"? changetoken.actor.data.data.attributes.perception.totalModifier : changetoken.actor.data.data.skills[skillname].totalModifier; 10 | await changetoken.actor.update({ 11 | 'data.attributes.initiative.ability':skillname, 12 | 'data.attributes.initiative.totalModifier':skillval 13 | }); 14 | }else{ 15 | await changetoken.actor.update({ 16 | 'data.attributes.initiative.ability':skillname 17 | }); 18 | } 19 | }); 20 | } 21 | 22 | let applyChanges=false; 23 | new Dialog({ 24 | title: `Set Initiative Skill`, 25 | content: ` 26 |
27 |
28 | 47 |
48 |
49 | `, 50 | buttons: { 51 | yes: { 52 | icon: "", 53 | label: `Apply Changes`, 54 | callback: () => applyChanges = true 55 | }, 56 | no: { 57 | icon: "", 58 | label: `Cancel Changes` 59 | }, 60 | }, 61 | default: "yes", 62 | close: html => { 63 | if (applyChanges) { 64 | let skillchoice = html.find('[name="init-skill"]')[0].value || "perception"; 65 | setInitSkill(skillchoice); 66 | } 67 | } 68 | }).render(true); 69 | -------------------------------------------------------------------------------- /pf2e/fall_damage.js: -------------------------------------------------------------------------------- 1 | When you fall more than 5 feet, you take bludgeoning damage equal to half the distance you fell when you land. If you take any damage from a fall, you land prone. 2 | 3 | ie. 10 ft. is [[/roll 5]] Bludgeoning Damage 4 | 5 | Falling on a Creature: 6 | 7 | If you land on a creature, that creature must attempt a DC 15 Reflex Save. 8 | 9 | Critical Success: The creature takes no damage. 10 | 11 | Success: The creature takes bludgeoning damage equal to one-quarter the falling damage you took. 12 | 13 | Failure: The creature takes bludgeoning damage equal to half the falling damage you took. 14 | 15 | Critical Failure: The creature takes the same amount of bludgeoning damage you took from the fall. -------------------------------------------------------------------------------- /roll/average_roll_results.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reads the chat log for a specific type of die rolled and returns the average of that die type. If no input is provided, it will check all d20 rolls. 3 | */ 4 | 5 | let dialogue = new Dialog({ 6 | title: `Dice rolls to check`, 7 | content: `

Enter the type of dice to check:

`, 8 | buttons: { 9 | one: { 10 | icon: '', 11 | label: 'Submit', 12 | callback: (html) => { 13 | const input = html.find('#diceFacesToCheck').val(); 14 | const diceToCheck = input ? parseInt(input) : 20; 15 | const chatLog = game.messages; 16 | let rolls = 0; 17 | let total = 0; 18 | 19 | chatLog.forEach(entry => { 20 | if (entry.roll) { 21 | const { terms } = entry.roll; 22 | terms 23 | .filter(die => die.faces === diceToCheck) 24 | .forEach(die => { 25 | rolls = rolls + die.number; 26 | total = total + die.total; 27 | }) 28 | } 29 | }); 30 | 31 | console.log(rolls, total); 32 | 33 | let dialogue = new Dialog({ 34 | title: `Average d${diceToCheck} rolls`, 35 | content: `

Amount of d${diceToCheck}'s checked: ${rolls}

Average result: ${Math.round(((total / rolls) + Number.EPSILON) * 100) / 100}

`, 36 | buttons: { 37 | one: { 38 | icon: '', 39 | label: 'Close' 40 | } 41 | } 42 | }) 43 | 44 | dialogue.render(true) 45 | } 46 | } 47 | } 48 | }) 49 | 50 | dialogue.render(true) -------------------------------------------------------------------------------- /roll/character_stat_roller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates character stats and outputs the table result. 3 | * Author: @Kekilla#7036 & KrishMero1792 4 | */ 5 | 6 | // Formula for rolling 7 | const statString = '4d6kh3'; 8 | 9 | // times to roll those stats 10 | const numRolls = 6; 11 | 12 | 13 | ////////////////////////////////////////// 14 | // Don't touch anything below this line // 15 | ////////////////////////////////////////// 16 | const stats = Array(numRolls).fill(0).map(e=>new Roll(statString).evaluate({async: false})); 17 | 18 | const rollData = stats[0].dice[0]; 19 | const {faces, values: keptRolls, results: rolls} = rollData; 20 | const totalAverage = (faces/2 + 1) * keptRolls.length; 21 | const totalDeviation = faces/2; 22 | const totalLow = Math.ceil(totalAverage - totalDeviation); 23 | const totalHigh = Math.ceil(totalAverage + totalDeviation); 24 | 25 | const header = rolls.map((roll, index) => `D${index + 1}`).join(''); 26 | 27 | let tableRows = ''; 28 | let finalSum = 0; 29 | for(let {terms, total} of stats) { 30 | tableRows += ``; 31 | tableRows += terms[0].results.map(({result, discarded}) => `${result}`).join(''); 32 | tableRows += `${total}`; 33 | finalSum += total; 34 | } 35 | 36 | const colspan = `colspan="${rolls.length + 1}"`; 37 | const center = `text-align:center;`; 38 | 39 | let content = ` 40 | 41 | 42 | 44 | 45 | 46 | ${header} 47 | 48 | 49 | ${tableRows} 50 | 51 | 52 | 53 | 54 |

New Ability Scores

43 |
${statString} was rolled ${numRolls} times.
Total
Final Sum:${finalSum}
55 | `; 56 | 57 | 58 | ChatMessage.create({content}); 59 | 60 | function colorSetter(number,low,high, discarded) 61 | { 62 | if(discarded === true) return 'text-decoration:line-through;color:gray'; 63 | if(number <= low) return 'color:red'; 64 | if(number >= high) return 'color:green'; 65 | return ''; 66 | } 67 | -------------------------------------------------------------------------------- /roll/chartopia_roller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Make a roll from chartopia and output the results in the chat window. 3 | * If you find yourself using this macro often, please support chartopia on patreon. 4 | */ 5 | 6 | // chart id from url. IE 19449 is the chart id in https://chartopia.d12dev.com/chart/19449/ 7 | const chartId = 61778; 8 | // only let the gm see the results. false = everyone sees in chat. true = gm whispered results. 9 | const gmOnly = false; 10 | 11 | 12 | ////////////////////////////////// 13 | /// Don't edit past this point /// 14 | ////////////////////////////////// 15 | 16 | const requestUrl = `https://chartopia.d12dev.com/api/charts/${chartId}/roll/`; 17 | 18 | 19 | function roll(id) { 20 | let request = new XMLHttpRequest(); 21 | request.open('POST', requestUrl, true); 22 | 23 | request.onload = function() { 24 | if (request.status >= 200 && request.status < 400) { 25 | // Success! 26 | let whisper = !!gmOnly ? game.users.map(u => { 27 | if (u.isGM) return u.id; 28 | }) : []; 29 | console.log('whisper', whisper); 30 | // Create chat content. 31 | const response = JSON.parse(request.response); 32 | let resultsList = ``; 33 | if (Array.isArray(response.results)) { 34 | response.results.forEach(result => { 35 | result = result.replace("**", ""); 36 | result = result.replace("**", ""); 37 | resultsList += `
  • ${result}
  • `; 38 | }); 39 | } 40 | console.log(resultsList); 41 | 42 | let chatContent = `

    Chart
    ${response.chart_name}(${response.chart_id})

    ` + 43 | `

    URL
    ${response.chart_url}

    ` + 44 | `

    Results

    ` + 45 | `
      ${resultsList}
    `; 46 | let chatData = { 47 | user: game.userId, 48 | speaker: ChatMessage.getSpeaker(), 49 | content: chatContent, 50 | whisper 51 | }; 52 | 53 | ChatMessage.create(chatData, {}); 54 | } else { 55 | // We reached our target server, but it returned an error 56 | console.log("Server error."); 57 | } 58 | }; 59 | 60 | request.onerror = function() { 61 | // There was a connection error of some sort 62 | console.log("Error getting result."); 63 | }; 64 | 65 | request.send(); 66 | } 67 | 68 | roll(chartId); 69 | -------------------------------------------------------------------------------- /roll/roll_die.js: -------------------------------------------------------------------------------- 1 | //Script to roll a die 2 | //Equivalently could be done by changing the macro to chat and entering /r 1d6 3 | //You can roll any number and any type of dice by changing Roll to ndx 4 | //n: number of dice 5 | //x: value (number of sides) on die 6 | const roll = new Roll(`1d6`); 7 | await roll.toMessage({ 8 | flavor: "Sneak Attack Damage", 9 | }); 10 | -------------------------------------------------------------------------------- /roll/roll_initiatives.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Takes all selected tokens and adds them to the combat tracker. Then rolls initative for all NPC tokens. 3 | */ 4 | async function main() { 5 | await canvas.tokens.toggleCombat(); 6 | game.combat.rollNPC({ messageOptions: { rollMode: CONST.DICE_ROLL_MODES.PRIVATE }}) 7 | } 8 | main(); 9 | -------------------------------------------------------------------------------- /roll/roll_table.js: -------------------------------------------------------------------------------- 1 | // Simple macro example to only roll from a table and whisper the result to the DM 2 | 3 | (async () => { 4 | const table = game.tables.find(t => t.name === "name of your table"); 5 | let roll = await table.roll(); 6 | 7 | let chatData = { 8 | user: game.user._id, 9 | speaker: ChatMessage.getSpeaker(), 10 | content: roll.results[0].data.text, 11 | whisper: game.users.filter(u => u.isGM).map(u => u._id) 12 | }; 13 | ChatMessage.create(chatData, {}); 14 | })(); 15 | -------------------------------------------------------------------------------- /roll/wandering_monsters.js: -------------------------------------------------------------------------------- 1 | // setting variables 2 | const tableName = "Wandering Monsters"; 3 | const msgContent = "Wandering Monster roll was: "; 4 | 5 | // roll to check for wandering monster 6 | const result = (await new Roll(`1d20`).roll()).total; 7 | 8 | // create the message 9 | const chatData = { 10 | content: msgContent + result, 11 | whisper: game.users.filter(u => u.isGM).map((u) => u.id), 12 | }; 13 | ChatMessage.create(chatData); 14 | 15 | // In this example, a roll between 17-20 will generate a roll from the Table. Tweak as needed! 16 | if (result >= 17) { 17 | const table = game.tables.getName(tableName); 18 | table.draw(); 19 | } 20 | -------------------------------------------------------------------------------- /token/actor_selector.js: -------------------------------------------------------------------------------- 1 | // Credit for helping me with this macro goes to @cole & @Kandashi on the Foundry Discord 2 | 3 | //Selects all actors of the same name on the scene. 4 | // eg: if you have a group of goblins mixed in with kobolds and you want to move all goblins 5 | // select 1 goblin and run this macro 6 | // all Goblins will now be selected 7 | // this should still work in conjunction with Token Mold 8 | let selectedId = canvas.tokens.controlled[0].document.actor.id 9 | 10 | canvas.tokens.ownedTokens.forEach(i => { 11 | if(i.data.actorId == selectedId) { 12 | i.control({releaseOthers: false}) 13 | } 14 | }) -------------------------------------------------------------------------------- /token/apply_prototype.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Apply Prototype Token to all placed tokens of that actor. 3 | * 4 | * @author Forien#2130 5 | * @url https://www.patreon.com/foundryworkshop 6 | * @licence MIT 7 | */ 8 | 9 | // Optionally modify it to select specific actor. 10 | // By default takes actor of selected token, or if none is selected, then linked character 11 | // let actor = ...; 12 | 13 | 14 | if (!actor) return; 15 | let tokens = actor.getActiveTokens(); 16 | 17 | let updates = tokens.map(t => { 18 | let token = duplicate(t.data); 19 | return mergeObject(token, actor.data.token); 20 | }); 21 | 22 | if (updates.length) 23 | canvas.scene.updateEmbeddedDocuments("Token", updates); -------------------------------------------------------------------------------- /token/bulk_change_initiative.js: -------------------------------------------------------------------------------- 1 | //for the selected tokens, adjust their initiative by X. Use with selective-select to modify all enemies, friendlies 2 | 3 | let applyChanges = false; 4 | 5 | new Dialog({ 6 | title: `Bulk change initiative`, 7 | content: ` 8 |
    9 |
    10 | 11 | 12 |
    13 |
    14 | `, 15 | buttons: { 16 | yes: { 17 | icon: "", 18 | label: `Apply Changes`, 19 | callback: () => applyChanges = true 20 | }, 21 | no: { 22 | icon: "", 23 | label: `Cancel Changes` 24 | }, 25 | }, 26 | default: "yes", 27 | close: html => { 28 | if (applyChanges) { 29 | if(!game.combat) return; 30 | const initadjust = parseInt(html.find('[name="init-adjust"]')[0].value || "0"); 31 | const updates = canvas.tokens.controlled.reduce((acc, t) => { 32 | if(!t.combatant) return acc; 33 | acc.push({_id: t.combatant.id, initiative: initadjust}); 34 | return acc; 35 | },[]); 36 | game.combat.updateEmbeddedDocuments("Combatant", updates) 37 | } 38 | } 39 | }).render(true); -------------------------------------------------------------------------------- /token/change_disposition.js: -------------------------------------------------------------------------------- 1 | let applyChanges = false; 2 | new Dialog({ 3 | title: `Token Disposition Changer`, 4 | content: ` 5 |
    6 |
    7 | 8 | 14 |
    15 |
    16 | `, 17 | buttons: { 18 | yes: { 19 | icon: "", 20 | label: `Apply Changes`, 21 | callback: () => applyChanges = true 22 | }, 23 | no: { 24 | icon: "", 25 | label: `Cancel Changes` 26 | }, 27 | }, 28 | default: "yes", 29 | close: html => { 30 | if (applyChanges) { 31 | const dispoType = html.find('[name="dispo-type"]')[0].value.toUpperCase(); 32 | if(dispoType === "NOCHANGE") return; 33 | const updates = canvas.tokens.controlled.map(t => ({_id: t.id, disposition: CONST.TOKEN_DISPOSITIONS[dispoType]})); 34 | canvas.scene.updateEmbeddedDocuments("Token", updates) 35 | } 36 | } 37 | }).render(true); -------------------------------------------------------------------------------- /token/clone_token_actor.js: -------------------------------------------------------------------------------- 1 | // Clones actor details from the selected token(s) into a new Actor in the item list. 2 | // Useful if you made changes to the actor associated with the token, but want to save that 3 | // updated Actor for later use or into a Compendium. 4 | // Created Actor will default to the name of the token with the actorNameSuffix (default: '_cloned') 5 | 6 | // WORKS ONLY FOR LINKED ACTORS 7 | 8 | const actorNameSuffix = "_cloned"; 9 | 10 | canvas.tokens.controlled.forEach(o => { 11 | Actor.create(o.actor).then(a => { 12 | a.update({name: a.name + actorNameSuffix}); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /token/end_turn.js: -------------------------------------------------------------------------------- 1 | /* 2 | Author: willisrocks 3 | Description: 4 | 5 | Ends the current actors turn in a combat encounter. Useful when you don't pop out your combat tracker 6 | and want to end the turn from your hotbar. 7 | 8 | If the user is a gamemaster, it will always end the current turn. For players, it will only end 9 | the turn when the current actor in the turn order is owned by you. 10 | 11 | Based on the work of reddit user serrag97: https://www.reddit.com/r/FoundryVTT/comments/j1b8gs/next_turn_shortcut/ 12 | */ 13 | 14 | // check if the user is a GM 15 | const isGM = game.user.isGM; 16 | // check if the user owns the combatant whose turn it is 17 | const isOwner = game.combat.combatant.isOwner; 18 | 19 | if (isGM || isOwner) { 20 | game.combat.nextTurn(); 21 | } else { 22 | ui.notifications.info("As a player you can only advance your turn"); 23 | } 24 | -------------------------------------------------------------------------------- /token/light_picker.js: -------------------------------------------------------------------------------- 1 | function tokenUpdate(data) { 2 | canvas.tokens.controlled.map(token => token.document.update({light: data})); 3 | } 4 | 5 | let torchAnimation = {"type": "torch", "speed": 1, "intensity": 1, "reverse": false}; 6 | 7 | let dialogEditor = new Dialog({ 8 | title: `Token Light Picker`, 9 | content: `Pick the light source the selected token is holding.`, 10 | buttons: { 11 | none: { 12 | label: `None`, 13 | callback: () => { 14 | tokenUpdate({"dim": 0, "bright": 0, "angle": 360, "luminosity": 0.5}); 15 | dialogEditor.render(true); 16 | } 17 | }, 18 | torch: { 19 | label: `Torch`, 20 | callback: () => { 21 | tokenUpdate({"dim": 40, "bright": 20, "angle": 360, "luminosity": 0.5, "animation": torchAnimation}); 22 | dialogEditor.render(true); 23 | } 24 | }, 25 | light: { 26 | label: `Light cantrip`, 27 | callback: () => { 28 | tokenUpdate({"dim": 40, "bright": 20, "angle": 360, "luminosity": 0.5, "animation": {"type": "none"}}); 29 | dialogEditor.render(true); 30 | } 31 | }, 32 | lamp: { 33 | label: `Lamp`, 34 | callback: () => { 35 | tokenUpdate({"dim": 45, "bright": 15, "angle": 360, "luminosity": 0.5, "animation": torchAnimation}); 36 | dialogEditor.render(true); 37 | } 38 | }, 39 | bullseye: { 40 | label: `Bullseye Lantern`, 41 | callback: () => { 42 | tokenUpdate({"dim": 120, "bright": 60, "angle": 45, "luminosity": 0.5, "animation": torchAnimation}); 43 | dialogEditor.render(true); 44 | } 45 | }, 46 | hoodedOpen: { 47 | label: `Hooded Lantern (Open)`, 48 | callback: () => { 49 | tokenUpdate({"dim": 60, "bright": 30, "angle": 360, "luminosity": 0.5, "animation": torchAnimation}); 50 | dialogEditor.render(true); 51 | } 52 | }, 53 | hoodedClosed: { 54 | label: `Hooded Lantern (Closed)`, 55 | callback: () => { 56 | tokenUpdate({"dim": 5, "bright": 0, "angle": 360, "luminosity": 0.5, "animation": torchAnimation}); 57 | dialogEditor.render(true); 58 | } 59 | }, 60 | darkness: { 61 | label: `Darkness spell`, 62 | callback: () => { 63 | tokenUpdate({"dim": 0, "bright": 15, "angle": 360, "luminosity": -0.5, "animation": {"type": "none"}}); 64 | dialogEditor.render(true); 65 | } 66 | }, 67 | close: { 68 | icon: "", 69 | label: `Close` 70 | }, 71 | }, 72 | default: "close", 73 | close: () => {} 74 | }); 75 | 76 | dialogEditor.render(true) -------------------------------------------------------------------------------- /token/light_picker_color.js: -------------------------------------------------------------------------------- 1 | function tokenUpdate(data) { 2 | canvas.tokens.controlled.map(token => token.document.update({ light: data })); 3 | } 4 | 5 | const isGM = game.user.isGM; 6 | 7 | let color = "#ffffff"; 8 | let alpha = 1.0; 9 | let tokens = canvas.tokens.controlled; 10 | if (tokens.length === 1) { 11 | color = tokens[0].data.light.color ?? color; 12 | alpha = tokens[0].data.light.alpha ?? alpha; 13 | } 14 | 15 | const torchAnimation = {type: "torch", speed: 1, intensity: 1}; 16 | const energyShield = {type: "energy", speed: 1, intensity: 1}; 17 | const lights = { 18 | none: { 19 | label: "None", 20 | data: {dim: null, bright: null, angle: 360} 21 | }, 22 | torch: { 23 | label: "Torch", 24 | data: {dim: 40, bright: 20, angle: 360, animation: torchAnimation} 25 | }, 26 | light: { 27 | label: "Light cantrip", 28 | data: {dim: 40, bright: 20, angle: 360, animation: {type: "none"}} 29 | }, 30 | lamp: { 31 | label: "Lamp", 32 | data: {dim: 45, bright: 15, angle: 360, animation: torchAnimation} 33 | }, 34 | shield: { 35 | label: "Shield", 36 | data: {dim: 0.5, bright: 0, angle: 360, animation: energyShield} 37 | }, 38 | bullseye: { 39 | label: "Bullseye Lantern", 40 | data: {dim: 120, bright: 60, angle: 45, animation: torchAnimation} 41 | } 42 | }; 43 | 44 | function getLights() { 45 | let lightButtons = {}; 46 | Object.entries(lights).forEach(([key, light]) => { 47 | lightButtons[key] = { 48 | label: light.label, 49 | callback: (html) => { 50 | const newColor = html.find("#color").val(); 51 | const newAlpha = Number(html.find("#alpha").val()); 52 | var data = light.data; 53 | tokenUpdate(Object.assign(data, {color: newColor, alpha: newAlpha})); 54 | } 55 | } 56 | }); 57 | 58 | lightButtons = Object.assign(lightButtons, { 59 | close: { 60 | icon: "", 61 | label: `Close` 62 | } 63 | }); 64 | 65 | return lightButtons; 66 | } 67 | 68 | new Dialog({ 69 | title: `Token Light Picker`, 70 | content: ` 71 |
    72 |
    73 | 74 | 75 | ${isGM ? '' : ''} 76 | 77 |
    78 |
    `, 79 | buttons: getLights(), 80 | default: "close", 81 | close: () => {} 82 | }).render(true); 83 | -------------------------------------------------------------------------------- /token/link_token_to_actor.js: -------------------------------------------------------------------------------- 1 | const unlinked = canvas.scene.data.tokens.map(t => { 2 | const actor = game.actors.find(a => a.name === t.name); 3 | if (actor) { 4 | return { 5 | _id: t.id, 6 | actorId: actor.id 7 | } 8 | } else { // this may include actors who's actor name is not the same as the token name (see Starter Heroes) 9 | console.log(t.name); 10 | return { 11 | _id: t.id, 12 | actorId: "" 13 | } 14 | } 15 | }); 16 | const updates = duplicate(unlinked); 17 | canvas.scene.updateEmbeddedDocuments("Token", updates); 18 | 19 | ui.notifications.info('Tokens linked to actors.'); 20 | //console.log(updates); -------------------------------------------------------------------------------- /token/mirror_token_image.js: -------------------------------------------------------------------------------- 1 | // Flips the selected token image along the Y axis. 2 | // Change mirrorY to mirrorX to flip across the X axis 3 | const updates = canvas.tokens.controlled.map(t => ({_id: t.id, mirrorY: !t.data.mirrorY})); 4 | await canvas.scene.updateEmbeddedDocuments("Token", updates); -------------------------------------------------------------------------------- /token/randomize_wildcard_tokens.js: -------------------------------------------------------------------------------- 1 | for(let nextToken of canvas.tokens.placeables) { 2 | if (nextToken.actor.data.token.randomImg) { 3 | let tokenImgArray = await game.actors.get(nextToken.actor.id).getTokenImages(); 4 | let imageChoice = Math.floor(Math.random() * tokenImgArray.length); 5 | let image = tokenImgArray[imageChoice] 6 | await nextToken.document.update({ "img": image }) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /token/remove_conditions.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | for ( let token of canvas.tokens.controlled ) { 3 | let effectsToDelete = token.actor.effects.filter(e => e.sourceName === "None") 4 | .map(e => { return e.id }); // documents api expects array of ids 5 | await token.actor.deleteEmbeddedDocuments("ActiveEffect", effectsToDelete); 6 | }})(); 7 | -------------------------------------------------------------------------------- /token/rename_token_and_actor.js: -------------------------------------------------------------------------------- 1 | // quickly change a token and its corresponding unlinked actor 2 | 3 | async function renameToken(newName) 4 | { 5 | for (const token of canvas.tokens.controlled) { 6 | console.log(newName); 7 | await token.document.update({'name':newName}); 8 | await token.actor.update({'name' : newName}); 9 | } 10 | } 11 | 12 | let applyChanges=false; 13 | new Dialog({ 14 | title: `Rename token & actor`, 15 | content: ` 16 |
    17 |
    18 | 19 | 20 |
    21 |
    22 | `, 23 | buttons: { 24 | yes: { 25 | icon: "", 26 | label: `Apply Changes`, 27 | callback: () => applyChanges = true 28 | }, 29 | no: { 30 | icon: "", 31 | label: `Cancel Changes` 32 | }, 33 | }, 34 | default: "yes", 35 | close: html => { 36 | if (applyChanges) { 37 | let newName = html.find('[name="new-name"]')[0].value || null; 38 | if(newName) 39 | renameToken(newName); 40 | } 41 | } 42 | }).render(true); 43 | -------------------------------------------------------------------------------- /token/select_token_in_stack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Lets the user select one token to control from a list of selected tokens. 3 | * Useful when tokens are stacked atop one another in the scene. 4 | * (The user is expected to select the tokens before calling this macro.) 5 | */ 6 | function templateForOptions() 7 | { 8 | let template = `

    Select one token to control from the selected group.

    9 |
    10 | 11 | 18 |
    `; 19 | return template; 20 | } 21 | 22 | if (canvas.tokens.controlled.length > 1) 23 | { 24 | let applyChanges = false; 25 | let tokenPicker = new Dialog({ 26 | title: `Token Picker`, 27 | content: templateForOptions(), 28 | buttons: { 29 | pick: { 30 | icon: "", 31 | label: "Pick", 32 | callback: () => applyChanges = true }, 33 | cancel: { 34 | icon: "", 35 | label: "Cancel", 36 | callback: () => applyChanges = false } 37 | }, 38 | close: html => { 39 | if (applyChanges) { 40 | let selectedID = html.find('#selected')[0].value; 41 | canvas.tokens.selectObjects([]); 42 | const observable = canvas.tokens.placeables.filter(t => t.id === selectedID); 43 | if (observable !== undefined) 44 | observable[0].control(); 45 | }} 46 | }); 47 | 48 | tokenPicker.render(true); 49 | } 50 | else 51 | ui.notifications.warn("Select a group of tokens to pick from."); 52 | -------------------------------------------------------------------------------- /token/set_name_and_bars.js: -------------------------------------------------------------------------------- 1 | // Update all tokens on the map so that the name shows on hover and the bars always show. 2 | // Display Modes: ALWAYS, CONTROL, HOVER, NONE, OWNER, OWNER_HOVER 3 | 4 | const tokens = canvas.tokens.placeables.map(token => { 5 | return { 6 | _id: token.id, 7 | "bar1.attribute": "attributes.hp", 8 | "bar2.attribute": "attributes.ac.value", 9 | "displayName": CONST.TOKEN_DISPLAY_MODES.OWNER_HOVER, 10 | "displayBars": CONST.TOKEN_DISPLAY_MODES.OWNER 11 | }; 12 | }); 13 | 14 | canvas.scene.updateEmbeddedDocuments('Token', tokens) 15 | -------------------------------------------------------------------------------- /token/shrink_or_enlarge.js: -------------------------------------------------------------------------------- 1 | // Update selected tokens to flip between a 1x1 or a 2x2 grid. 2 | 3 | const updates = []; 4 | for (let token of canvas.tokens.controlled) { 5 | let newSize = (token.data.height == 1 && token.data.width == 1) ? 2 : 1; 6 | updates.push({ 7 | _id: token.id, 8 | height: newSize, 9 | width: newSize 10 | }); 11 | }; 12 | 13 | // use `canvas.tokens.updateMany` instead of `token.update` to prevent race conditions 14 | // (meaning not all updates will be persisted and might only show locally) 15 | canvas.scene.updateEmbeddedDocuments("Token", updates); 16 | -------------------------------------------------------------------------------- /token/switch_images.js: -------------------------------------------------------------------------------- 1 | // Allows swapping between two different .png images. 2 | // Token sides should have "a" and "b" at the end of the name like "name-a.png" and "name-b.png". 3 | // If you have a different ending, change aName and bName respectively. 4 | // Author: Phenomen 5 | 6 | // IMPORTANT. These two values MUST be the same length. 7 | let aName = 'a.png' 8 | let bName = 'b.png' 9 | 10 | let tok = canvas.tokens.controlled[0]; 11 | let img = tok.data.img; 12 | var currentSide = img[img.length - aName.length]; 13 | img = img.slice(0,-Math.abs(aName.length)) + (currentSide == 'a' ? bName: aName); 14 | tok.document.update({ img }); 15 | -------------------------------------------------------------------------------- /token/token_behavior.js: -------------------------------------------------------------------------------- 1 | const version = 'v1.0'; 2 | 3 | main(); 4 | 5 | 6 | function main() { 7 | // Add Vision Type only if the Game Master is using the Macro 8 | let dialogue_content = ` 9 |
    10 |
    11 | 12 | 20 |
    21 |
    22 | `; 23 | 24 | let applyChanges = false; 25 | let dialogButtons = { 26 | yes: { 27 | icon: "", 28 | label: `Apply Changes`, 29 | callback: (html) => { 30 | changeName(html); 31 | } 32 | }, 33 | no: { 34 | icon: "", 35 | label: `Cancel Changes` 36 | } 37 | } 38 | 39 | // Main Dialogue 40 | new Dialog({ 41 | title: `Token Name - ${version}`, 42 | content: dialogue_content, 43 | buttons: dialogButtons, 44 | default: "yes", 45 | }).render(true); 46 | } 47 | 48 | async function changeName(html) { 49 | let nameBehavior = html.find('[name="light-source"]')[0].value || "none"; 50 | let nameConst; 51 | // Update all tokens on the map so that the name shows on hover and the bars always show. 52 | // Display Modes: ALWAYS, CONTROL, HOVER, NONE, OWNER, OWNER_HOVER 53 | 54 | switch(nameBehavior) { 55 | case 'NONE': 56 | nameConst = CONST.TOKEN_DISPLAY_MODES.NONE 57 | break; 58 | case 'ALWAYS': 59 | nameConst = CONST.TOKEN_DISPLAY_MODES.ALWAYS 60 | break; 61 | case 'CONTROL': 62 | nameConst = CONST.TOKEN_DISPLAY_MODES.CONTROL 63 | break; 64 | case 'HOVER': 65 | nameConst = CONST.TOKEN_DISPLAY_MODES.HOVER 66 | break; 67 | case 'OWNER': 68 | nameConst = CONST.TOKEN_DISPLAY_MODES.OWNER 69 | break; 70 | case 'OWNER_HOVER': 71 | nameConst = CONST.TOKEN_DISPLAY_MODES.OWNER_HOVER 72 | break; 73 | default: 74 | nameConst = CONST.TOKEN_DISPLAY_MODES.NONE 75 | } 76 | 77 | const tokens = canvas.tokens.placeables.map(token => { 78 | return { 79 | _id: token.id, 80 | "displayName": nameConst 81 | }; 82 | }); 83 | 84 | canvas.scene.updateEmbeddedDocuments('Token', tokens) 85 | } -------------------------------------------------------------------------------- /token/unlink_tokens_from_actor.js: -------------------------------------------------------------------------------- 1 | // Unlinks all currently-selected tokens from their actors. This is useful if you're running 2 | // combat with several copies of the same enemy, as if they're all linked to the same actor, 3 | // applying damage to one applies damage to all. Select all the baddies and apply this macro 4 | // to be able to track their HP individually. 5 | 6 | canvas.tokens.updateAll( 7 | t => ({ actorLink: false }), 8 | t => t._controlled 9 | ); 10 | -------------------------------------------------------------------------------- /wfrp4e/readme.md: -------------------------------------------------------------------------------- 1 | wfrp4e folder --------------------------------------------------------------------------------