Skip to content
This repository was archived by the owner on Oct 21, 2022. It is now read-only.
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 162 additions & 120 deletions shuffle.js
Original file line number Diff line number Diff line change
@@ -1,141 +1,183 @@
//@ts-check
'use strict';
const fs = require('fs')
const plist = require('plist');
const assets = require('./assets.json')

try { // god-tier crash prevention system

Array.prototype.shuffle = function() {
let length = this.length; let unshuffled = this; let shuffled = [];
while (shuffled.length !== length) {
let index = Math.floor(Math.random() * unshuffled.length);
shuffled.push(unshuffled[index]);
unshuffled = unshuffled.filter((x, y) => y !== (index))}
return shuffled;
}

function plistToJson(file) {
let data = plist.parse(file)
for (let key in data.frames) {
let fileData = data.frames[key];
for (let innerKey in fileData) {
if (typeof fileData[innerKey] == 'string') {
if (!fileData[innerKey].length) delete fileData[innerKey]
else fileData[innerKey] = JSON.parse(fileData[innerKey].replace(/{/g, '[').replace(/}/g, ']'));
/**
* returns a pseudo-random 32bit unsigned integer
* in the interval [0, `n`)
*/
const randU32 = (n = 2**32) => Math.random() * n >>> 0;

/**
* https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
* https://github.com/steventhomson/array-generic-shuffle/blob/master/shuffle.js
* @param {any[]} a
*/
const shuffle = a => {
let len = a.length;
while (len > 0) {
const i = randU32(len);
len--;
[a[len], a[i]] = [a[i], a[len]]; // swap
}
};

/**
* get unique/distinct elements (same order)
* @param {any[]} arr
*/
const undupe = arr => arr.filter((x, y) => arr.indexOf(x) == y);

/**
* convert plist string to JSON, then JSON to `Object`
* @param {string} file
* @return {import('plist').PlistObject}
*/
const plistToJson = file => {
const {frames: datFrames} = plist.parse(file);
// not using `Object.values`, because we want to mutate in-place
for (const out_k in datFrames) {
const fileData = datFrames[out_k];
for (const in_k in fileData) {
const fdik = fileData[in_k];
if (typeof fdik == 'string') {
if (fdik.length == 0) delete fileData[in_k]
else fileData[in_k] = JSON.parse(fdik.replace(/{/g, '[').replace(/}/g, ']'));
}
}}
return data.frames
return datFrames
}

if (!fs.existsSync('./pack')) fs.mkdirSync('./pack');

function glow(name) { return name.replace("_001.png", "_glow_001.png") }
function undupe (arr) { return arr.filter((x, y) => arr.indexOf(x) == y) }
//function spriteRegex(name) { return new RegExp(`(<key>${name.replace(".", "\\.")}<\/key>\\s*)(<dict>(.|\\n)+?<\\/dict>)`) }
let iconRegex = /^.+?_(\d+?)_.+/

let forms = assets.forms
let sheetList = Object.keys(assets.sheets)
let glowName = sheetList.filter(x => x.startsWith('GJ_GameSheetGlow'))

// newlines/CRs are usually present in text files, strip them out so they aren't part of the pathname
let gdPath = process.argv[2] ?? fs.readFileSync('directory.txt', 'utf8').replace(/[\n\r]/g, '')

if (!fs.existsSync(gdPath)) throw "Couldn't find your GD directory! Make sure to enter the correct file path in directory.txt"
let glowPlist = fs.readFileSync(`${gdPath}/${glowName[0]}.plist`, 'utf8')
let sheetNames = sheetList.filter(x => !glowName.includes(x))
let resources = fs.readdirSync(gdPath)

let plists = []
let sheets = []
let glowBackups = []
let glowSheet = plistToJson(glowPlist)

resources.forEach(x => {
if (x.startsWith('PlayerExplosion_') && x.endsWith('-uhd.plist')) sheetNames.push(x.slice(0, -6))
})

sheetNames.forEach(x => {
let file = fs.readFileSync(`${gdPath}/${x}.plist`, 'utf8')
plists.push(file)
try { sheets.push(plistToJson(file)) }
catch(e) { throw `Error parsing ${x}.plist - ${e.message}` }
})

sheets.forEach((gameSheet, sheetNum) => {
let plist = plists[sheetNum]
let name = sheetNames[sheetNum]
if (!name.startsWith('PlayerExplosion_')) console.log("Shuffling " + name)
else if (name == "PlayerExplosion_01-uhd") console.log("Shuffling death effects")

let sizes = {}
Object.keys(gameSheet).forEach(x => {
let obj = gameSheet[x]
obj.name = x
if (sheetNum == sheetNames.findIndex(y => y.startsWith('GJ_GameSheet02')) && forms.some(y => x.startsWith(y))) {
let form = forms.find(y => x.startsWith(y))
if (!sizes[form]) sizes[form] = [obj]
else sizes[form].push(obj)
}
else {
let sizeDiff = assets.sheets[name] || 30
let size = obj.textureRect[1].map(x => Math.round(x / sizeDiff) * sizeDiff).join()
if (name.startsWith('PlayerExplosion')) size = "deatheffect"
if (!sizes[size]) sizes[size] = [obj]
else sizes[size].push(obj)
}
/** working directory */
const wd = './pack/';

try { // god-tier crash prevention system

fs.mkdirSync(wd, { recursive: true, mode: 0o766 });

const glow = (/**@type {string}*/ name) => name.replace("_001.png", "_glow_001.png");
//const spriteRegex = name => new RegExp(`(<key>${name.replace(".", "\\.")}<\/key>\\s*)(<dict>(.|\\n)+?<\\/dict>)`);
const iconRegex = /^.+?_(\d+?)_.+/

const
{forms} = assets,
sheetList = Object.keys(assets.sheets),
glowName = sheetList.filter(x => x.startsWith('GJ_GameSheetGlow'));

// newlines/CRs are usually present in text files, strip them out so they aren't part of the pathname
const gdPath = process.argv[2] ?? fs.readFileSync('directory.txt', 'utf8').replace(/[\n\r]/g, '')

if (!fs.existsSync(gdPath))
throw "Couldn't find your GD directory! Make sure to enter the correct file path in directory.txt"
let glowPlist = fs.readFileSync(`${gdPath}/${glowName[0]}.plist`, 'utf8')
const sheetNames = sheetList.filter(x => !glowName.includes(x))
const resources = fs.readdirSync(gdPath)

/**@type {string[]}*/
const plists = []
const sheets = []
const glowBackups = []
const glowSheet = plistToJson(glowPlist)

resources.forEach(x => {
if (x.startsWith('PlayerExplosion_') && x.endsWith('-uhd.plist'))
sheetNames.push(x.slice(0, -6)) // -6 removes ".plist", efficiently
})

Object.keys(sizes).forEach(obj => {
let objects = sizes[obj]
if (objects.length == 1) return delete sizes[obj]
let iconMode = forms.includes(obj)
let oldNames = objects.map(x => x.name)
if (iconMode) oldNames = undupe(oldNames.map(x => x.replace(iconRegex, "$1")))
let newNames = oldNames.shuffle()
if (iconMode) {
let iconList = {}
oldNames.forEach((x, y) => iconList[x] = newNames[y])
newNames = iconList
}

oldNames.forEach((x, y) => {
let newName = newNames[iconMode ? x : y]
if (iconMode) {
plist = plist.replace(new RegExp(`<key>${obj}_${x}_`, "g"), `<key>###${obj}_${newName}_`)
glowPlist = glowPlist.replace(`<key>${obj}_${x}_`, `<key>###${obj}_${newName}_`)

sheetNames.forEach(x => {
const file = fs.readFileSync(`${gdPath}/${x}.plist`, 'utf8')
plists.push(file)
try { sheets.push(plistToJson(file)) }
catch(e) { throw `Error parsing ${x}.plist - ${e.message}` }
})

sheets.forEach((gameSheet, sheetNum) => {
let plist = plists[sheetNum]
let name = sheetNames[sheetNum]
if (!name.startsWith('PlayerExplosion_')) console.log("Shuffling " + name)
else if (name == "PlayerExplosion_01-uhd") console.log("Shuffling death effects")

let sizes = {}
Object.keys(gameSheet).forEach(x => {
let obj = gameSheet[x]
obj.name = x
if (sheetNum == sheetNames.findIndex(y => y.startsWith('GJ_GameSheet02')) && forms.some(y => x.startsWith(y))) {
let form = forms.find(y => x.startsWith(y))
if (!sizes[form]) sizes[form] = [obj]
else sizes[form].push(obj)
}
else {
plist = plist.replace(`<key>${x}</key>`, `<key>###${newName}</key>`)
if (glowSheet[glow(x)]) {
glowBackups.push(glow(x))
glowPlist = glowPlist.replace(`<key>${glow(x)}</key>`, `<key>###${glow(newName)}</key>`)
}
/**@type {number}*/
let sizeDiff = assets.sheets[name] || 30
let size = obj.textureRect[1].map(x => Math.round(x / sizeDiff) * sizeDiff).join()
if (name.startsWith('PlayerExplosion')) size = "deatheffect"
if (!sizes[size]) sizes[size] = [obj]
else sizes[size].push(obj)
}
})

Object.keys(sizes).forEach(k => {
/**@type {{name: string}[]}*/
const objects = sizes[k]
if (objects.length == 1) return delete sizes[k]
const iconMode = forms.includes(k)
let oldNames = objects.map(x => x.name)
if (iconMode) oldNames = undupe(oldNames.map(x => x.replace(iconRegex, "$1")))
let newNames = shuffle(oldNames)
if (iconMode) {
let iconList = {}
oldNames.forEach((x, y) => iconList[x] = newNames[y])
newNames = iconList
}

oldNames.forEach((x, y) => {
let newName = newNames[iconMode ? x : y]
if (iconMode) {
plist = plist.replace(new RegExp(`<key>${k}_${x}_`, "g"), `<key>###${k}_${newName}_`)
glowPlist = glowPlist.replace(`<key>${k}_${x}_`, `<key>###${k}_${newName}_`)
}
else {
plist = plist.replace(`<key>${x}</key>`, `<key>###${newName}</key>`)
if (glowSheet[glow(x)]) {
glowBackups.push(glow(x))
glowPlist = glowPlist.replace(`<key>${glow(x)}</key>`, `<key>###${glow(newName)}</key>`)
}
}
})
})
plist = plist.replace(/###/g, '')
fs.writeFileSync(wd + sheetNames[sheetNum] + '.plist', plist, 'utf8')
})
plist = plist.replace(/###/g, "")
fs.writeFileSync('./pack/' + sheetNames[sheetNum] + '.plist', plist, 'utf8')
})

console.log("Shuffling misc textures")
let specialGrounds = []
assets.sprites.forEach(img => {
let spriteMatch = img.split("|")
let foundTextures = resources.filter(x => x.match(new RegExp(`^${spriteMatch[0].replace("#", "\\d+?")}-uhd\\.${spriteMatch[1] || "png"}`)))
console.log("Shuffling misc textures")
/**@type {string[]}*/
const specialGrounds = []
assets.sprites.forEach(img => {
const spriteMatch = img.split("|")
let foundTextures = resources.filter(x => x.match(new RegExp(`^${spriteMatch[0].replace("#", "\\d+?")}-uhd\\.${spriteMatch[1] || "png"}`)))

if (spriteMatch[2] == "*") specialGrounds = specialGrounds.concat(foundTextures.map(x => x.slice(0, 15)))
if (spriteMatch[2] == "g1") foundTextures = foundTextures.filter(x => !specialGrounds.some(y => x.startsWith(y)))
if (spriteMatch[2] == "g2") foundTextures = foundTextures.filter(x => specialGrounds.some(y => x.startsWith(y)))
if (spriteMatch[2] == "*") specialGrounds.push(...foundTextures.map(x => x.slice(0, 15))) // in-place `concat`
if (spriteMatch[2] == "g1") foundTextures = foundTextures.filter(x => !specialGrounds.some(y => x.startsWith(y)))
if (spriteMatch[2] == "g2") foundTextures = foundTextures.filter(x => specialGrounds.some(y => x.startsWith(y)))

let shuffledTextures = foundTextures.shuffle()
foundTextures.forEach((x, y) => fs.copyFileSync(`${gdPath}/${x}`, `./pack/${shuffledTextures[y]}`))
})
let shuffledTextures = shuffle(foundTextures)
foundTextures.forEach((x, y) => fs.copyFileSync(`${gdPath}/${x}`, wd + shuffledTextures[y]))
})

let emptyDict = glowPlist.match(/<dict>\s*<key>aliases<\/key>(.|\n)+?<\/dict>/)[0].replace(/{\d+,\d+}/g, "{0, 0}")
let mappedBackups = glowBackups.reverse().map(x => `<key>${x}</key>${emptyDict}`).join("")
glowPlist = fs.writeFileSync('./pack/GJ_GameSheetGlow-uhd.plist', glowPlist.replace(/###/g, "").replace(/<dict>\s*<key>frames<\/key>\s*<dict>/g, "$&" + mappedBackups), 'utf8')
console.log("Randomization complete!")
let emptyDict = glowPlist.match(/<dict>\s*<key>aliases<\/key>(.|\n)+?<\/dict>/)[0].replace(/{\d+,\d+}/g, "{0, 0}")
let mappedBackups = glowBackups.reverse().map(x => `<key>${x}</key>${emptyDict}`).join('')
glowPlist = fs.writeFileSync(wd + 'GJ_GameSheetGlow-uhd.plist', glowPlist.replace(/###/g, "").replace(/<dict>\s*<key>frames<\/key>\s*<dict>/g, "$&" + mappedBackups), 'utf8')
console.log("Randomization complete!")

}

catch(e) { console.log(e); fs.writeFileSync('crash_log.txt', e.stack ? `Something went wrong! Send this error to Colon and he'll get around to fixing it at some point.\n\n${e.stack}` : e, 'utf8') }
catch(e) {
console.error(e);
fs.writeFileSync(
'crash_log.txt',
e.stack ? `Something went wrong! Send this error to Colon and he'll get around to fixing it at some point.\n\n${e.stack}` : e,
'utf8'
)
}