Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Glitchtext for scanning Abominable characters to journal #591

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

programmablereya
Copy link

Adds functionality to the Scan (Journal) macro which enables it to render glitchy, distorted unreadable garbage in place of the normal results when the scan target has the Abominable feature (from the Horror template from Dustgrave).

Some information is still conveyed - the name of the character (in the journal entry's title, but not in the scan results header), the number of templates, the number of systems from each one, the number of weapons, and of course that it is a Horror and it has the Abominable feature, as well as what the Abominable feature means. Additionally, the glitchy text roughly matches the length of the original text with some random variance thrown in, which does allow for a certain level of metagaming. If Abominable is marked as destroyed, the scan will work as normal.

Example screenshot:
Screenshot from 2023-11-12 14-56-18

The human-readable version of the updated script is here:

//Sanity Check - Can this user even create folders and journal entries? Does the folder journal folder exist?
if (JournalEntry.canUserCreate(game.user) === false) {
    ui.notifications.error(`${game.user.name} attempted to run SCAN to Journal but lacks proper permissions. Please correct and try again.`);
    return;
}

//Variables - Change these to control the macro

//journalFolderName - The name, as displayed in foundry, of the folder you want the journal entries to save to. Remember to enclose in quotes: 'example'
var journalFolderName = 'SCAN Database';

//nameTemplate - The text before the scan number and target name. Remember to enclose in quotes: 'example'
var nameTemplate = 'SCAN: ';

//numberLength - The total length of the scan number, extra spaces are filled with 0s. Setting this to 3, for example would produce scan number 001 on your first scan. Integers only and no quotes.
var numberLength = 3;

//startingNumber - If you want the scan number to start at something other than 1 then change this. Integers only and no quotes.
var startingNumber = 1;

//permissionLevel - This sets the default permission level of the scan entry. This must be an integer between 0 and 3 where 0 is "None", 1 is "Limited", 2 is "Observer", and 3 is "Owner"
var permissionLevel = 3;

//updateExisting - This macro will check if a scan journal entry exists and update it, set this to false if you want it to create a new scan journal entry.
var updateExisting = true;

// noiseSymbols - Set this to the set of characters or short strings you want to use for malfunctioning scans with Abominable Horror targets.
// The default set comes from the OEM-US character set, codepage 437.
const glitchSymbols = [
    '¿', '⌐', '¬', '¡', '«', '»', '░', '▒', '▓',
    '│', '┤', '╡', '╢', '╖', '╕', '╣', '║', '╗',
    '╝', '╜', '╛', '┐', '└', '┴', '┬', '├', '─',
    '┼', '╞', '╟', '╚', '╔', '╩', '╦', '╠', '═',
    '╬', '╧', '╨', '╤', '╥', '╙', '╘', '╒', '╓',
    '╫', '╪', '┘', '┌', '█', '▄', '▌', '▐', '▀',
    'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ', 'Φ',
    'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩', '≡', '±',
    '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·',
    '√', 'ⁿ', '²', '■', '☺', '☻', '♥', '♦', '♣',
    '♠', '•', '◘', '○', '◙', '♂', '♀', '♪', '♫',
    '☼', '►', '◄', '↕', '‼', '¶', '§', '▬', '↨',
    '↑', '↓', '→', '←', '∟', '↔', '▲', '▼', '#',
    '$', '&', '*', '½', '¼', 'æ', 'Æ', '!', '₧',
    '¥', '£', 'ƒ', '¢', '?', '0', 'x', 'X', ' ']

// glitchNumberLength - Set this to the length of a string of glitchSymbols to add in place of numbers that scan would normally reveal.
const glitchNumberLength = {
    min: 2,
    max: 4,
}

// errorText - Set this to the set of short texts to sprinkle into longer error texts, such as in headers and system names, for scans with Abominable Horror targets.
const errorText = [
    'ERR', 'ERROR', 'MALFUNCTION', 'STATUS',
    'SCAN', 'SCANNING', 'SCANNED', 'CORRUPT',
    'CORRUPTED', 'CORRUPTION', 'FAIL', 'FAILURE',
    'SYSTEM', 'WARN', 'WARNING', 'DATA', 'HORROR',
    'ABOMINABLE', 'DETECT', 'DETECTED', 'DETECTION',
    'ABORT', 'ABORTED', 'UNEXPECTED', 'UNKNOWN',
    'UNFORESEEN',  'INVALID', 'BAD', 'MEMORY',
    'ACCESS', 'PROTOCOL', 'GLITCH', 'BUFFER',
    'OVERFLOW', 'UNDERRUN', 'ACCESS', 'DENY',
    'DENIED', 'FATAL', 'STACK', 'PARSE',
    'WEAPON', 'SYSTEM', 'MECH', 'DANGER',
    'NOMINAL', 'OK', 'SUCCESS', 'UNDEFINED'
]

// errorTextPadding: The minimum and maximum (inclusive) number of glitchSymbols to add before, after, and between errorText.
const errorTextPadding = {
    min: 2,
    max: 10,
}

// errorTextVariance: The minimum and maximum (inclusive) number of characters to add to the length of a class, template, system, or weapon name.
const errorTextVariance = {
    min: -3,
    max: 3
}

//targets - Gets the data for your currently selected target(s) and stores it for later use. Do not change.
let targets = Array.from(game.user.targets);

//Functions
//zeroPad - Adds a set number 0s to the fed number to produce a consistent length number.
const zeroPad = (num, places) => String(num).padStart(places, '0');

//sort_features - Sorts the feature list for the scanned target
function sort_features(a, b) {
    return b.Origin.base - a.Origin.base
}

// isFeatureAbominable - checks if the given feature is the Abominable feature from Horror
function isFeatureAbominable(f) {
    return f.Origin.name === "Horror" && f.Name === "Abominable" && !f.Origin.base && !f.Destroyed
}

// isTargetAbominable - checks if the target has the Abominable feature from Horror
function isTargetAbominable(sc_dir) {
    return sc_dir._features.filter(isFeatureAbominable).length > 0
}

//construct_features - Builds out the list of selectable features for the scanned target, includes support for exotics.
function construct_features(sc_dir, o) {
    let sc_list = ``;
    const isAbominable = isTargetAbominable(sc_dir)
    sc_list += `<p>${isAbominable && o !== "Horror" ? glitchyError(o) : o}</p>`
    let sc_features = sc_dir._features.filter(f => f.Origin.name === o).sort(sort_features)
    sc_features.forEach(i => {
        let sc_name;
        let sc_desc;
        const glitchOut = isAbominable && !isFeatureAbominable(i)
        if (!glitchOut && i.Origin.name === "EXOTIC" && !i.Origin.base) {
            sc_name = "<code class=\"horus--subtle\">UNKNOWN EXOTIC SYSTEM</code>";
            sc_desc = "???";
        }
        else {
            sc_name = i.Name;
            if (i.Effect) {
                sc_desc = i.Effect;
            } else {
                sc_desc = "No description given.";
            }
            if (i.Trigger) {
                sc_desc = `Trigger:${i.Trigger}<br>${sc_desc}`;
            }
        }
        let sc_entry = `<details><summary>${glitchOut ? glitchyError(sc_name) : sc_name}</summary><p>${glitchOut ? glitchyErrorMultiline(sc_desc) : sc_desc}</p></details>`;
        sc_list += sc_entry;
    });
    return sc_list
}

//construct_weapons - Builds out the table of weapons for the scanned target, includes support for exotics.
function construct_weapons(sc_dir, o, sc_tier) {
    let sc_weapons = ``;
    let sc_features = sc_dir._features.filter(f => f.Origin.name === o).sort(sort_features)
    const glitchOut = isTargetAbominable(sc_dir)
    sc_features.forEach(i => {
        let sc_name = ``;
        let sc_desc = ``;
        let sc_entry = ``;
        let sc_range = ``;
        let sc_damage = ``;
        let sc_accuracy = ``;
        if (!i.WepType) { return sc_weapons }
        sc_weapons += `<table>`;
        if (!glitchOut && i.Origin.name === "EXOTIC" && !i.Origin.base) {
            sc_name = "<tr><th><code class=\"horus--subtle\">UNKNOWN EXOTIC WEAPON</code></th></tr>";
            sc_desc = "<tr><td>???</td></tr>";
            sc_entry = sc_name + sc_desc;
        } else {
            sc_name = `<tr><th colspan="4">${glitchOut ? glitchyError(i.Name) : i.Name}</th></tr>`;
            sc_entry += sc_name;
            sc_desc = `<tr>`;
            sc_desc += `<td>+${glitchOut ? glitchyValue() : i.AttackBonus[sc_tier - 1]} ATTACK</td>`;
            if (i.Accuracy[sc_tier - 1]) {
                if (i.Accuracy[sc_tier - 1] > 0) {
                    sc_accuracy = '+' + i.Accuracy[sc_tier - 1] + ' ACCURACY'
                } else {
                    sc_accuracy = '-' + i.Accuracy[sc_tier - 1] + ' DIFFICULTY'
                }
            }
            sc_desc += `<td>${glitchOut ? glitchyError(sc_accuracy) : sc_accuracy}</td>`;
            if (i.Range.length > 0) { i.Range.forEach(r => sc_range += (glitchOut ? glitchyError(r.RangeType) : r.RangeType) + ' ' + (glitchOut ? glitchyValue() : r.Value) + '&nbsp&nbsp&nbsp') }
            sc_desc += `<td>${sc_range}</td>`;
            if (i.Damage.length > 0) { i.Damage[sc_tier - 1].forEach(d => sc_damage += (glitchOut ? glitchyValue() : d.Value) + ' ' + (glitchOut ? glitchyError(d.DamageType) : d.DamageType) + '&nbsp&nbsp&nbsp') }
            sc_desc += `<td>${sc_damage}</td>`;i.Damage
            if (glitchOut) {
                sc_desc += `<td>${glitchyError("NLOADED")}</td>`
            } else if (i.Loaded) { sc_desc += `<td>LOADED</td>` } else { sc_desc += `<td>UNLOADED</td>` }
            if (glitchOut) {
                sc_desc += `<td>${glitchyError("USES:") + " " + glitchyValue() + "/" + glitchyValue()}</td>`
            } else if (i.Uses > 0 && i.BaseLimit > 0) { sc_desc += `<td>USES: ${i.Uses}/${i.BaseLimit}</td>` }
            sc_desc += `<tr>`;
            if (i.Trigger) {
                sc_desc += `<tr><td colspan="6"><details><summary>Trigger</summary><p>${glitchOut ? glitchyErrorMultiline(i.Trigger) : i.Trigger}</p></details></td></tr>`;
            }
            if (i.OnHit) {
                sc_desc += `<tr><td colspan="6"><details><summary>On Hit</summary><p>${glitchOut ? glitchyErrorMultiline(i.OnHit) : i.OnHit}</p></details></td></tr>`;
            }
            if (i.Effect) {
                sc_desc += `<tr><td colspan="6">${glitchOut ? glitchyErrorMultiline(i.Effect) : i.Effect}</td></tr>`;
            }
            if (i.Tags.length > 0) {
                sc_desc += `<tr><td colspan="6"><details><summary>Tags</summary>`;
                i.Tags.forEach((t) => {
                    const text = t.Tag.Name.replace("{VAL}", t.Value)
                    sc_desc += `<p>${glitchOut ? glitchyError(text) : text}</p>`;
                });
                sc_desc += `</details></td></tr>`;
            }
            sc_entry += sc_desc;
        }
        sc_weapons += sc_entry;
        sc_weapons += `</table>`
    });
    return sc_weapons
}

//construct_templates
function construct_templates(sc_dir) {
    let sc_templates = ``;
    let sc_temp = sc_dir._templates;
    const glitchOut = isTargetAbominable(sc_dir)
    if (!sc_temp || sc_temp.length === 0) {
        sc_templates += "<p>NONE</p>";
    } else {
        sc_temp.forEach(i => {
            let sc_entry = `<p>${glitchOut && i.Name !== "Horror" ? glitchyError(i.Name) : i.Name}</p>`;
            sc_templates += sc_entry;
        });
    }

    sc_templates += "<br>";
    return sc_templates
}

function genRandomInt(min, max) {
    return Math.floor(Math.random() * (1 + max - min)) + min
}

function getRandomItem(arr) {
    return arr.length === 0 ? null : arr[genRandomInt(0, arr.length - 1)]
}

function generate_glitch_text(length) {
    const out = []
    let remaining = length
    if (glitchSymbols.length === 0) {
        throw Error("No glitch symbols available")
    }
    while (remaining > 0) {
        const next = getRandomItem(glitchSymbols)
        out.push(next)
        remaining -= next.length
    }
    return out.join('').substring(0, Math.max(0, length + 1))
}

function generate_error_text(length) {
    const out = []
    let remaining = length
    while (remaining > Math.max(0, genRandomInt(errorTextPadding.min, errorTextPadding.max))) {
        const available = errorText.filter(s => s.length <= remaining)
        if (available.length === 0) {
            break;
        }
        const nextText = getRandomItem(available)
        remaining -= nextText.length
        const before = Math.max(0, Math.min(remaining, genRandomInt(errorTextPadding.min, errorTextPadding.max)))
        const beforeText = generate_glitch_text(before)
        remaining -= beforeText.length
        out.push(beforeText)
        out.push(nextText)
    }
    out.push(generate_glitch_text(remaining))
    return out.join("")
}

function escapeHtml(s) {
    return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
}

function glitchyValue() {
    const glitchTextLength = Math.max(1, genRandomInt(glitchNumberLength.min, glitchNumberLength.max))
    return `<code class="horus--subtle">${escapeHtml(generate_glitch_text(glitchTextLength))}</code>`
}

function glitchyError(realText) {
    const errorTextLength = Math.max(1, genRandomInt(realText.length + errorTextVariance.min, realText.length + errorTextVariance.max))
    return `<code class="horus--subtle">${escapeHtml(generate_error_text(errorTextLength))}</code>`
}

const newlineSplitRegex = /<(p|br|div|li|table|tr)>/

function glitchyErrorMultiline(realText) {
    return realText.split(newlineSplitRegex).map(glitchyError).join("<br>")
}

const journalFolder = game.folders.getName(journalFolderName)
if (!journalFolder && journalFolderName.length > 0) {
    try {
        await Folder.create({ name: journalFolderName, type: "JournalEntry" })
    } catch (error) {
        ui.notifications.error(`${journalFolderName} does not exist and must be created manually by a user with permissions to do so.`);
        return;
    }
}

targets.forEach(async (target) => {
    let sc_dir = await target.document.actor.system.derived.mm_promise;
    const glitchOut = isTargetAbominable(sc_dir)
    let hase_table_html = `
    <p><img style="border: 3px dashed #000000; float: left; margin-right: 5px; margin-left: 5px;" src="${target.document.actor.img}" width="30%" height="30%" /></p>
    <div style="color: #000000; width: 65%; float: right; text-align: left;">
    <table>
        <tr>
            <th>HULL</th><th>AGI</th><th>SYS</th><th>ENG</th>
        </tr>
        <tr>
            <td>${glitchOut ? glitchyValue() : sc_dir.Hull || 0}</td><td>${glitchOut ? glitchyValue() : sc_dir.Agi || 0}</td><td>${glitchOut ? glitchyValue() : sc_dir.Sys || 0}</td><td>${glitchOut ? glitchyValue() : sc_dir.Eng || 0}</td>
        </tr>
    </table>
    `
    let stat_table_html = `
    <table>
        <tr>
            <th>Armor</th><th>HP</th><th>Heat</th><th>Speed</th>
        </tr>
        <tr>
            <td>${glitchOut ? glitchyValue() : sc_dir.Armor}</td><td>${glitchOut ? glitchyValue() : sc_dir.CurrentHP}/${glitchOut ? glitchyValue() : sc_dir.MaxHP}</td><td>${glitchOut ? glitchyValue() : sc_dir.CurrentHeat || 0}/${glitchOut ? glitchyValue() : sc_dir.HeatCapacity || 0}</td><td>${glitchOut ? glitchyValue() : sc_dir.Speed}</td>
        </tr>
        <tr>
            <th>Evasion</th><th>E-Def</th><th>Save</th><th>Sensors</th>
        </tr>
        <tr>
            <td>${glitchOut ? glitchyValue() : sc_dir.Evasion}</td><td>${glitchOut ? glitchyValue() : sc_dir.EDefense}</td><td>${glitchOut ? glitchyValue() : sc_dir.SaveTarget}</td><td>${glitchOut ? glitchyValue() : sc_dir.SensorRange}</td>
        </tr>
        <tr>
            <th>Size</th><th>Activ</th><th>Struct</th><th>Stress</th>
        </tr>
        <tr>
            <td>${glitchOut ? glitchyValue() : sc_dir.Size}</td><td>${glitchOut ? glitchyValue() : sc_dir.Activations || 1}</td><td>${glitchOut ? glitchyValue() : sc_dir.CurrentStructure || 0}/${glitchOut ? glitchyValue() : sc_dir.MaxStructure || 0}</td><td>${glitchOut ? glitchyValue() : sc_dir.CurrentStress || 0}/${glitchOut ? glitchyValue() : sc_dir.MaxStress || 0}</td>
        </tr>
    </table>
    `
    console.log(sc_dir)
    let sc_class = (!sc_dir._classes || sc_dir._classes.length === 0) ? "NONE" : glitchOut ? glitchyError(sc_dir._classes[0].Name) : sc_dir._classes[0].Name
    let sc_tier = sc_dir.Tier ? sc_dir.Tier : 0
    let sc_templates = construct_templates(sc_dir)
    let sc_list = ``
    let sc_weapons = ``
    if (!sc_dir._features || sc_dir._features.length === 0) {
        sc_list += "<p>NONE</p>";
        sc_weapons += "<p>NONE</p>";
    } else {
        let sc_origins = [];
        sc_dir._features.forEach(f => {
            let origin = f.Origin.name;
            if (!sc_origins.includes(origin)) {
                sc_origins.push(origin);
            }
        });
        sc_origins.forEach(o => {
            sc_list += construct_features(sc_dir, o);
            sc_weapons += construct_weapons(sc_dir, o, sc_tier);
        });
    }

    // ChatMessage.create({
    //     user: game.user._id,
    //     content: `<h2>Scan results: ${sc_dir.Name}</h2>` + `<h3>Class: ${sc_class}, Tier ${sc_tier}</h3>`  + hase_table_html + stat_table_html + `<h3>Templates:</h3>` + sc_templates + `<h3>Systems:</h3>` + sc_list
    // });

    var scanContent = `<h2>Scan results: ${glitchOut ? glitchyError(sc_dir.Name) : sc_dir.Name}</h2>` + `<h3>Class: ${sc_class}, Tier ${glitchOut ? glitchyValue() : sc_tier}</h3>` + hase_table_html + stat_table_html + `</div><div style="color: #000000; width: 100%; float: right; text-align: left;"><h3>Weapons:</h3>` + sc_weapons + `<h3>Templates:</h3>` + sc_templates + `<h3>Systems:</h3>` + sc_list + `</div>`

    //This checks and updates the scan entry for the target(s) if a single scan entry exists in the specified folder for the target(s) along with the updateExisting flag.
    //If either are false then this creates a new scan entry.

    let rootScanFolder = journalFolderName ? journalFolder : game.journal;
    let scanFolderId = journalFolderName ? journalFolder.id : null;

    let scanObj = {};

    scanObj.folder = scanFolderId;

    let scanEntry;

    let matchingJournalEntries = rootScanFolder.contents.filter(e => e.name.match(sc_dir.Name));

    if (matchingJournalEntries.length === 1 && updateExisting === true) {
        console.log("Updating an existing scan")
        scanObj.name = matchingJournalEntries[0].name;
        scanObj._id = matchingJournalEntries[0]._id;
        scanObj.text = {};
        scanObj.text.content = scanContent;
        scanEntry = game.journal.getName(scanObj.name);
        let scanPage = scanEntry.pages.getName(scanObj.name)
        await scanPage.update(scanObj);
    } else {
        console.log("Creating a new scan")
        let scanCount = zeroPad(rootScanFolder.contents.filter(e => e.name.startsWith(nameTemplate)).length + startingNumber, numberLength);
        scanObj.name = nameTemplate + scanCount + ` - ` + sc_dir.Name;
        scanObj.content = scanContent;
        scanEntry = await JournalEntry.create(scanObj);
    }

    scanEntry.update({ permission: { default: permissionLevel } });
    scanEntry.sheet.render(true);

})

Adds functionality to the Scan (Journal) macro which enables it to render glitchy, distorted unreadable garbage in place of the normal results when the scan target is Abominable (from the Horror template from Dustgrave).

Some information is still conveyed - the number of templates, the number of systems from each one, the number of weapons, and of course that it is a Horror and it has the Abominable feature. Additionally, the glitchy text roughly matches the length of the original text. 

If Abominable is marked as destroyed, the scan will work as normal.
@Eranziel
Copy link
Owner

Unfortunately I'm not taking PRs to master until after the 2.0 release is done. There's just too much difference between master and the 2.0 branch.

I'll leave this PR open, though, and try to draw some inspiration from here when we update the macros for 2.0.

@BoltsJ
Copy link
Collaborator

BoltsJ commented Jul 20, 2024

Hi! 2.0 is out now and we have a guide for contributing macros if you're feeling willing to update this for the new version.

https://github.com/Eranziel/foundryvtt-lancer/blob/master/docs/macro_guide.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants