Kruhlmann/gatekeeper

View on GitHub
src/captchas.ts

Summary

Maintainability
A
4 hrs
Test Coverage
import * as adjectives from "../res/adjectives.json";
import * as nouns from "../res/nouns.json";
import { Scenario, CombatScenario, Captcha } from "./typings/types";

const sexes = ["non-binary", "male", "female"];
const races = ["orc", "human", "troll", "gnome"];
const weapons = [
    ["sword", "dual wielded"],
    ["sword", "2 handed"],
    ["mace", "dual wielded"],
    ["mace", "2 handed"],
    ["axe", "dual wielded"],
    ["axe", "2 handed"],
    ["dagger", "dual wielded"],
    ["fist", "dual wielded"],
    ["staff", "2 handed"],
    ["polearm", "2 handed"],
];
const targets = [
    "Firelord",
    "Lava Reaver",
    "Lava Surger",
    "Firewalker",
    "Core Hound",
    "Lava Annihilator",
];
const min_lvl = 57;
const max_lvl = 63;

function title_case(str: string): string {
    let splitStr = str.toLowerCase().split(" ");
    for (let i = 0; i < splitStr.length; i++) {
        // You do not need to check if i is larger than splitStr length, as your for does that for you
        // Assign it back to the array
        splitStr[i] =
            splitStr[i].charAt(0).toUpperCase() + splitStr[i].substring(1);
    }
    // Directly return the joined string
    return splitStr.join(" ");
}

/**
 * Returns a random element in an array.
 *
 * @param arr - Array of elements.
 * @returns - A random element in the array.
 */
function arr_random(arr: any[]): any {
    return arr[Math.floor(Math.random() * arr.length)];
}

/**
 * Generates random data for a scenario.
 *
 * @returns - Scenario data.
 */
function make_scenario(): Scenario {
    const sex = arr_random(sexes) as string;
    const race = arr_random(races) as string;
    const sex_prefix = sex === "male" && race === "human" ? "bald" : "";
    const guild_adjective = arr_random(adjectives.words);
    const guild_noun = arr_random(nouns.words);
    const guild_name = title_case(`${guild_adjective} ${guild_noun}`);

    return { sex, race, sex_prefix, guild_name };
}

/**
 * Generates a random combat scenario.
 *
 * @returns - Scenario data.
 */
function make_combat_scenario(): CombatScenario {
    const scenario = make_scenario();
    const [wpn_type, wpn_subtype]: [string, string] = arr_random(weapons);
    const orc_factor = scenario.race === "orc" && wpn_type === "axe" ? 5 : 0;
    const human_factor =
        scenario.race === "human" &&
        (wpn_type === "mace" || wpn_type === "sword")
            ? 5
            : 0;
    const wpn_skill = 300 + orc_factor + human_factor;

    const tar_nam = arr_random(targets) as string;
    const tar_lvl = Math.round(Math.random() * (max_lvl - min_lvl) + min_lvl);

    const weapon = { type: wpn_type, subtype: wpn_subtype, skill: wpn_skill };
    const target = { name: tar_nam, level: tar_lvl, defense: tar_lvl * 5 };
    return {
        ...scenario,
        weapon,
        target,
        skill_delta: tar_lvl * 5 - wpn_skill,
    };
}

function calc_mitigation(
    mitigation_type: string,
    front: boolean,
    scenario: CombatScenario,
    yellow_hits: boolean
): { answer: number; query: string; example: string } {
    const miss_modifier = scenario.skill_delta > 10 ? 0.2 : 0.1;
    const miss_penalty = scenario.skill_delta > 10 ? 1 : 0;
    let answer: number;
    let example: string;
    let query: string;

    if (mitigation_type !== "none") {
        // Mitigation type is either block, dodge, parry or glancing.
        switch (mitigation_type) {
            case "parry":
                // TODO: This has no formula yet.
                const parry_chance = 10;
                answer = Math.ceil(parry_chance / 10) * 10;
                query =
                    "the chance that your attacks are parried (**rounded up to nearest 1/10th**)?";
                example = "19.4";
                break;
            case "block":
                const block_chance = front
                    ? Math.min(5, 5 + scenario.skill_delta * 0.1)
                    : 0;
                answer = block_chance;
                query =
                    "the chance that your attacks are blocked (**rounded up to nearest 1/10th**)?";
                example = "14.2";
                break;
            case "dodge":
                const dodge_chance = 5 + scenario.skill_delta * 0.1;
                answer = dodge_chance;
                query =
                    "the chance that your attacks are dodged (**rounded up to nearest 1/10th**)?";
                example = "7.2";
                break;
            case "glancing":
                const glancing_chance =
                    10 +
                    (scenario.target.defense -
                        Math.min(300, scenario.weapon.skill)) *
                        2;
                answer = Math.ceil(glancing_chance / 10) * 10;
                query =
                    "the chance that your auto attacks land a glancing blow (**rounded up to nearest 1/10th**)?";
                example = "5.2";
                break;
            default:
                throw new Error(`Unknown mitigation type: ${mitigation_type}`);
        }
    } else {
        // No mitigation type means hit cap calculation.
        let miss_chance = 5 + scenario.skill_delta * miss_modifier;
        // DWMissChance = NormalMissChance * 0.8 + 20%.
        // This only applies to white hits.
        if (scenario.weapon.subtype === "dual wielded" && !yellow_hits) {
            miss_chance = miss_chance * 0.8 + 20;
        }
        answer = Math.ceil(miss_chance + miss_penalty);
        query = `the hit from gear required to reach your **${
            yellow_hits ? "yellow" : "white"
        }** hit cap (rounded up to nearest 1/10th)?`;
        example = "13.1";
    }

    return { answer, query, example };
}

