All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

307 lines
12 KiB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
  1. class SmartTarget {
  2. static handleTargeting(token,shift) {
  3. const isTargeted = token.isTargeted;
  4. const release = shift ? !SmartTarget.settings().release : SmartTarget.settings().release;
  5. token.setTarget(!isTargeted, { releaseOthers: release });
  6. }
  7. static getBorderColor({hover}={}) {
  8. const colors = CONFIG.Canvas.dispositionColors;
  9. if ( this.controlled ) return colors.CONTROLLED;
  10. else if ( (hover ?? this.hover) || canvas.tokens._highlight ) {
  11. let d = this.document.disposition;
  12. if ( !game.user.isGM && this.isOwner ) return colors.CONTROLLED;
  13. else if ( this.actor?.hasPlayerOwner ) return colors.PARTY;
  14. else if ( d === CONST.TOKEN_DISPOSITIONS.FRIENDLY ) return colors.FRIENDLY;
  15. else if ( d === CONST.TOKEN_DISPOSITIONS.NEUTRAL ) return colors.NEUTRAL;
  16. else if ( d === CONST.TOKEN_DISPOSITIONS.HOSTILE ) return colors.HOSTILE;
  17. else if ( d === CONST.TOKEN_DISPOSITIONS.SECRET ) return this.isOwner ? colors.SECRET : null;
  18. }
  19. return null;
  20. }
  21. static _tokenOnClickLeft(wrapped, ...args) {
  22. const mode = SmartTarget.settings().mode;
  23. switch (mode) {
  24. case 0:
  25. return wrapped(...args);
  26. break;
  27. case 1:
  28. if (game.smartTarget.altModifier) {
  29. SmartTarget.handleTargeting(this,game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.SHIFT));
  30. return
  31. }else{
  32. return wrapped(...args);
  33. }
  34. break;
  35. case 2:
  36. if ((!game.user.isGM && !this.isOwner) || (this.isOwner && game.smartTarget.altModifier)) {
  37. SmartTarget.handleTargeting(this,game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.SHIFT));
  38. return
  39. } else {
  40. return wrapped(...args);
  41. }
  42. break;
  43. }
  44. super._onClickLeft(...args);
  45. }
  46. static canvasOnClickLeft(wrapped, ...args) {
  47. const canvasMousePos = args[0].interactionData.origin
  48. if (game.smartTarget.altModifier){
  49. let distance = Infinity
  50. let closestTemplate = null
  51. for(let template of canvas.templates.placeables){
  52. if(!template.owner) continue
  53. const inTemplate = template.shape.contains(canvasMousePos.x-template.x,canvasMousePos.y-template.y)
  54. const d = Math.sqrt(Math.pow(template.x-canvasMousePos.x,2)+Math.pow(template.y-canvasMousePos.y,2))
  55. if(inTemplate && d<distance){
  56. distance = d
  57. closestTemplate = template
  58. }
  59. }
  60. if(closestTemplate){
  61. const release = game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.SHIFT) ? !SmartTarget.settings().release : SmartTarget.settings().release;
  62. if (release)canvas.tokens.placeables[0]?.setTarget(false, { releaseOthers: true });
  63. for(let token of canvas.tokens.placeables){
  64. if(closestTemplate.shape.contains(token.center.x-closestTemplate.x,token.center.y-closestTemplate.y)){
  65. token.setTarget(!token.isTargeted, { releaseOthers: false });
  66. }
  67. }
  68. }
  69. }
  70. return wrapped(...args);
  71. }
  72. static _canControl(wrapped,...args){
  73. if(!args[1]) return wrapped(...args);
  74. const mode = SmartTarget.settings().mode;
  75. if(mode==1 && game.smartTarget.altModifier) return true;
  76. if(mode==2 && !game.user.isGM && !this.isOwner) return true;
  77. return wrapped(...args);
  78. }
  79. static getOffset(token, length) {
  80. const width = token.w;
  81. const height = token.h;
  82. const position = game.settings.get(SMARTTARGET_MODULE_NAME, "pipPosition")
  83. const circleR = game.settings.get(SMARTTARGET_MODULE_NAME, "pipScale") || 12;
  84. let circleOffsetMult = game.settings.get(SMARTTARGET_MODULE_NAME, "pipOffset") || 16;
  85. let insidePip = game.settings.get(SMARTTARGET_MODULE_NAME, "insidePips") ? circleR : 0;
  86. const totalHeight = circleR*2;
  87. const totalWidth = (circleR*2)*length - circleOffsetMult*(length-1);
  88. const offset = {
  89. x: 0,
  90. y: 0,
  91. };
  92. switch (position) {
  93. case "topleft":
  94. break;
  95. case "topright":
  96. offset.x = width - totalWidth;
  97. break;
  98. case "bottomleft":
  99. offset.y = height - totalHeight;
  100. break;
  101. case "bottomright":
  102. offset.x = width - totalWidth;
  103. offset.y = height - totalHeight;
  104. break;
  105. case "centertop":
  106. offset.x = (width - totalWidth) / 2;
  107. break;
  108. case "centerbottom":
  109. offset.x = (width - totalWidth) / 2;
  110. offset.y = height - totalHeight;
  111. break;
  112. case "random":
  113. offset.x = Math.floor(Math.random() * (width - totalWidth));
  114. offset.y = Math.floor(Math.random() * (height - totalHeight));
  115. break;
  116. }
  117. return offset;
  118. }
  119. /**
  120. * Creates a sprite from the selected avatar and positions around the container
  121. * @param {User} u -- the user to get
  122. * @param {int} i -- the current row count
  123. * @param {token} target -- PIXI.js container for height & width (the token)
  124. */
  125. static buildCharacterPortrait(u, i, target, token, totalOffset) {
  126. let color = Color.from(u.color);
  127. let circleR = game.settings.get(SMARTTARGET_MODULE_NAME, "pipScale") || 12;
  128. let circleOffsetMult =
  129. game.settings.get(SMARTTARGET_MODULE_NAME, "pipOffset") || 16;
  130. let scaleMulti =
  131. game.settings.get(SMARTTARGET_MODULE_NAME, "pipImgScale") || 1;
  132. let insidePip = game.settings.get(SMARTTARGET_MODULE_NAME, "insidePips")
  133. ? circleR
  134. : 0;
  135. let pTex;
  136. if (!u.isGM) {
  137. let character = u.character;
  138. if (!character) {
  139. character = u.character;
  140. }
  141. if (character) {
  142. pTex = game.settings.get(SMARTTARGET_MODULE_NAME, "useToken")
  143. ? character.prototypeToken.texture.src || character.img
  144. : character.img || character.prototypeToken.texture.src;
  145. } else {
  146. pTex = u.avatar;
  147. }
  148. }
  149. const gmTexSetting = game.settings.get(SMARTTARGET_MODULE_NAME, "useTokenGm")
  150. let gmTexture = gmTexSetting ? token.document.getFlag(SMARTTARGET_MODULE_NAME,"gmtargetimg") || u.avatar : u.avatar
  151. function redraw(){
  152. token._refreshTarget()
  153. }
  154. let texture = u.isGM
  155. ? PIXI.Texture.from(gmTexture)
  156. : PIXI.Texture.from(pTex);
  157. if (!texture.baseTexture.valid) texture.once("update", redraw);
  158. let newTexW = scaleMulti * (2 * circleR);
  159. let newTexH = scaleMulti * (2 * circleR);
  160. let borderThic = game.settings.get(SMARTTARGET_MODULE_NAME, "borderThicc");
  161. let portraitCenterOffset =
  162. scaleMulti >= 1 ? (16 + circleR / 12) * Math.log2(scaleMulti) : 0;
  163. portraitCenterOffset +=
  164. game.settings.get(SMARTTARGET_MODULE_NAME, "pipOffsetManualY") || 0;
  165. let portraitXoffset =
  166. game.settings.get(SMARTTARGET_MODULE_NAME, "pipOffsetManualX") || 0;
  167. let matrix = new PIXI.Matrix(
  168. (scaleMulti * (2 * circleR + 2)) / texture.width,
  169. 0,
  170. 0,
  171. (scaleMulti * (2 * circleR + 2)) / texture.height,
  172. newTexW / 2 + 4 + i * circleOffsetMult + portraitXoffset + insidePip + totalOffset.x,
  173. newTexH / 2 + portraitCenterOffset + insidePip + totalOffset.y
  174. );
  175. token.target
  176. .beginFill(color)
  177. .drawCircle(2 + i * circleOffsetMult + insidePip + totalOffset.x, 0 + insidePip + totalOffset.y, circleR)
  178. .beginTextureFill({
  179. texture: texture,
  180. alpha: 1,
  181. matrix: matrix,
  182. })
  183. .lineStyle(borderThic, 0x0000000)
  184. .drawCircle(2 + i * circleOffsetMult + insidePip + totalOffset.x, 0 + insidePip + totalOffset.y, circleR)
  185. .endFill()
  186. .lineStyle(borderThic / 2, color)
  187. .drawCircle(2 + i * circleOffsetMult + insidePip + totalOffset.x, 0 + insidePip + totalOffset.y, circleR)
  188. }
  189. // Draw custom crosshair and pips
  190. static async _refreshTarget(wrapped, ...args) {
  191. const usePips = game.settings.get(SMARTTARGET_MODULE_NAME, "portraitPips");
  192. const selectedIndicator = game.settings.get(SMARTTARGET_MODULE_NAME, "target-indicator");
  193. if(!usePips && selectedIndicator == "0") return wrapped(...args);
  194. if (!this.target) return;
  195. let reticule = args[0] ?? {};
  196. this.target.clear();
  197. // We don't show the target arrows for a secret token disposition and non-GM users
  198. const isSecret = this.document.disposition === CONST.TOKEN_DISPOSITIONS.SECRET && !this.isOwner;
  199. if (!this.targeted.size || isSecret) return;
  200. // Determine whether the current user has target and any other users
  201. const [others, user] = Array.from(this.targeted).partition(
  202. (u) => u === game.user
  203. );
  204. const userTarget = user.length;
  205. // For the current user, draw the target arrows
  206. if (userTarget) {
  207. let textColor = game.settings.get(
  208. SMARTTARGET_MODULE_NAME,
  209. "crossairColor"
  210. )
  211. ? game.settings
  212. .get(SMARTTARGET_MODULE_NAME, "crossairColor")
  213. .replace("#", "0x")
  214. : SmartTarget.getBorderColor.bind(this)({hover: true});
  215. if (game.settings.get(SMARTTARGET_MODULE_NAME, "use-player-color")) {
  216. textColor = Color.from(game.user["color"]);
  217. }
  218. let p = 4;
  219. let aw = 12;
  220. let h = this.h;
  221. let hh = h / 2;
  222. let w = this.w;
  223. let hw = w / 2;
  224. let ah = canvas.dimensions.size / 3;
  225. switch (selectedIndicator) {
  226. case "0":
  227. reticule.color = textColor;
  228. this._drawTarget(reticule)//{color: textColor})//drawDefault(this, textColor, p, aw, h, hh, w, hw, ah);
  229. break;
  230. case "1":
  231. drawCrossHairs1(this, textColor, p, aw, h, hh, w, hw, ah);
  232. break;
  233. case "2":
  234. drawCrossHairs2(this, textColor, p, aw, h, hh, w, hw, ah);
  235. break;
  236. case "3":
  237. drawBullsEye1(this, textColor, p, aw, h, hh, w, hw, ah);
  238. break;
  239. case "4":
  240. drawBullsEye2(this, textColor, p, aw, h, hh, w, hw, ah);
  241. break;
  242. case "5":
  243. drawBetterTarget(this, textColor, p, aw, h, hh, w, hw, ah);
  244. break;
  245. default:
  246. drawDefault(this, textColor, p, aw, h, hh, w, hw, ah);
  247. break;
  248. }
  249. }
  250. // For other users, draw offset pips
  251. if (usePips) {
  252. for (let [i, u] of others.entries()) {
  253. const offset = SmartTarget.getOffset(this, others.length);
  254. SmartTarget.buildCharacterPortrait(u, i, this.target,this, offset);
  255. }
  256. } else {
  257. const hw2 = this.w / 2 + (others.length % 2 === 0 ? 8 : 0);
  258. for (let [i, u] of others.entries()) {
  259. const offset = Math.floor((i + 1) / 2) * 16;
  260. const sign = i % 2 === 0 ? 1 : -1;
  261. const x = hw2 + sign * offset;
  262. this.target.beginFill(Color.from(u.color), 1.0).lineStyle(2, 0x0000000).drawCircle(x, 0, 6);
  263. }
  264. }
  265. }
  266. static settings() {
  267. const settings = {
  268. mode: game.settings.get(SMARTTARGET_MODULE_NAME, "targetingMode"),
  269. release: !game.settings.get(SMARTTARGET_MODULE_NAME, "release"),
  270. };
  271. return settings;
  272. }
  273. }
  274. Hooks.on("targetToken", (user,token,targeted) =>{
  275. const gmTexSetting = game.settings.get(SMARTTARGET_MODULE_NAME, "useTokenGm")
  276. if(!game.user.isGM || !targeted || !gmTexSetting) return
  277. let flag
  278. if(gmTexSetting == 1) flag = _token?.document.actor?.img || _token?.document.texture.src
  279. if(gmTexSetting == 2) flag = _token?.document.texture.src || _token?.document.actor?.img
  280. flag && flag != token.document.getFlag(SMARTTARGET_MODULE_NAME,"gmtargetimg") && token.document.setFlag(SMARTTARGET_MODULE_NAME,"gmtargetimg",flag)
  281. })
  282. Hooks.on("updateToken", (token, update) => {
  283. if (update?.flags?.[SMARTTARGET_MODULE_NAME]?.gmtargetimg) {
  284. token.object._refreshTarget()
  285. }
  286. });