Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Script for automating Thunderwave 5e

December 20 (4 years ago)

Hi,

Super noob, here. I'm trying to create a script/macro combo to help me resolve the Thundewave spell. I've been looking into it, but couldn't find it already done, and my poor coding skills are clearly not up to the task. What I'm trying to achieve:

On a chat event/macro click:
• ask the player what level they want to cast Thunderwave at (level) and 1 is default
• Check if there are tokens in all the squares directly around the caster, get their ids and store them, somehow (15-foot cube originating from the caster)
[ ]    [ ]    [ ]
[ ]     C    [ ]
[ ]    [ ]    [ ]
• If there are, they need to do a Constitution Saving throw against the Caster's Spell DC, which is known. The script combo Groupcheck+ApplyDamage is really nice to take care of the Constitution Save and damages, but you still have to manually select all the tokens in the AoE. And I'm not sure I could combo them further with Kaboom (Apply the Kaboom only to failed saves if that makes sense)
• on a fail
    -They take ((level)+1)d8 thunder damage (Damage)
    -AND are pushed 10feet away from the caster (Kaboom?)
• On a success
    - They take (Damage)*0,5
    - are not pushed

I have been exploring the idea of getting ids of all objects that are inside an Area of Effect, but no joy.
Maybe it is possible to get ids of object filtered by checking their position relative to the caster (it would only require to check into 9 squares around the caster), but I can't wrap my head around that either.


Any help/pointers would be greatly appreciated, as I'm way in over my head, here. Thanks

December 20 (4 years ago)

Edited December 21 (4 years ago)
Oosh
Sheet Author
API Scripter

I'm happy to have a look at this when I get time, if no one else comes to the rescue - though this is the kind of thing Aaron seems to conjure up scripts for all the time - have a look at his script for changing the color of tiles when they're stepped on. It doesn't quite do what you want - it's comparing the position of a token on the object layer with one on the map layer, and looking for an actual overlap. But that function is close to what you want for the initial detection, you just need to expand the "size" of the source token (the caster) to one tile larger than its current size. Any tokens it overlaps with is in the AoE. It's a starting point anyway, get the script to fire when the spell is cast, and get the correct target detection working.

You can grab the direction from that code too (I think, would need to look at it again) by comparing the left & top values to the caster's token. For example if left(target) = left(caster) and top(target) < top(caster), the target is to the north, and need to be pushed one tile to the north. I'm not sure how you avoid pushing a token through a dynamic lighting barrier - there's probably a script already that handles something similar.



edit - Or you could look at Kaboom... seems to contain a lot of what you need

edit 2 - whoops, you already mentioned Kaboom, now that I read your post again. Also.... this sounds like a lot of Thunder, is someone playing a Tempest Cleric?

December 22 (4 years ago)

Hey Oosh,

thanks for stopping by. I haven't had the time to have a look at the changing tiles colors script, but now I'm on holiday, I'll dig right into it. As for Kaboom, I have not yet found a way to use it against only a few handpicked ids (rather than every creature around), but you can definitely filter by size, so there must be a way. Still a lot of reading to do.

Also.... this sounds like a lot of Thunder, is someone playing a Tempest Cleric?

Ha! no, it's for a druid. She jumps in the middle of battles (Or sometimes pulls creatures to her with Thorn Whip) then use this spell to hurt a ton of creatures at the same time. It's a very efficient dangerous strategy, she loves it.

December 23 (4 years ago)

Edited December 23 (4 years ago)
Oosh
Sheet Author
API Scripter

Ok, so here's a bit of a proof-of-concept. It's using a (very lightly) modified Kaboom for the heavy lifting, and findContains() is stolen from Aaron. It's very rough, currently just accepts !thunderwave --charid @{selected|character_id} (or you can specify the charId, of course), and has some ugly sendChat logging. There's no saves or spell involved yet, I was just playing with detecting the right tokens and sending our custom array through to Kaboom to make sure it would work. So currently, the 'lightning-helix' marker means 'failed save', and the target is pushed. There's a crude DL line to the south of the Archmage just to make sure I didn't break Kaboom's DL detection. Click for animation:



The code is this:

const thunderwaveKaboom = (() => { // eslint-disable-line no-unused-vars

const pixelsPerFoot = 14;

const clog = (txt) => {sendChat('thunderBot', txt, null, {noarchive: true})}

const findContains = (obj,filter,layer, spellRadius) => {
if(obj) {
let cx = obj.get('left'),
cy = obj.get('top');

filter = filter || (() => true);
layer = layer || 'objects';

return findObjs({
_pageid: obj.get('pageid'),
_type: "graphic",
subtype: "token",
layer: layer
})
.filter(filter)
.reduce((m,o) => {
let aura = spellRadius*pixelsPerFoot;
let l=o.get('left');
let t=o.get('top');
let w=parseInt(o.get('width')) + 2*aura;
let h=parseInt(o.get('height')) + 2*aura;
let ol=l-(w/2);
let or=l+(w/2);
let ot=t-(h/2);
let ob=t+(h/2);

if( ol <= cx && cx <= or
&& ot <= cy && cy <= ob
){
m.push(o);
}
return m;
},[]);
}
return [];
};

const handleInput = (msg) => { // !thunderwave --charid @{selected|character_id}
if (msg.type === "api" && msg.content.match(/^!thunderwave/i)) {
let charId = (msg.content.match(/--charid\s*(-[^\s]*)/i)) ? msg.content.match(/--charid\s*(-[^\s]*)/i)[1] : null;

if (charId && getObj('character', charId)) {
let casterToken = findObjs({type: 'graphic', subtype: 'token', represents: charId});
casterToken = (casterToken.length > 0) ? casterToken[0] : null;
if (!casterToken) return clog(`No token found for ${getObj('character', charId).get('name')}`)
let targetArray = findContains(casterToken, (o) => (o.id !== casterToken.id && o.get('represents')), 'objects', 5)
.filter((o) => o.get('statusmarkers').search(/lightning/i) !== -1);
// ^^^ saving throws go here instead of the filter, mapping the failed saves to a new array
let targetNames = targetArray.map((o) => o.get('name')).join(',');
clog(`Targets affected: ${targetNames}`);

KABOOM.NOW({effectPower: 10, effectRadius: 10, vfx: false, scatter: false}, casterToken, targetArray);

} else return clog(`Cannot find character for id: ${charId}`);
}
};
const registerEventHandlers = () => {
on('chat:message', handleInput);
};

on('ready', () => {
//checkInstall();
log('<< thunderWave test >> v0.0.0')
registerEventHandlers();
});
return {
// Public interface here
};
})();


And the minor modification to Kaboom (which does mean disabling the one-click version), lines 460 & 482 (or thereabouts):

  const prepareExplosion = function (rawOptions, rawCenter, targetObjects) {
// Check if our inputs are valid
var options = verifyOptions(rawOptions);
var explosion_center = verifyObject(rawCenter);
var pageInfo = getPageInfo(explosion_center.pageid);

// Error checking for API users
if (!options.effectPower) {
log('KABOOM - Effect power missing.');
return false;
} else if (!explosion_center.position) {
log('KABOOM - Explosion center missing.');
return false;
} else if (options.effectPower > options.effectRadius) {
log('KABOOM - Effect radius must always be higher than effect power.');
return false;
} else if (!pageInfo) {
log('KABOOM - Pageid supplied does not exist.');
return false;
}

// findObjs arrays here
var affectedObjects = (targetObjects) ? targetObjects : findGraphics(explosion_center);
var walls = state.KABOOM.walls_stop_movement


I've added an extra argument for the public-facing function. If it isn't provided, Kaboom defaults to its normal findGraphics function. If it is provided, it skips the findGraphics bit and only processes the tokens we feed it. There's the one modification at the top: targetObjects, and the replaced definition for affectedObjects.


So... I'm not familiar with Group Check or Apply Damage. The easiest way might be to cherry-pick a couple of required functions and throw them in the custom script... I'm not sure. I'd assume trying to chain together too many scripts is going to involve some async issues... which could probably be fixed with Promises. That's something I'm a total novice on though :/


Let me know what you think. One of the cleverer people may have a much simpler solution though :)


edit - one minor issue: Kaboom uses Euclidean distance for diagonal pushes. I've not really dealt with token positions enough to know a good way around that (can't be too hard though).

December 23 (4 years ago)

Wow! this looks very promising. I'm gonna play around with this today. Thanks a lot for your time