Skip to content

Instantly share code, notes, and snippets.

@diwako
Last active February 25, 2023 20:03
Show Gist options
  • Save diwako/03554d6e3e682ae9fcb9c2e895bc7db7 to your computer and use it in GitHub Desktop.
Save diwako/03554d6e3e682ae9fcb9c2e895bc7db7 to your computer and use it in GitHub Desktop.
Foundry VTT world script for the cyberpunk red core system to display if a ranged attack has hit or not
/*
==Info about this script==
This script will hook into chat messages and look up attack rolls for ranged attacks,
check the distance between attacker and defender token, check the DV of used weapon and
determine if the attack hits or not. The result is displayed in chat.
Make sure to TARGET the token you want to attack ;)
It also plays hit animations and miss animations depending on the roll
Item Piles support was added to disallow dropping or selling of upgraded items, as those
can either leave the upgrade items behind or add a broken weapon to a character sheet,
rendering the sheet unusable.
This script adds settings to the "Game Settings" menu in the "Cyberpunk Red - Core" entry.
Just look for "(Hit Script)" to identify the settings of this script.
==Requirements==
For the animations you will need the JB2A Patreon animation module and Sequencer module
If you lack any of them or do not want the animations. Either have Sequencer not loaded or search
for "new Sequence" and kill these parts of the code. Sequencer is a soft requirement.
Item Piles is supported, not a requirement
==Known issues==
Only Ranged DV tables from the system's compendium are being used. Custom made ones are not supported.
==Installation==
This script is a world script. See this link how to install these kind of scripts
https://foundryvtt.wiki/en/basics/world-scripts
==Updating or making changes to this script==
If you want to add more to it, change thigns, or fix stuff, go nuts. In case you want to share the enhancements
so it can be added to this gist, feel free to contact me.
==Contact==
You can find me (probably) in the discord server for the CPRED-Core Foundry system named Jay's Table
with the name "diwako"
or via DMs in discord diwako#7777
*/
console.log("diwako start");
// Check if an attack hits or not
// Huge thanks to Zhell from the foundry discord for all the help
Hooks.on("createChatMessage", async function (message) {
// if (!game.user.isGM) return;
if (game.userId != message._source.user) return;
const DIV = document.createElement("DIV");
DIV.innerHTML = message.content;
const isAttack = DIV.querySelector(
`[data-tooltip='${game.i18n.localize(
"CPR.actorSheets.commonActions.rollDamage"
)}']`
);
const data = DIV.querySelector("[data-action=rollDamage]")?.dataset;
if (!isAttack || !data) {
return;
}
// console.log(message);
const attackType = DIV.querySelector(
"div.rollcard-subtitle-center.text-small"
).innerHTML;
if (attackType == `${game.i18n.localize("CPR.rolls.suppressiveFire")}`)
return;
const target = message.user.targets.first();
if (!target) {
console.log(`diwako ===== No target was selected!`);
return;
}
let token =
message.speaker?.token ||
canvas.scene.tokens.get(data.tokenId) ||
canvas.scene.tokens.getName(message.speaker?.alias);
const actor = token?.actor ?? game.actors.get(data.actorId);
// fourth pass trying to get the token
if (actor && !token) {
console.log(
`diwako ===== Fourth token look up fallback! Alias: ${message.speaker?.alias} | Actor ${actor.name}`
);
token = canvas.scene.tokens.getName(actor.prototypeToken.name);
}
const item = actor.items.get(data.itemId);
if (!token || !actor || !item) {
console.log(
`diwako ===== Token missing: ${!token} | Actor missing: ${!actor} | Item missing: ${!item}`
);
return;
}
const attackRoll = parseInt(
DIV.querySelector("span.clickable[data-action='toggleVisibility']")
.innerHTML
);
let dvTable = item.system?.dvTable;
if (!dvTable || dvTable === "") {
console.log(`diwako ===== Weapon has no DV table assigned!`);
return;
}
if (
attackType == `${game.i18n.localize("CPR.global.itemType.skill.autofire")}`
) {
dvTable = dvTable + " (Autofire)";
}
const a = canvas.grid.measureDistance(token, target, { gridSpaces: true });
const b = token.elevation - target.document.elevation;
const dist = Math.round(Math.sqrt(a * a + b * b));
const pack = game.packs.get("cyberpunk-red-core.dvTables");
const tableId = pack.index.getName(dvTable)?._id;
if (!tableId) {
console.log(`diwako ===== No compendium table found => ${dvTable}`);
return;
}
const table = await pack.getDocument(tableId);
const draw = await table.getResultsForRoll(dist);
if (!draw || draw.length === 0) {
console.log(
`diwako ===== Could not draw from compendium table => ${table.name}`
);
return;
}
const dv = parseInt(draw[0].text);
let chatMessage = "";
let backgroundColor = "#b90202ff";
if (dv >= attackRoll) {
if (target.document._actor.system.stats.ref.value >= 8) {
chatMessage = `<b>${token.name} <span class="fg-red">missed</span> ${
target.document.name
}</b> by ${
dv - attackRoll + 1
} according to the ranged DV (${dv})! Roll damage IF they have declared that they are dodging AND your roll has beat their evasion roll!`;
} else {
chatMessage = `<b>${token.name} <span class="fg-red">missed</span> ${
target.document.name
}</b> by ${dv - attackRoll + 1} (DV: ${dv})!`;
}
if (
window.Sequence &&
game.settings.get(game.system.id, "diwako-hit-animations")
) {
new Sequence()
.effect()
.delay(1000)
.file(
"modules/jb2a_patreon/Library/Generic/UI/Miss_01_Red_200x200.webm"
)
.snapToGrid()
.atLocation(token, { gridUnits: true, offset: { x: 0, y: -0.55 } })
.scaleToObject(1.35)
.locally(message.whisper.length != 0)
.play();
}
} else {
backgroundColor = "#2d9f36";
if (target.document._actor.system.stats.ref.value >= 8) {
chatMessage = `<b>${
token.name
} <span class="fg-green">beats the ranged DV</span> </b>(${dv}, ${
attackRoll - dv
} over)<b> to hit ${target.document.name}</b> by ${
attackRoll - 1 - dv
}! Roll damage IF they have NOT declared that they are dodging OR your roll has beat their evasion roll!`;
} else {
chatMessage = `<b>${token.name} <span class="fg-green">hits</span> ${
target.document.name
}</b> (DV: ${dv}, ${attackRoll - dv} over)! Roll damage!`;
}
if (
window.Sequence &&
game.settings.get(game.system.id, "diwako-hit-animations")
) {
let angle =
(360 +
Math.atan2(target.y - token.y, target.x - token.x) *
(180 / Math.PI)) %
360;
// hit marker sound effect
const sequence = new Sequence();
if (game.settings.get(game.system.id, "diwako-use-hit-sounds")) {
let sounds = [
"bf1.ogg",
"cod.ogg",
"quake.ogg",
"hl1.ogg",
"windows.ogg",
"crash.ogg",
"roblox.ogg",
"boardgameonline.ogg",
"disco.ogg",
"titanfall2.mp3",
];
sequence
.sound()
.delay(1000)
.file(
`worlds/diw_cpred/sounds/hitsounds/${
sounds[Math.floor(Math.random() * sounds.length)]
}`
)
.volume(0.35)
.locally(message.whisper.length != 0);
}
// blood effect
sequence
.effect()
.delay(250)
.file(
"modules/jb2a_patreon/Library/Generic/Weapon_Attacks/Melee/DmgBludgeoning_01_Regular_Yellow_2Handed_800x600.webm"
)
.atLocation(target, {
offset: {
x: -Math.cos((angle * Math.PI) / 180),
y: -Math.sin((angle * Math.PI) / 180),
},
gridUnits: true,
})
.rotate(angle * -1)
.locally(message.whisper.length != 0)
.play();
}
}
ChatMessage.create(
{
speaker: message.speaker,
content: `<div class="cpr-block" style="padding:10px;background-color:${backgroundColor}">${chatMessage}</div>`,
type: message.type,
whisper: message.whisper,
},
{ chatBubble: false }
);
});
[
"preDropItemDetermined",
"preTradeItems",
"preDropItem",
"preTransferItems",
"preGiveItem",
"preRemoveItems",
"preTransferAllItems",
].forEach((hookname) => {
Hooks.on(
`item-piles-${hookname}`,
function (actor, someBoolean1, itemObject, someBoolean2) {
if (itemObject.item.system.upgrades.length) {
ui.notifications.error(
"Dropping/trading/giving upgraded items will break parts of the character sheet. If you want to trade an upgraded item or drop it, tell the GM."
);
return false;
}
return true;
}
);
});
Hooks.once("init", function () {
console.log("diwako settings start");
game.settings.register(game.system.id, "diwako-hit-animations", {
name: "(Hit Script) Show hit or miss animations",
hint: "Enable or disable hit or miss animations. Disable if you do not have the JB2A patron module or Sequencer loaded!",
scope: "world",
config: true,
type: Boolean,
default: true,
});
game.settings.register(game.system.id, "diwako-use-hit-sounds", {
name: "(Hit Script) Play sounds on hit",
hint: "THIS IS ONLY AN OPTION AS I USE CUSTOM SOUNDS! I cannot supply these sounds, so I am adding an option to make maintenance of this script easier! This option REQUIRES the hit animations",
scope: "world",
config: true,
type: Boolean,
default: false,
});
console.log("diwako settings end");
});
console.log("diwako end");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment