Pickpocket Helper

color pick-pocket targets based on difficulty

目前为 2023-11-07 提交的版本。查看 最新版本

// ==UserScript==
// @name         Pickpocket Helper
// @namespace    http://tampermonkey.net/
// @version      0.65
// @description  color pick-pocket targets based on difficulty
// @author       Terekhov
// @match        https://www.torn.com/loader.php?sid=crimes*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        none
// ==/UserScript==

(function () {
  'use strict';
  //
  // Based on guide here https://www.torn.com/forums.php#/p=threads&f=61&t=16358739&b=0&a=0
  // Thanks Emforus [2535044]!
  //
  // This script is triggered down at the bottom; see formatCrimesContainerOnce and startListeningToFormatNewCrimes
  //
  const lsKey = 'pickpocketSkillLevel';

  // Need to wait for page to initialize, before we know this. Assume 1, until then
  let currentSkillLevel = 1;

  function findChildByClassStartingWith(name, parentEle) {
    for (let child of parentEle.children) {
      for (let childClass of child.classList) {
        if (!!childClass && childClass.startsWith(name)) {
          return child;
        }
      }
    }
    return null;
  }

  const colors = {
    ideal:         '#65E037',
    easy:          '#B4E0AD',
    tooEasy:       '#C7DCC4',
    tooHard:       '#fa8e8e',
    uncategorized: '#DA85FF'
  };
  const ALL_MARK_TYPES = [
    'Businessman',
    'Businesswoman',
    'Classy lady',
    'Cyclist',
    'Drunk man',
    'Drunk woman',
    'Elderly man',
    'Elderly woman',
    'Gang member',
    'Homeless person',
    'Jogger',
    'Junkie',
    'Laborer',
    'Mobster',
    'Police officer',
    'Postal Worker',
    'Rich kid',
    'Sex worker',
    'Student',
    'Thug',
    'Young man',
    'Young woman'
  ];
  const MARK_CS_LEVELS_MAP = {

    'Drunk man': '100',
    'Drunk woman': '100',
    'Elderly man': '100',
    'Elderly woman': '100',
    'Homeless person': '100',
    'Junkie': '100',

    'Classy lady': '150',
    'Laborer': '150',
    'Postal Worker': '150',
    'Young man': '150',
    'Young woman': '150',
    'Student': '150',

    'Rich kid': '200',
    'Sex worker': '200',
    'Thug': '200',

    'Businessman': '250',
    'Businesswoman': '250',
    'Jogger': '250',
    'Gang member': '250',
    'Mobster': '250',

    'Cyclist': '300',

    'Police officer': '350'
  };

  /**
   *
   * @return {
   *    csSemantic: 'tooHard', // Police officer when you're lvl 1
   *    activitySemantic: 'ideal',    // Businesswoman 'on phone'
   *    buildSemantic: 'tooHard'     // Skinny businessman,
   *    finalSemantic: 'tooHard'
   * }
   */
  function setColorsForCrimeTarget(crimeChild) {
    const crimesDivAkaSections = findChildByClassStartingWith('sections', crimeChild);
    const mainSection = findChildByClassStartingWith('mainSection', crimesDivAkaSections);


    let targetTypeEle = findChildByClassStartingWith('titleAndProps', mainSection).children[0];
    let targetType;

    for (let type of ALL_MARK_TYPES) {
      if (targetTypeEle.textContent.startsWith(type)) {
        // Handle mobile view e.g. "Police officer 5m 10s"
        targetType = type;
      }
    }

    // e.g. Average 5'0" 158 lbs
    const physicalPropsEle = findChildByClassStartingWith('titleAndProps', mainSection).children[1];
    const physicalProps = physicalPropsEle.textContent;

    // Average
    const build = physicalProps.substring(0, physicalProps.indexOf(' '));

    // e.g. Begging0s
    const activityEle = findChildByClassStartingWith('activity', mainSection);
    const activity = activityEle.textContent;

    // e.g. Begging
    // The ternary handles mobile - in mobile we don't get the status like "Begging" so we can't do optimize there.
    const activityName = activity.match(/^\D+/) ? activity.match(/^\D+/)[0] : '';

    // e.g. 0s
    // const activityTime = activity.substring(activityName.length);

    //
    // type DifficultySemantic = 'ideal' | 'easy' | 'tooEasy' | 'tooHard' | 'uncategorized'
    // interface Difficulties: { [key]: DifficultySemantic } = {
    //   csSemantic: 'tooHard',       // Police officer when you're lvl 1
    //   activitySemantic: 'ideal',    // Businesswoman 'on phone'
    //   buildSemantic: 'tooHard'     // Skinny businessman
    //   finalSemantic: 'tooHard',    // Based on all the above
    // }
    //
    const difficulties = getDifficulties(targetType, build, activityName);

    //
    // Now Set all the colors
    //
    if (difficulties.buildSemantic) {
      physicalPropsEle.style.color = colors[difficulties.buildSemantic];
    }
    if (difficulties.activitySemantic) {
      activityEle.style.color = colors[difficulties.activitySemantic];
    }
    for (let type of ALL_MARK_TYPES) {
      if (targetTypeEle.textContent.startsWith(type)) {
        // Handle mobile view e.g. "Police officer 5m 10s"
        if (targetTypeEle.textContent.indexOf('%)') === -1) {
          targetTypeEle.textContent = targetTypeEle.textContent + ` (${MARK_CS_LEVELS_MAP[type]}%)`;
        }
        targetTypeEle.style.color = colors[difficulties.csSemantic];
      }
    }



    // Set 'Pickpocket' button color
    const divContainingButton = findChildByClassStartingWith('commitButtonSection', crimesDivAkaSections);
    divContainingButton.style.backgroundColor = colors[difficulties.finalSemantic];
  }

  let skillCats = ['Safe', 'Moderately Unsafe', 'Unsafe', 'Risky', 'Dangerous', 'Very Dangerous'];
  let skillStarts = [1, 10, 35, 65, 90, 100];

  function getMaxSkillIndex() {
    let idx = 0;
    skillStarts.forEach((ele, currentIdx) => {
      if (Math.floor(currentSkillLevel) >= ele) {
        idx = currentIdx;
      }
    });
    return idx;
  }

  function getAllSafeSkillCats() {
    let maxIndex = getMaxSkillIndex();
    if (maxIndex >= skillCats.length) {
      return skillCats.slice();
    } else {
      return skillCats.slice(0, maxIndex + 1);
    }
  }
  const markGroups = {
    // CS 1-20
    'Safe': ['Drunk man', 'Drunk woman', 'Homeless person', 'Junkie', 'Elderly man', 'Elderly woman'],

    // CS 10-70
    'Moderately Unsafe': ['Laborer', 'Postal worker', 'Young man', 'Young woman', 'Student'],

    // CS 35-90
    'Unsafe': ['Classy lady', 'Rich kid', 'Sex worker'],

    // CS 65+
    'Risky': ['Thug', 'Jogger', 'Businessman', 'Businesswoman', 'Gang member'],

    // CS 90+
    'Dangerous': ['Cyclist'],

    // ???
    'Very Dangerous': ['Mobster', 'Police officer'],
  };


  /**
   * @param mark e.g. 'Rich Kid'
   *
   * @return 'ideal' | 'easy' | 'tooEasy' | 'tooHard' | 'uncategorized'
   */
  function getMarkIdealityBasedOnCS(mark) {
    // type colorSemantic = 'ideal' | 'easy' | 'tooEasy' | 'tooHard' | 'uncategorized'
    let safeSkillCats = getAllSafeSkillCats();
    for (let idx = 0; idx < safeSkillCats.length; idx++) {
      let safeSkillCat = safeSkillCats[idx];
      if (markGroups[safeSkillCat].includes(mark)) {
        if (idx === safeSkillCats.length - 1) {
          return 'ideal';
        } else if (idx === safeSkillCats.length - 2) {
          return 'easy';
        } else {
          return 'tooEasy';
        }
      }
    };
    return 'tooHard';
  }

  /**
   *
   * @param markType  Elderly woman
   * @param build     Average
   * @param status    Begging
   *
   * @return {
   *    csSemantic: 'tooHard', // Police officer when you're lvl 1
   *    activitySemantic: 'ideal',    // Businesswoman 'on phone'
   *    buildSemantic: 'tooHard'     // Skinny businessman,
   *    finalSemantic: 'tooHard'
   * }
   */
  function getDifficulties(markType, build, status) {
    // TODO builds and statuses to favor. Too much for now
    const buildsToAvoid = {
      'Businessman': ['Skinny'],
      'Drunk man': ['Muscular'],
      'Gang member': ['Muscular'],
      'Sex worker': ['Muscular'],
      'Student': ['Athletic'],
      'Thug': ['Muscular']
    };
    const statusesToAvoid = {
      'Businessman': ['Walking'],
      'Drunk man': ['Distracted'],
      'Drunk woman': ['Distracted'],
      'Homeless person': ['Loitering'],
      'Junkie': ['Loitering'],
      'Laborer': ['Distracted'],
      'Police officer': ['Walking'],
      'Sex worker': ['Distracted'],
      'Thug': ['Loitering', 'Walking']
    };
    const difficulties = {
      csSemantic: 'uncategorized',
      activitySemantic: undefined,
      buildSemantic: undefined,
      finalSemantic: 'uncategorized'
    }

    // type colorSemantic = 'ideal' | 'easy' | 'tooEasy' | 'tooHard' | 'uncategorized'
    difficulties.csSemantic = getMarkIdealityBasedOnCS(markType);

    // We use csSemantic as baseline; activity and build can override.
    difficulties.finalSemantic = difficulties.csSemantic;

    if (buildsToAvoid[markType] && buildsToAvoid[markType].includes(build)) {
      difficulties.finalSemantic = 'tooHard';
      difficulties.buildSemantic = 'tooHard'
    }
    if (statusesToAvoid[markType] && statusesToAvoid[markType].includes(status)) {
      difficulties.finalSemantic = 'tooHard';
      difficulties.activitySemantic = 'tooHard';
    }
    return difficulties;
  }

  function getCrimesContainer() {
    let crimesContainerName = document.querySelectorAll('[class^="crimeOptionGroup"]')[0].classList[0];
    return document.getElementsByClassName(crimesContainerName)[0];
  }

  function setSkillLevel() {
    currentSkillLevel = +document.getElementsByClassName('slick-slide')[0].children[0].children[0].children[0].children[2].textContent;
  }

  function formatCrimesContainerOnce() {
      console.error('got to formatCrimesContainerOnce');
    if (!window.location.href.includes("#/pickpocketing")) {
      return;
    }
    setSkillLevel();

    for (let node of getCrimesContainer().children) {
      setColorsForCrimeTarget(node);
    }
  }

  let observer;
  let alreadyListening = false;
  function startListeningToFormatNewCrimes() {
    if (!window.location.href.includes("#/pickpocketing")) {
      if (observer) {
        observer.disconnect();
        observer = undefined;
      }
      alreadyListening = false;
      return;
    }
    if (alreadyListening) {
      return;
    }
    setSkillLevel();

    // Select the node that will be observed for mutations
    const targetNode = getCrimesContainer();

    // Options for the observer (which mutations to observe)
    const config = {
      attributes: false,
      childList: true,
      subtree: false
    };

    // Callback function to execute when mutations are observed
    const callback = (mutationList, observer) => {
      for (const mutation of mutationList) {
        if (mutation.type === "childList" && mutation.addedNodes.length > 0) {

          for (let node of targetNode.children) {
            setColorsForCrimeTarget(node);
          }
        }
      }
    };

    // Create an observer instance linked to the callback function
    observer = new MutationObserver(callback);

    // Start observing the target node for configured mutations
    observer.observe(targetNode, config);
    alreadyListening = true;
  }

  // If we land directly on pickpocket page, these handle it correctly.
  setTimeout(formatCrimesContainerOnce, 650);
  setTimeout(startListeningToFormatNewCrimes, 650);

  //
  // GreaseMonkey can't listen for Pickpocket page directly, so we run this on all crimes pages.
  // however if we navigate away from Pickpocket, we stop listening with our observer
  //
  window.onpopstate = function(event) {
    setTimeout(formatCrimesContainerOnce, 650);
    setTimeout(startListeningToFormatNewCrimes, 650);
  }


})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址