function generate_scenario_text(
    scenario: CombatScenario,
    front: boolean
): string {
    return `You (a level 60 **${
        scenario.sex_prefix !== "" ? `${scenario.sex_prefix} ` : ""
    }${scenario.sex} ${scenario.race}** DPS fury warrior in **<${
        scenario.guild_name
    }>**) are attacking a level **${scenario.target.level}** **${
        scenario.target.name
    }** from **${front ? "the front" : "behind"}** using your **${
        scenario.weapon.subtype
    } ${scenario.weapon.type}${
        scenario.weapon.subtype == "dual wielded" ? "s" : ""
    }**. You have max weapon skills and no other source of +weapon skill.`;
}

/**
 * Hit cap question generator.
 * What is the chance for a players attacks to be [missed/dodged/glanced, if from the front then also blocked]
 *   Miss
 *     If (MobLevel*5 - AttackerSkill) > 10 then MissChance = 5% + (MobLevel*5 - AttackerSkill) * 0.2%
 *     If (MobLevel*5 - AttackerSkill) <= 10 then MissChance = 5% + (MobLevel*5 - AttackerSkill) * 0.1%
 *     If dual-wielding then MissChance = NormalMissChance * 0.8 + 20%
 *   Dodge = 5% + (MobLevel*5 - AttackerSkill) * 0.1%
 *   Glance = 10% + (MobLevel*5 - MIN(AttackerLevel*5, AttackerSkill)) * 2%
 *   Block = MIN(5%, 5% + (MobLevel*5 - AttackerSkill) * 0.1%)
 *
 * @returns - Message content object.
 */
export function hit_cap_generator(
    _scenario?: CombatScenario,
    _mitigation_type?: string,
    _yellow_hits?: boolean,
    _front?: boolean
): Captcha {
    const scenario = _scenario ? _scenario : make_combat_scenario();
    const mitigation_type = _mitigation_type
        ? _mitigation_type
        : arr_random(["none", "dodge", "block", "glancing"]);
    const yellow_hits =
        _yellow_hits !== undefined ? _yellow_hits : Math.random() < 0.5;
    const front =
        _front !== undefined
            ? _front || mitigation_type === "block"
            : Math.random() < 0.5 || mitigation_type === "block";
    const mitigation_calc = calc_mitigation(
        mitigation_type,
        front,
        scenario,
        yellow_hits
    );

    // Message text.
    const scenario_txt = generate_scenario_text(scenario, front);
    const question = `Given these parameters what is ${mitigation_calc.query}\n\nAnswer example: \`${mitigation_calc.example}\``;

    return {
        answer: Math.max(0, mitigation_calc.answer).toFixed(1),
        text: `${scenario_txt}\n\n${question}`,
    };
}

function wrapper_parry_generator(_scenario?: CombatScenario) {
    return hit_cap_generator(_scenario, "parry");
}
function wrapper_block_generator(_scenario?: CombatScenario) {
    return hit_cap_generator(_scenario, "block");
}
function wrapper_dodge_generator(_scenario?: CombatScenario) {
    return hit_cap_generator(_scenario, "dodge");
}
function wrapper_glancing_generator(_scenario?: CombatScenario) {
    return hit_cap_generator(_scenario, "glancing");
}
function wrapper_hit_cap_generator(_scenario?: CombatScenario) {
    return hit_cap_generator(_scenario, "none");
}

// List of all generators.
export const generators: Function[] = [
    // TODO: Re-add this when we actually have a formula.
    // wrapper_parry_generator,
    wrapper_block_generator,
    wrapper_dodge_generator,
    wrapper_glancing_generator,
    wrapper_hit_cap_generator,
];

/**
 * Returns a random generator function.
 *
 * @returns - Generator function.
 */
export function generate(): Captcha {
    return arr_random(generators)();
}