Skip to content

Instantly share code, notes, and snippets.

@NamelessCoder
Last active April 26, 2024 21:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe to your computer and use it in GitHub Desktop.
Save NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe to your computer and use it in GitHub Desktop.
Sixty-Four Mods

Sixty-Four Mod Collection

These are mods for the game Sixty-Four, an idle/clicker/factory game. You can find the game on Steam: https://store.steampowered.com/app/2659900/Sixty_Four/

All of the mods in this collection require the "Mod Autoloader" as manager. That is the mod provided in the file autoloader.js. To install that mod, follow the official guide on the link above, place the autoloader.js file in the mods folder and include that mod - and only that mod! - in the index.html file. The other mods are then loaded automatically.

In the game you will then have a "MODS" menu item where you can configure the mods' settings and enable/disable the mods.

If you want to create your own mods and have it use the autoloader, there's an example of how to write a mod in the demo.js file. You can use that class as a skeleton - just change the class name, label, description etc. to suit your own needs.

If you experience bugs or have feature requests you are welcome to drop a comment here or find me on the Sixty-Four Discord server, https://discord.gg/2Z6tGTpe, under the name "Ziltoid".

/*
* Sixty-Four Mod: Mod Autoloader
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* Temporary implementation of proposed feature for game. Rather than
* require each individual mod to be included in the "index.html" file,
* this mod can be the only included mod and automatically load all
* other mods.
*
* The autoloader also adds a new menu item called "MODS" which contains
* an interface to edit all settings and enabled/disabled state of mods.
*
* A fully annotated example of how to write a mod can be found at:
*
* https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-demo-js
*/
class Mod {
version = '0.0.0';
settings = {};
styles = '';
label = ''
description = '';
dependencies = {};
conflicts = {};
originalMethods = {};
constructor(configuredOptions, mods) {
this.configuredOptions = configuredOptions;
this.mods = mods;
this.label = this.label.length > 0 ? this.label : this.getName();
};
initializeOptions() {
const settings = this.getSettings();
for (const settingName in settings) {
this.configuredOptions[settingName] = this.configuredOptions[settingName] ?? settings[settingName]?.default ?? null;
}
};
getOptions() {
return this.configuredOptions ?? {};
}
getSettings() {
return this.settings ?? {};
};
getName() {
return this.constructor.name;
};
getLabel() {
return this.label ?? this.getName();
};
getDescription() {
return this.description ?? this.getDescription();
};
getVersion() {
return this.version ?? '0.0.0';
};
getStyles() {
return this.styles ?? '';
};
getDependencies() {
return this.dependencies ?? {};
};
getConflicts() {
return this.conflicts ?? {};
};
getMethodReplacements() {
return [];
};
setOriginalMethod(prototypeName, methodName, method) {
this.originalMethods[prototypeName] = this.originalMethods[prototypeName] ?? {};
this.originalMethods[prototypeName][methodName] = method;
}
isEnabled() {
return this.enabled;
};
init() {
};
};
class LoadedMod {
settings = {};
settingTemplates = {};
label = '';
enabled = false;
description = '';
version = '0.0.0';
legacy = false;
error = null;
constructor(modInstance, savedSettings) {
if (!modInstance) {
return;
}
this.settings = modInstance.getOptions();
this.settingTemplates = modInstance.getSettings();
this.label = modInstance.getLabel();
this.enabled = savedSettings.enabled ?? false;
this.description = modInstance.getDescription();
}
}
class LoadedLegacyMod extends LoadedMod {
constructor(modName) {
super();
this.label = modName;
this.legacy = true;
}
}
class LoadedModWithError extends LoadedMod {
constructor(modName, error) {
super();
this.label = modName + ' (error while loading)';
this.legacy = true;
this.error = error;
this.description = error.message;
}
}
class Loader {
constructor() {
}
loadModules() {
const fs = require("fs");
let date = new Date();
let timestamp = date.getTime().toString();
const fromStorage = window.localStorage.getItem('mods');
const saved = JSON.parse(fromStorage ? fromStorage : '{}');
const mods = {};
const modObjects = {};
fs.readdirSync(`${__dirname}/mods`).filter(e => e.substr(-3) === '.js' && e !== 'autoloader.js').forEach(entry => {
let modName = entry.substr(0, entry.length - 3);
let hasSavedSetting = typeof saved !== 'undefined' && typeof saved[modName] !== 'undefined';
(function() {
const configuration = saved[modName] ?? {};
try {
const mod = require(`${__dirname}/mods/` + entry);
if (typeof mod === 'function') {
const classNames = {};
classNames[modName] = mod;
const instance = new classNames[modName](configuration.settings ?? {}, mods);
modObjects[modName] = instance;
modObjects[modName].initializeOptions();
mods[modName] = new LoadedMod(instance, configuration);
} else {
mods[modName] = new LoadedLegacyMod(modName);
}
} catch (error) {
mods[modName] = new LoadedModWithError(modName, error);
}
})();
});
for (const modName in mods) {
if (!mods[modName].enabled) {
continue;
}
const mod = modObjects[modName];
if (typeof mod !== 'undefined') {
const style = mod.getStyles();
if (style.length > 0) {
document.head.appendChild(document.createElement("style")).innerHTML = style;
}
const methodOverrides = mod.getMethodReplacements();
methodOverrides.forEach(reg => {
mod.setOriginalMethod(reg.class.name, reg.method, reg.class['prototype'][reg.method]);
reg.class['prototype'][reg.method] = reg.replacement;
});
mod.init();
}
}
return mods;
}
}
(function () {
const loader = new Loader();
const mods = loader.loadModules();
let _game_init = Game.prototype.init;
let _splash_init = Splash.prototype.init;
let _splash_show = Splash.prototype.show;
let _splash_close = Splash.prototype.close;
Game.prototype.init = function() {
_game_init.call(this);
// Fill the global reference to this current instance to allow mods to access it as they are loaded.
if (typeof game !== 'object') {
window.game = this;
}
this.mods = mods;
};
Splash.prototype.init = function() {
_splash_init.call(this);
this.modScreen = document.createElement('div');
this.modScreen.classList.add('achievementSplash');
this.modScreen.id = 'mod-editor';
this.modScreen.innerHTML = '<h1>MOD SETTINGS</h1><p class="warning">Save your game and take a backup before proceeding! Change at your own risk!</p>';
const menu = this.element.getElementsByClassName('menu')[0];
const quit = menu.removeChild(menu.lastChild);
const reset = menu.removeChild(menu.lastChild);
const modShowButton = document.createElement('div');
modShowButton.classList.add('menuItem');
modShowButton.innerHTML = 'MODS';
modShowButton.onclick = _ => {
this.modScreen.style.display = 'block';
}
menu.append(modShowButton);
menu.append(reset);
menu.append(quit);
this.deModButton = document.createElement(`div`)
this.deModButton.classList.add(`gloryButton`)
this.deModButton.innerHTML = this.texts.deglory
this.deModButton.style.display = `none`
this.modScreen.append(this.deModButton)
modShowButton.onclick = _=>{
console.log('Showing mods');
this.modScreen.style.left = 0
this.deModButton.style.display = `block`
}
this.deModButton.onclick = _=>{
this.modScreen.style.left = `100%`
this.deModButton.style.display = `none`
}
this.applyButton = document.createElement('button');
this.applyButton.innerHTML = 'APPLY SETTINGS';
this.applyButton.onclick = _ => {
window.localStorage.setItem('mods', JSON.stringify(mods));
document.location.reload();
}
this.settings = document.createElement('div');
this.settings.classList.add('settings');
for (const modName in mods) {
const table = document.createElement('table');
//console.log(mods[vendorName][modName]);
const tr = document.createElement('tr');
tr.innerHTML = '<td class="header" colspan="3">' + mods[modName].label + '</td>';
table.append(tr);
if (mods[modName].description.length > 0) {
const descriptionTr = document.createElement('tr');
descriptionTr.innerHTML = '<td class="mod-description" colspan="3">' + mods[modName].description + '</td>';
table.append(descriptionTr);
}
const fieldId = 'f_' + modName + '___enabled';
if (!mods[modName].error) {
const enabled = document.createElement('tr');
enabled.innerHTML = '<td class="label"><label for="' + fieldId + '">Enabled?</td><td class="setting"></td>';
const enabledCheckbox = document.createElement('input');
enabledCheckbox.type = 'checkbox';
enabledCheckbox.value = 1;
enabledCheckbox.id = fieldId;
enabledCheckbox.checked = mods[modName].enabled;
enabledCheckbox.onchange = e => {
if (mods[modName].legacy) {
enabledCheckbox.checked = true;
return true;
} else {
mods[modName].enabled = enabledCheckbox.checked;
}
};
if (mods[modName].legacy) {
enabledCheckbox.checked = true;
enabledCheckbox.disabled = true;
}
enabled.lastChild.append(enabledCheckbox);
if (mods[modName].legacy) {
const description = document.createElement('td');
description.classList.add('description');
description.innerHTML = 'Legacy mod - cannot disable. Delete from mods folder to disable.';
enabled.append(description);
}
table.append(enabled);
}
for (const settingName in mods[modName].settingTemplates ?? {}) {
const setting = mods[modName].settingTemplates[settingName];
const currentValue = mods[modName].settings[settingName] ?? setting.default ?? null;
//console.log(setting, mods[modName].settings, currentValue);
const fieldId = 'f_' + modName + '_' + settingName;
const tr = document.createElement('tr');
table.append(tr);
const settingLabel = document.createElement('td');
settingLabel.classList.add('label');
settingLabel.innerHTML = '<label for="' + fieldId + '">' + setting.label + '</label>';
tr.append(settingLabel);
const settingValue = document.createElement('td');
settingValue.classList.add('setting');
let input;
//console.log('Current value of ' + settingName + ' = ' + currentValue);
//console.log(settingName, typeof currentValue);
if (typeof setting.options === 'object') {
input = document.createElement('select');
input.onchange = _ => {
console.log(input.children[input.selectedIndex].value);
mods[modName].settings[settingName] = input.children[input.selectedIndex].value;
}
for (option in setting.options) {
const item = document.createElement('option');
let itemValue = null;
if (typeof setting.options[option] === 'object') {
item.innerHTML = typeof setting.options[option][1] !== 'undefined'
? setting.options[option][1]
: setting.options[option][0];
itemValue = setting.options[option][0];
} else {
item.innerHTML = setting.options[option];
itemValue = setting.options[option];
}
if (itemValue === currentValue) {
item.selected = true;
}
item.value = itemValue;
input.appendChild(item);
}
} else {
switch (typeof currentValue) {
case 'boolean':
input = document.createElement('input');
input.type = 'checkbox';
input.value = 1;
input.checked = currentValue;
input.onchange = _ => {
mods[modName].settings[settingName] = input.checked;
}
break;
case 'number':
default:
input = document.createElement('input');
input.classList.add(typeof currentValue);
input.value = currentValue;
input.onchange = (e) => {
let value = e.srcElement.value;
if (typeof value === 'string') {
if (parseInt(value).toString() === value.toString()) {
value = parseInt(value);
} else if (parseFloat(value).toString() === value.toString()) {
value = parseFloat(value);
}
}
mods[modName].settings[settingName] = value;
};
break;
}
}
input.id = fieldId;
settingValue.append(input);
tr.append(settingValue);
if (typeof setting.description !== 'undefined') {
const description = document.createElement('td');
description.classList.add('description');
description.innerHTML = setting.description;
tr.append(description);
}
}
const space = document.createElement('tr');
space.innerHTML = '<td class="space" colspan="2"></td>';
table.append(space);
this.settings.append(table);
}
this.warningText = document.createElement('p');
this.warningText.classList.add('warning');
this.warningText.innerText = 'Applying settings will force-reload the game without saving. Any progress since last save is lost!';
this.modScreen.append(this.settings);
this.modScreen.append(this.applyButton);
this.modScreen.append(this.warningText);
document.body.appendChild(this.modScreen);
};
Splash.prototype.show = function() {
_splash_show.call(this);
document.body.appendChild(this.modScreen);
};
Splash.prototype.close = function() {
_splash_close.call(this);
document.body.removeChild(this.modScreen);
this.modScreen.style.left = '100%';
this.deModButton.style.display = 'none';
};
document.head.appendChild(document.createElement('style')).innerText = `
#mod-editor {
color: white;
font: 24px 'Montserrat';
display: block;
text-align: center;
}
#mod-editor h1 {
letter-spacing: 6px;
}
#mod-editor .settings {
width: 50%;
margin: 0px auto 0px auto;
}
#mod-editor input,
#mod-editor select {
font: 24px 'Montserrat';
}
#mod-editor select {
width: 80px;
}
#mod-editor input.number {
width: 40px;
}
#mod-editor input.string,
#mod-editor select {
width: 80px;
}
#mod-editor input[type="checkbox"] {
height: 24px;
width: 24px;
accent-color: #333;
}
#mod-editor table {
width: 100%;
margin-bottom: 2em;
margin: 0px auto 0px auto;
}
#mod-editor table td {
padding: 0.25em;
vertical-align: top;
}
#mod-editor table td.header {
text-align: center;
font-size: 40px;
}
#mod-editor table td.space {
height: 16px;
}
#mod-editor table td.label {
text-align: right;
width: 50%;
}
#mod-editor td.description,
#mod-editor td.mod-description {
color: #666;
font-style: italic;
text-align: left;
}
#mod-editor td.mod-description {
text-align: center;
}
#mod-editor table td.setting {
text-align: left;
width: 24px;
}
#mod-editor button {
color: white;
display: 'block';
width: 250px;
font-size: 24px;
border: none;
background-color: black;
padding: 0.5em;
}
#mod-editor button:hover {
color: silver;
}
#mod-editor .warning {
color: #cc6666;
}
`;
})();
/*
* Sixty-Four Mod: Build Upgrades
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* ----------------------------------------------
*
* REQUIRES THE MOD AUTOLOADER
* See https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-_readme-md
*
* ----------------------------------------------
*
* Allows building buildings which are upgrades to other buildings, without
* first building the lower-tier building - with the exception of "one-off"
* buildings like the recycling tower, streaming tower, etc.
*
* Also does not allow building Hollow Flower or Hollow Fruit without a Hollow
* Stone present, but does allow Hollow Fruit directly on top of Hollow Stone.
*/
module.exports = class BuildUpgrades extends Mod
{
// Standard metadata. All of these are optional but a minimum of "label" and "version" should be specified.
label = 'Build Upgrades';
description = 'Allows building upgrade buildings without first building the lower-tier building, with the ' +
'exception of "one-off" buildings like the fill director, streaming tower, etc.';
version = '1.0.0';
getMethodReplacements() {
const self = this;
const placeItem = function() {
const price = this.getRealPrice(this.itemInHand.name);
this.requestResources(price, this.hoveredCell, false, true);
this.addEntity(this.itemInHand.name, this.hoveredCell);
this.stats.machinesBuild++;
this.processMousemove();
if (!this.canAfford(this.itemInHand?.name)) {
delete this.itemInHand;
} else {
this.pickupItem(this.itemInHand.name);
}
};
return [
{
class: Game,
method: 'processClick',
replacement: function(e) {
if (this.transportedEntity) {
return self.originalMethods.Game.processClick.call(this, e);
}
const ok = this.itemInHand && this.hoveredCell && this.canAfford(this.itemInHand.name) && !(this.itemInHand.eraser && (this.hoveredEntity instanceof Pump || this.hoveredEntity instanceof Gradient) && ((this.entitiesInGame[`pump`] || 0) + (this.entitiesInGame[`pump2`] || 0) + (this.entitiesInGame[`gradient`] || 0) < 2))
if (!ok) {
return self.originalMethods.Game.processClick.call(this, e);
}
// Special case: allow building a Hollow Fruit directly on top of a Hollow Stone withou first
// building the Hollow Flower that is normally required.
const isPlacingFruitOrFlower = this.itemInHand.name === 'flower' || this.itemInHand.name === 'fruit';
const isPlacingFruitOrFlowerOnStone = this.hoveredEntity?.name === 'hollow' && isPlacingFruitOrFlower;
// Alternative allowance before letting the original method attempt to place a building.
// If we are placing an upgrade and it isn't an upgrade for a one-off building, allow the
// building to be constructed and DO NOT call the original click handling method.
const isPlacingUpgradeOnBlank = !this.hoveredEntity && !this.itemInHand.eraser && !this.codex.entities[this.itemInHand.name].onlyone && this.codex.entities[this.itemInHand.name].isUpgradeTo;
if (isPlacingFruitOrFlowerOnStone || (isPlacingUpgradeOnBlank && !isPlacingFruitOrFlower)) {
if (isPlacingFruitOrFlowerOnStone) {
this.clearCell(this.hoveredCell);
}
placeItem.call(this);
} else {
return self.originalMethods.Game.processClick.call(this, e);
}
}
},
{
class: Game,
method: 'processMousemove',
replacement: function(e, dxy) {
self.originalMethods.Game.processMousemove.call(this, e, dxy);
const isHoveringFruitOnStone = this.hoveredEntity?.name === 'hollow' && this.itemInHand?.name === 'fruit';
const isPlacingUpgradeOnBlank = !this.hoveredEntity && !this.itemInHand?.eraser && !this.codex.entities[this.itemInHand?.name]?.onlyone && this.codex.entities[this.itemInHand?.name]?.isUpgradeTo;
if ((isHoveringFruitOnStone || isPlacingUpgradeOnBlank) && this.canAfford(this.itemInHand.name)) {
this.canPlace = true;
}
}
}
]
}
}
/*
* Sixty-Four Mod: Console
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* ----------------------------------------------
*
* REQUIRES THE MOD AUTOLOADER
* See https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-_readme-md
*
* ----------------------------------------------
*
* Adds a console feature to the game. Pressing "C" while in the game
* opens the console. Type "help" to get a list of supported commands.
* Type "help command" (for example: "help give") to get a description
* of what a given command can do.
*/
module.exports = class Console extends Mod
{
label = 'In-game Console';
description = 'Adds a console that lets you execute various commands. Press "C" or "Enter" to open.';
styles = `
#console, #console-help {
display: none;
background-color: black;
border-color: silver;
color: white;
font-family: monospace;
border-radius: 0;
font-size: 16px;
width: 700px;
padding: 10px;
position: absolute;
margin-left: -340px;
left: 50%;
z-index: 100;
}
#console {
height: 25px;
bottom: 10%;
}
#console-help {
top: 10%;
height: 60%;
overflow-y: auto;
overflow-x: auto;
}
#console-help code {
font-weight: bold;
background-color: #666;
color: white;
padding: 2px 6px;
}
`;
init() {
if (typeof window.game === 'object') {
// We're using the mod autoloader on a game version without the global "game" variable:
window.game.game_console = new GameConsole(window.game);
window.game.game_console.attach();
//console.log('Console registered via window.game');
} else if (typeof game === 'object') {
// We're using a game version that has the global "game" variable
game.game_console = new GameConsole(game);
game.game_console.attach();
//console.log('Console registered via game (global variable)');
} else {
// We're using a method override to set the references
let _game_setListeners = Game.prototype.setListeners;
Game.prototype.setListeners = function () {
_game_setListeners.call(this);
this.game_console = new GameConsole(this);
this.game_console.attach();
};
//console.log('Console registered via Game.prototype.setListeners override');
}
};
}
class GameConsole {
constructor(game) {
this.game = game;
this.toggleTrigger = "keyup";
this.submitTrigger = "keydown";
this.element = document.createElement("input");
this.element.id = "console";
this.help = document.createElement("div");
this.help.id = "console-help";
this.historyIndex = 0;
this.history = [];
this.commands = new Commands(game, this);
};
show = function(e) {
if (game.splash.isShown) {
return;
}
e.stopImmediatePropagation();
//e.preventDefault();
if (e.key !== "c" && e.key !== "Enter") {
return;
};
window.game_console.element.style.display = "initial";
window.game_console.element.focus();
};
showHelp = function(which, data) {
this.help.innerHTML = `
<h2>Console Help</h2>
<em>Click anywhere inside me to close</em>
`;
if (typeof this.helpTexts[which] === "undefined") {
this.help.innerHTML += "<h4>Unknown command: <code>" + which + "</code></h4>";
} else {
let text = this.helpTexts[which];
if (typeof data === "object" && data.length > 0) {
for (const index in data) {
text = text.replace("%" + index, data[index]);
}
}
this.help.innerHTML += text;
}
this.help.style.display = "block";
};
hide = function() {
this.element.blur();
this.element.style.display = "none";
};
hideHelp = function() {
window.game_console.help.style.display = "none";
};
submit = function(e) {
e.stopImmediatePropagation();
//e.preventDefault();
if (e.key == "Escape") {
window.game_console.hide();
} else if (e.key == "ArrowUp") {
--window.game_console.historyIndex;
if (window.game_console.historyIndex < 0) {
window.game_console.historyIndex = 0;
}
window.game_console.recall();
} else if (e.key == "ArrowDown") {
++window.game_console.historyIndex;
if (window.game_console.historyIndex <= window.game_console.history.length - 1) {
window.game_console.recall();
} else if (window.game_console.historyIndex == window.game_console.history.length) {
window.game_console.element.value = '';
} else if(window.game_console.historyIndex > window.game_console.history.length) {
window.game_console.historyIndex = window.game_console.history.length;
}
} else if (e.key == "Enter") {
//console.log("console submitted");
let value = window.game_console.element.value;
if (value !== window.game_console.history[window.game_console.history.length - 1]) {
window.game_console.history.push(value);
window.game_console.historyIndex = window.game_console.history.length;
}
window.game_console.commands.perform(value);
}
};
recall = function() {
if (typeof this.history[this.historyIndex] !== "undefined") {
this.element.value = this.history[this.historyIndex];
} else {
this.element.value = '';
}
};
attach = function() {
document.body.appendChild(this.help);
document.body.appendChild(this.element);
addEventListener(this.toggleTrigger, this.show);
this.element.addEventListener(this.submitTrigger, this.submit);
this.help.addEventListener("click", this.hideHelp);
window.game_console = this;
if (typeof game !== 'undefined') {
game.game_console = this;
}
console.log('Console attached');
};
helpTexts = {
"unknown": `
<h4>Unknown command: <code>%0</code></h4>
`,
"error": `
<h4>Command error: <code>%0</code></h4>
`,
"global": `
<h4>Usage</h4>
<ul>
<li>Press <code>c</code> or <code>Enter</code> to open or focus the console field.</li>
<li>Press <code>Enter</code> in console field to execute the command.</li>
<li>Press <code>Escape</code> in console field to close without running command.</li>
<li>Press <code>ArrowUp</code> to select the previously entered command(s).</li>
<li>
Press <code>ArrowDown</code> to select the next command when browsing through
previously entered commands.
</li>
</ul>
<h4>Commands</h4>
<ul>
<li>
<code>help</code>
<p>Shows this help text.</p>
</li>
<li>
<code>help $command</code>
<p>Shows help for specific command, e.g. <code>help give</code>.</p>
</li>
<li>
<code>give $resource $amount</code>
<p>
Adds amount of resources. Resource type $resource (1-10) and $amount either
a whole number or a shortened number like <code>100K</code> or <code>1M</code> etc.
</p>
</li>
<li>
<code>take $resource $amount</code>
<p>
Adds amount of resources. Resource type $resource (1-10) and $amount either
a whole number or a shortened number like <code>100K</code> or <code>1M</code> etc.
</p>
</li>
<li>
<code>zoom $level</code>
<p>
Zooms in or out, not constrained by usual limits. Level must be more than zero,
lower values means zoomed further out. Decimal values between <code>1</code> and
<code>0</code> zoom out from default level (lower = more zoomed out). E.g.
<code>0.5</code> is zoomed out twice as much as default view.
</p>
</li>
<li>
<code>coords</code>
<p>Copies the grid position X,Y coordinates to clipboard.</p>
</li>
<li>
<code>export</code>
<p>
Exports the current game data as encoded data to clipboard (same as using "export"
from the main menu).
</p>
</li>
<li>
<code>import</code>
<p>
Imports the current game data from clipboard (same as using "import" from the main menu).
</p>
</li>
<li>
<code>place $building [$coordinates]</code>
<p>
Places a building of type <code>$building</code> at coordinates <code>$coordinates</code>.
If <code>$coordinates</code> is omitted, places the building at the tile hovered with mouse.
</p>
</li>
<li>
<code>blank</code>
<p>
Removes all GUI elements. Repeat the command to show the elements again.
</p>
</li>
<li>
<code>reload</code>
<p>
Reloads the game, returning you to the main menu and resetting pan/zoom positions.
</p>
</li>
<li>
<code>tp</code>
<p>
Teleports you to the desired location, either coordinates or entity name. If entity name,
teleports you to the last built building with that name.
</p>
</li>
<li>
<code>depth</code>
<p>
Sets the depth of the currently hovered channel. Example: <code>depth 1000</code> to set
depth to 1000 meters, or <code>depth 10k</code> to set the depth to 10,000 meters.
</p>
</li>
</ul>
`,
"give": `
<h4>Command: <code>give $resource amount</code>
<p>
Adds resources of the specified type in the specified amount. Resource type is
a number <code>1-10</code> or <code>all</code> and amount can be either whole numbers
or shortened numbers, e.g. <code>100000</code> and <code>100K</code> both mean
"one hundred thousand".
</p>
<p>
Valid shortened suffixes are: K, M, B and T.
</p>
`,
"take": `
<h4>Command: <code>take $resource $amount</code>
<p>
Subtracts resources of the specified type in the specified amount. Resource type is
a number <code>1-10</code> or <code>all</code> and amount can be either whole numbers
or shortened numbers, e.g. <code>100000</code> and <code>100K</code> both mean
"one hundred thousand".
</p>
<p>
Valid shortened suffixes are: K, M, B and T.
</p>
`,
"zoom": `
<h4>Command: <code>zoom $level</code>
<p>
Unconstrained zoom. Capable of zooming in or out further than is possible with the
mouse wheel. <code>$level</code> must be greater than zero, lower values means zoomed
further out.
</p>
`,
"coords": `
<h4>Command: <code>coords</code>
<p>
Copies the grid position X,Y coordinates to clipboard.
</p>
`,
"export": `
<h4>Command: <code>export</code>
<p>
Exports the current game data as encoded data to clipboard (same as using "export" from
the main menu).
</p>
`,
"import": `
<h4>Command: <code>import</code>
<p>
Imports the current game data from clipboard (same as using "import" from the main menu).
</p>
`,
"place": `
<h4>Command: <code>place $building [$coordinates]</code>
<p>
Places a structure of the given type and the given coordinates, without subtracting the
resource cost. Example: <cpde>place gradient 10,2</code> places a Gradient Well at
coordinates x=10, y=2. Tip: coordinates of mouse pointer can be copied to clipboard with
the <code>coords</code> command and pasted as <code>$coordinates</code> of this command.
If coordinates are not provided the building is placed in the currently hovered tile.
</p>
<ul>
<li>Can only place structures which can be bought.</li>
<li>Cannot place structures that are one-only and which already exists on the game field.</li>
<li>
Can place structures that are upgrades for lower tier structures without the lower tier
structure being present.
</li>
</ul>
<p>
Valid building names:
</p>
<ol>
%0
</ol>
`,
"blank": `
<h4>Command: <code>blank</code>
<p>
Removes all GUI elements. Repeat the command to show the elements again.
</p>
`,
"reload": `
<h4>Command: <code>reload</code>
<p>
Reloads the game, returning you to the main menu and resetting pan/zoom positions.
</p>
`,
"tp": `
<h4>Command: <code>tp $location</code>
<p>
Teleports you to the specified location. The location can be either an <code>x,y</code>
coordinate or the name of an entity. If there is more than one entity with the given name,
you will be teleported to the last-built entity of that type.
</p>
<p>
Valid entity names:
</p>
<ol>
%0
</ol>
`,
"depth": `
<h4>Command: <code>depth</code>
<p>
Sets the depth of the currently hovered channel. Example: <code>depth 1000</code> to set
depth to 1000 meters, or <code>depth 10k</code> to set the depth to 10,000 meters.
</p>
`
};
}
class Commands {
constructor(game, console) {
this.game = game;
this.console = console;
};
perform = function (value) {
//console.log("asked to perform: " + value);
let parts = value.split(" ");
if (parts.length === 0) {
return;
}
switch (parts[0]) {
default:
if (typeof this[parts[0]] !== "function") {
this.console.showHelp("unknown", parts);
} else {
this[parts[0]].call(this, ...parts.slice(1));
}
break;
case "help":
if (typeof parts[1] === "undefined") {
// Show global help
this.console.showHelp("global");
} else {
// Show help for specific command
let html = "";
let data = [];
switch (parts[1]) {
case "place":
let buildables = this.collectEntities(true);
for (const entityName in buildables) {
let entity = buildables[entityName];
if (entity.onlyone && this.game.onlyones[entityName]) {
continue;
}
html += "<li>" + entityName + "</li>";
}
break;
case "tp":
let entities = this.collectEntities(false);
for (const entityName in entities) {
html += "<li>" + entityName + "</li>";
}
break;
}
data.push(html);
this.console.showHelp(parts[1], data);
}
break;
}
};
give = function(resourceIndex, amount) {
if (typeof resourceIndex === "undefined") {
this.console.showHelp("error", "Command requires a resource index");
return;
}
if (typeof amount === "undefined") {
this.console.showHelp("error", "Command requires an amount");
return;
}
if (resourceIndex !== "all" && typeof this.game.resources[parseInt(resourceIndex) - 1] === "undefined") {
this.console.showHelp("error", "Command requires a valid resource index (1-10)");
return;
}
let newAmount = 0;
let storage = typeof this.game.quantities !== "undefined" ? this.game.quantities : this.game.resources;
amount = this.expandNumber(amount);
if (resourceIndex === "all") {
for (const i in storage) {
storage[i] += amount;
if (storage[i] < 0) {
storage[i] = 0;
}
}
} else {
resourceIndex = parseInt(resourceIndex) - 1;
storage[resourceIndex] += amount;
if (storage[resourceIndex] < 0) {
storage[resourceIndex] = 0;
}
}
};
take = function (resourceIndex, amount) {
this.give(resourceIndex, '-' + amount);
};
zoom = function (newZoom) {
if (newZoom < 0.00001) {
// Ignore ridiculous values
return;
}
this.game.zoom = parseFloat(newZoom);
};
coords = function() {
console.log(this.game.hoveredCell.toString());
navigator.clipboard.writeText(this.game.hoveredCell);
};
export = async function() {
await this.game.exportSave();
};
import = async function() {
await this.game.loadSaveFromClipboard();
};
blank = function() {
let shop = document.getElementsByClassName('shop')[0];
let chatIcon = document.getElementsByClassName('chatIcon')[0];
let messenger = document.getElementsByClassName('messenger')[0];
if (shop.style.display !== "none") {
// hide all
shop.style.display = "none";
chatIcon.style.display = "none";
messenger.style.display = "none";
} else {
// show all
shop.style.display = "initial";
chatIcon.style.display = "initial";
messenger.style.display = "initial";
}
};
place = function(entityName, coordinates) {
if (typeof coordinates === "undefined") {
coordinates = this.game.hoveredCell.toString();
}
if (typeof this.game.codex.entities[entityName] === "undefined") {
this.console.showHelp("error", "Unknown entity type: " + entityName);
return;
}
let position = coordinates.split(",");
position[0] = parseInt(position[0]);
position[1] = parseInt(position[1]);
this.game.addEntity(entityName, position);
};
tp = function(position) {
const delta = this.game.uvToXY(this.expandPosition(position));
this.game.translation[0] += delta[0] / this.game.zoom;
this.game.translation[1] += delta[1] / this.game.zoom;
};
reload = function() {
document.location.reload();
};
depth = function(depth) {
if (this.game.hoveredEntity?.name !== 'pump' && this.game.hoveredEntity?.name !== 'pump2') {
return;
}
this.game.hoveredEntity.depth = this.expandNumber(depth) / 10;
//console.log(this.game.hoveredEntity.depth);
};
expandPosition = function(coordinatesOrEntityName) {
let position = coordinatesOrEntityName.split(",");
if (coordinatesOrEntityName.indexOf(",") < 0) {
// Given position was an entity name. Scan the stuff list, use the first match for position.
for (const i in this.game.stuff) {
if (this.game.stuff[i].name === coordinatesOrEntityName) {
return this.game.stuff[i].position;
}
}
}
position[0] = parseInt(position[0]);
position[1] = parseInt(position[1]);
return position;
};
expandNumber = function(number) {
let numberString = number.toString();
let suffix = numberString.substring(numberString.length - 1).toUpperCase();
let suffixes = ['K', 'M', 'B', 'T'];
if (suffixes.indexOf(suffix) >= 0) {
number = parseInt(numberString.substring(0, numberString.length - 1));
switch (suffix) {
case 'K': number *= 1000; break;
case 'M': number *= 1000000; break;
case 'B': number *= 1000000000; break;
case 'T': number *= 1000000000000; break;
}
};
return parseFloat(number);
};
collectEntities = function(onlyBuildable) {
let entities = {};
for (const entityName in this.game.codex.entities) {
let entity = this.game.codex.entities[entityName];
if (onlyBuildable && !entity.canPurchase) {
continue;
}
entities[entityName] = entity;
}
return entities;
};
};
/*
* Sixty-Four Mod: Darken
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* ----------------------------------------------
*
* REQUIRES THE MOD AUTOLOADER
* See https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-_readme-md
*
* ----------------------------------------------
*
* Darkens the playing field from bright white to darker by applying an overlay shade with configurable intensity.
*/
module.exports = class GridLines extends Mod {
label = 'Darken';
description = 'Darkens the playing field from bright white to darker by applying an overlay shade with configurable intensity.';
version = '1.0.1';
settings = {
intensity: {
default: 0.1,
label: 'Shade intensity',
description: 'Use a value from 0.01 to 1.0 - lower value means shade effect is less intense.'
},
shadeColor: {
default: '#000',
label: 'Shading color',
description: 'The color of the shade. Will be made semi-transparent based on intensity. Use 3 or 6 hex digits + 1 optional hex digit for transparency value.'
},
blendMode: {
default: 'darken',
label: 'Blending mode',
description: 'Which Canvas blending mode to use when applying the shade.',
options: [
'multiply',
'screen',
'overlay',
'darken',
'lighten',
'color-dodge',
'color-burn',
'hard-light',
'soft-light',
'difference',
'exclusion',
'hue',
'saturation',
'color',
'luminosity',
]
},
};
getMethodReplacements() {
const self = this;
return [
{
class: Game,
method: 'renderloop',
replacement: function (dt) {
self.originalMethods.Game.renderloop.call(this);
this.ctx.save();
this.ctx.globalCompositeOperation = self.configuredOptions.blendMode;
this.ctx.globalAlpha = parseFloat(self.configuredOptions.intensity);
this.ctx.fillStyle = self.configuredOptions.shadeColor;
this.ctx.fillRect(0, 0, this.w, this.h);
this.ctx.restore();
}
}
];
};
}
/*
* Sixty-Four Mod: Demo Mod Class
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* ----------------------------------------------
*
* REQUIRES THE MOD AUTOLOADER
* See https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-_readme-md
*
* ----------------------------------------------
*
* Demonstration of class-based mod compatible with the Mod Autoloader.
*/
module.exports = class DemoModClass extends Mod
{
// Standard metadata. All of these are optional but a minimum of "label" and "version" should be specified.
label = 'Demo Mod';
description = 'Demonstrates how to write a mod class';
version = '1.0.0';
settings = {
demoOption: {
default: true,
label: 'Demo option',
description: 'Demonstrates a boolean (checkbox) option'
}
};
// If this mod had a dependency on another mod and version:
// dependencies {
// SomeOtherMod: '1.2.0'
// }
// If it has conflicts with other mods ("*" for any version, or put a specific version):
// conflicts {
// ConflictMod: '*'
// }
styles = 'body { background-color: #ddd; }';
constructor(configuredOptions, mods) {
// configuredOptions: an object with all settings for this mod, as configured by the player.
// mods: an object with metadata of all mods, may be incomplete at this point but is complete when the "init"
// method of this mod class is called.
// THIS METHOD IS OPTIONAL AND SHOULD ONLY BE IMPLEMENTED IF YOU REQUIRE DYNAMIC PRE-INITIALIZATION, SUCH AS
// SETTING METADATA ATTRIBUTES BASED ON SELECTED OPTIONS OR PRESENCE OF OTHER MODS.
super(configuredOptions, mods);
console.log('DemoModClass was constructed');
}
// Mods which need to replace methods on various game objects will need to implement this method. Will only be
// called if the mod is enabled.
getMethodReplacements() {
// First, create a reference to this current object, necessary since "this" is a different reference inside
// the replacement functions we will need to call.
const self = this;
// Define an array of functions. Each is an object with three properties identifying original and new functions.
// These replacements will only be applied if the mod is enabled.
return [
{
class: Game, // The class, NOT surrounded by quotes
method: 'init', // The method on the class, WITH quotes
replacement: function () { // The function that will replace the original function.
console.log('Calling "init" on "Game"');
// Call the original function. Depending on your needs there are three approaches:
// - call the original BEFORE (your method can use/replace things changed in the original method).
// To call the original method BEFORE your method place this call at the start of your method.
// - call the original AFTER (your method prepares things that the original method will use).
// To call it AFTER your method place this call at the end of your method.
// - don't call the original method (your method completely replaces the original).
// To NOT call the original method don't add this call anywhere.
self.originalMethods.Game.init.call(this);
}
}
];
};
init() {
// In this method you can do things that will only happen if the mod is enabled. For example, register keypress
// listeners, use/change aspects (configuration, metadata, etc.) of other mods, etc.
// In short: do things that cannot be done by configuring your mod.
// If your mod has no need for such custom actions simply omit the method completely.
console.log('DemoModClass was initialized');
};
};
/*
* Sixty-Four Mod: Drag to build/delete/upgrade multiple
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* ----------------------------------------------
*
* REQUIRES THE MOD AUTOLOADER
* See https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-_readme-md
*
* ----------------------------------------------
*
* Allows you to build or delete multiple buildings by holding the
* mouse button down and dragging the mouse. Also adds a setting you
* can enable which will keep the held item after placing it, allowing
* you to build another without picking it from the menu. Click the
* right mouse button or ESC to clear the held item.
*/
module.exports = class DragBuild extends Mod
{
label = 'Drag-to-build';
description = 'Allows you to drag to create / delete / upgrade multiple buildings.';
version = '1.0.0';
settings = {
dragToBuild: {
default: true,
label: 'Drag to build',
description: 'If enabled, allows you to drag the mouse to build multiple buildings'
},
dragToDelete: {
default: true,
label: 'Drag to delete'
},
dragToUpgrade: {
default: true,
label: 'Drag to upgrade'
},
keepItemInHand: {
default: true,
label: 'Keep selected item',
description: 'After building, keep selected item in hand'
}
};
down = false;
recursed = false;
itemToPlace = null;
forceDeselectHeldItem = false;
getMethodReplacements() {
const self = this;
const methods = [
{
class: Game,
method: 'processMousedown',
replacement: function(e) {
self.originalMethods.Game.processMousedown.call(this, e);
if (!e || e.button === 0) {
self.down = true;
}
if (self.configuredOptions.keepItemInHand && this.transportedEntity) {
// We have to register the need to deselect the held item here, since the "transportedEntity"
// property will be empty when we hit the "processMouseup" method. We instead set this property
// and read that in the "processMouseup" method to determine if we must deselect the item.
self.forceDeselectHeldItem = true;
}
}
},
{
class: Game,
method: 'processMouseup',
replacement: function(e) {
self.originalMethods.Game.processMouseup.call(this);
if (!e || e.button === 0) {
self.down = false;
}
if (self.configuredOptions.keepItemInHand) {
if ((e && e.button === 2 && !this.mouse.positionChanged) || self.forceDeselectHeldItem) {
self.itemToPlace = null;
self.forceDeselectHeldItem = false;
delete this.itemInHand;
} else {
self.itemToPlace = this.itemInHand;
}
}
}
},
{
class: Game,
method: 'processMousemove',
replacement: function(e, dxy) {
self.originalMethods.Game.processMousemove.call(this, e, dxy);
if (self.recursed || !self.down) {
return;
}
const dragToBuild = self.configuredOptions.dragToBuild;
const dragToDelete = self.configuredOptions.dragToDelete;
const dragToUpgrade = self.configuredOptions.dragToUpgrade;
const keepItemInHand = self.configuredOptions.keepItemInHand;
const upgradeForItem = this.codex.entities[this.itemInHand?.name]?.isUpgradeTo;
const isErasing = this.hoveredEntity && this.itemInHand?.eraser;
const isPlacing = !this.hoveredEntity && this.itemInHand;
const isUpgradeForHeldItem = upgradeForItem === this.hoveredEntity?.name;
const isUpgrading = this.hoveredEntity && this.itemInHand && isUpgradeForHeldItem;
if ((dragToBuild && isPlacing) || (dragToDelete && isErasing) || (dragToUpgrade && isUpgrading)) {
const item = this.itemInHand;
self.recursed = true;
this.processClick();
if (!this.itemInHand) {
// Keep the held item, ready to place another, whether affordable or not.
this.itemInHand = item;
}
self.recursed = false;
}
}
}
];
if (this.configuredOptions.keepItemInHand) {
methods.push(
{
class: Game,
method: 'processClick',
replacement: function(e) {
self.originalMethods.Game.processClick.call(this, e);
if (!self.down && self.itemToPlace) {
// Mouse up and an item was held and not cleared with right mouse button. Restore held item.
this.itemInHand = self.itemToPlace;
}
}
}
)
}
return methods;
};
};
/*
* Sixty-Four Mod: Fast & Furious Rock
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* Decreases the time between Hollow Stone spawning from the Hollow Rock.
* Adjust the "speed" variable to your liking. By default, makes the Rock x4 faster.
* The "speed" variable supports decimals, e.g. "1.5" makes it one and a half times
* faster. Values above 4 will make it hard to keep up with collecting.
*
* Also capable of increasing the limit on how many Hollow Stone can exist at the
* same time. By default, this number isn't changed but you can increase it by
* changing the "maximum" variable. The first tier of Hollow Rock which spawns
* Hollow Stone will use the "maximum" variable as maximum number of stones.
* Tiers above that will use the "maximum" multiplied by 2. The default value is 8,
* same as the value used by the base game.
*
* Please note that this mod does NOT increase the speed of the Hollow Fruit, they
* still take as long as before to process a single Hollow Stone. But, increasing
* the speed means you can keep many more Hollow Fruits active at the same time.
*/
module.exports = class FastAndFuriousRock extends Mod
{
label = 'Fast & Furious Rock';
description = 'Allows you to modify the speed and Hollow Stone cap of the Hollow Rock';
version = '1.0.0';
settings = {
speed: {
default: 2,
label: 'Speed',
description: 'How quickly the Hollow Rock spawns Hollow Stones, multiplies default speed. Higher means faster.'
},
maximum: {
default: 8,
label: 'Maximum',
description: 'Maximum number of Hollow Stones that can exist before Hollow Rock stops spawning more.'
}
};
getMethodReplacements() {
const self = this;
return [
{
class: Strange1,
method: 'init',
replacement: function () {
const original_value = 80000;
self.originalMethods.Strange1.init.call(this);
this.spawnTimerBase = original_value / self.configuredOptions.speed;
this.maxSpawnedHollows = self.configuredOptions.maximum;
}
},
{
class: Strange2,
method: 'init',
replacement: function () {
let original_value = 40000;
self.originalMethods.Strange2.init.call(this);
this.spawnTimerBase = original_value / self.configuredOptions.speed;
this.maxSpawnedHollows = self.configuredOptions.maximum * 2;
}
},
{
class: Strange3,
method: 'init',
replacement: function () {
let original_value = 6000;
self.originalMethods.Strange3.init.call(this);
this.spawnTimerBase = original_value / self.configuredOptions.speed;
this.maxSpawnedHollows = self.configuredOptions.maximum * 2;
}
},
];
};
};
/*
* Sixty-Four Mod: Faster Resonators
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* Allows you to change how fast the Entropy Resonator I and II will hit
* adjecent blocks. By default, doubles the speed of the Resonators.
*
* Also allows you to change the power to make the Resonators hit harder.
* A value of "2" will make the Resonator hit twice as hard. By default
* the mod will NOT increase the power of the Resonators.
*
* Both variables support decimals, e.g. "1.5" makes the speed/power one
* and a half times faster. Values below zero, e.g. "0.5" will make the
* speed/power increase instead of decrease.
*
* To define the settings, either edit this file and change the values right after
* the "??" in the first lines of the script, or use the "autoloader" mod and put
* the settings in the "mods.json" file.
*/
module.exports = class FasterResonators extends Mod
{
label = 'Faster Resonators';
description = 'Modify the performance (speed and power) of the lower tiers of Resonators';
version = '1.0.0';
settings = {
speed: {
default: 2,
label: 'Speed',
description: 'How quickly the Resonators attack adjacent blocks.'
},
power: {
default: 1,
label: 'Power',
description: 'How hard each Resonator attack hits the adjacent block(s).'
}
};
getMethodReplacements() {
const self = this;
return [
{
class: Entropic,
method: 'init',
replacement: function() {
self.originalMethods.Entropic.init.call(this);
const original_speed = 1000;
const original_power = .33;
this.interval = original_speed / self.configuredOptions.speed;
this.power = original_power * self.configuredOptions.power;
}
},
{
class: Entropic2,
method: 'init',
replacement: function() {
self.originalMethods.Entropic2.init.call(this);
const original_speed = 300;
const original_power = .66;
this.interval = original_speed / self.configuredOptions.speed;
this.power = original_power * self.configuredOptions.power;
}
},
{
class: Entropic2a,
method: 'init',
replacement: function() {
self.originalMethods.Entropic2a.init.call(this);
const original_power = 2;
this.power = original_power * self.configuredOptions.power;
}
},
{
class: Entropic3,
method: 'init',
replacement: function() {
self.originalMethods.Entropic3.init.call(this);
const original_power = 256;
this.power = original_power * self.configuredOptions.power;
}
}
];
};
};
/*
* Sixty-Four Mod: Grid Lines
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* ----------------------------------------------
*
* REQUIRES THE MOD AUTOLOADER
* See https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-_readme-md
*
* ----------------------------------------------
*
* Toggle visual grid lines on/off by pressing "G".
*/
module.exports = class GridLines extends Mod {
label = 'Grid Lines';
description = 'Toggle visual grid lines on/off by pressing "G"';
version = '1.0.0';
settings = {
lineColor: {
default: '#CCC',
label: 'Grid Line Color',
description: 'Use 3 or 6 hex digits + 1 optional hex digit for transparency value.'
},
lineWidth: {
default: '1',
label: 'Grid Line Width',
description: 'Width of each grid line, in number of pixels.'
},
alternate: {
default: false,
label: 'Alternate Backgrounds',
description: 'When grid lines are shown, render every other cell with a different background color.'
},
alternateColor: {
default: '#EEE',
label: 'Alternate Background Color',
description: 'Use 3 or 6 hex digits + 1 optional hex digit for transparency value.'
},
tenths: {
default: false,
label: 'Alternate Backgrounds, every 10th',
description: 'When grid lines are shown, render every other cell with a different background color.'
},
tenthColor: {
default: '#AAA',
label: 'Alternate Background Color for every 10th cell',
description: 'Use 3 or 6 hex digits + 1 optional hex digit for transparency value.'
},
};
shown = false;
init() {
addEventListener('keyup', e => {
// Show the grid lines, but only if the "g" key wasn't pressed while the "Console" mod's input is shown.
if (e.key === 'g' && !(e.srcElement && e.srcElement.id === 'console')) this.shown = !this.shown;
})
};
getMethodReplacements() {
const self = this;
return [
{
class: Game,
method: 'renderEntities',
replacement: function (dt) {
if (self.shown) {
const size = 70;
const ctx = this.ctx;
let uv = this.hoveredEntity?.position || this.hoveredCell;
uv = [].slice.call(uv);
uv[0] -= uv[0] % 2;
uv[1] -= uv[1] % 2;
const range = {x: [uv[0] - size, uv[0] + size], y: [uv[1] - size, uv[1] + size]};
ctx.save();
ctx.globalAlpha = 1;
ctx.strokeStyle = self.configuredOptions.lineColor;
ctx.lineWidth = self.configuredOptions.lineWidth * this.pixelRatio;
ctx.strokeWidth = self.configuredOptions.lineWidth * this.pixelRatio;
ctx.lineCap = 'square';
ctx.beginPath();
for (let y = range.y[0]; y <= range.y[1]; y++) {
const xy0 = this.uvToXY([range.x[0] + .5, y + .5]);
const xy1 = this.uvToXY([range.x[1] + .5, y + .5]);
ctx.moveTo(...xy0);
ctx.lineTo(...xy1);
}
for (let x = range.x[0]; x <= range.x[1]; x++) {
const xy0 = this.uvToXY([x + .5, range.y[0] + .5]);
const xy1 = this.uvToXY([x + .5, range.y[1] + .5]);
ctx.moveTo(...xy0);
ctx.lineTo(...xy1);
}
ctx.fill();
ctx.stroke();
ctx.restore();
if (self.configuredOptions.alternate) {
ctx.save();
for (let x = range.x[0]; x <= range.x[1]; x += 2) {
for (let y = range.y[0]; y <= range.y[1]; y += 2) {
if (self.configuredOptions.tenths && x % 10 === 0 && y % 10 === 0) {
ctx.fillStyle = self.configuredOptions.tenthColor;
} else {
ctx.fillStyle = self.configuredOptions.alternateColor;
}
const xy0 = this.uvToXY([x + .5, y + .5])
const xy1 = this.uvToXY([x + .5, y - .5])
const xy2 = this.uvToXY([x - .5, y - .5])
const xy3 = this.uvToXY([x - .5, y + .5])
ctx.beginPath();
ctx.moveTo(...xy0);
ctx.lineTo(...xy1);
ctx.lineTo(...xy2);
ctx.lineTo(...xy3);
ctx.fill();
}
}
ctx.fillStyle = self.configuredOptions.alternateColor;
for (let x = range.x[0] -1; x <= range.x[1]; x += 2) {
for (let y = range.y[0] - 1; y <= range.y[1]; y += 2) {
const xy0 = this.uvToXY([x + .5, y + .5])
const xy1 = this.uvToXY([x + .5, y - .5])
const xy2 = this.uvToXY([x - .5, y - .5])
const xy3 = this.uvToXY([x - .5, y + .5])
ctx.beginPath();
ctx.moveTo(...xy0);
ctx.lineTo(...xy1);
ctx.lineTo(...xy2);
ctx.lineTo(...xy3);
ctx.fill();
}
}
ctx.restore();
}
}
self.originalMethods.Game.renderEntities.call(this, dt);
}
}
];
};
}
/*
* Sixty-Four Mod: Improved Silos
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* ----------------------------------------------
*
* REQUIRES THE MOD AUTOLOADER
* See https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-_readme-md
*
* ----------------------------------------------
*
* Doubles the charges held by each type of silo, by artificially increasing the
* "fill" of the silo by 50% of what it consumes for every neighbor filled.
*
* The mod has a couple of settings:
*
* - silo1_ratio: sets the ratio of fill reduction when a silo refills a neighbor.
* A value of 0.0 means the mod is essentially disabled (no improvement to silo).
* A value of 1.0 means that fill reduction is disabled, which causes the silo to
* never need to be refilled.
* A value of 0.5 means that the fill reduction is halved, which causes the silo
* to only need to be refilled half as frequently.
* - silo2_ratio: same as silo1_ratio, but applies to the industrial silo instead.
* - cost_multipler: can increase or decrease the cost of filling the silo. A value
* of zero removes all costs. A value of 1.0 uses the original cost. And a value
* of for example 10 increases the cost 10x. If you're setting the two "ratio"
* settings to 0.0 for no-refill silos, you may want to increase this value of
* 100 or more, to at least keep a hint of game difficulty balance.
*
* To define the settings, either edit this file and change the values right after
* the "??" in the first lines of the script, or use the "autoloader" mod and put
* the settings in the "mods.json" file.
*/
module.exports = class ImprovedSilos extends Mod
{
label = 'Improved Silos';
description = 'Makes it possible to change how long a silo can run unattended (infinite is possible) ' +
'and allows you to change the cost required to fill/refill the silo.';
version = '1.0.0';
settings = {
silo1_ratio: {
default: 0.5,
label: 'Ratio, Underground Silo',
description: 'Drain reduction. A value of 0.0 means no reduction (no improvement to silo). ' +
'A value of 1.0 means the silo to never need to be refilled. A value of 0.5 causes ' +
'the silo to only need to be refilled half as frequently.'
},
silo2_ratio: {
default: 0.5,
label: 'Ratio, Industrial Silo',
description: 'Same as above, but applies to the industrial silo instead.'
},
cost_multiplier: {
default: 1,
label: 'Fill cost multipler',
description: 'Silo refill cost multiplier. A value of zero removes all costs. A value of 1.0 uses the ' +
'original cost. And a value of 10 increases the cost 10x. If you\'re setting the two "ratio" ' +
'settings to 0.0 for no-refill silos, you may want to increase this value of 100 or more, to at ' +
'least keep a hint of game difficulty balance.'
}
};
getMethodReplacements() {
const self = this;
return [
{
class: Silo,
method: 'init',
replacement: function() {
self.originalMethods.Silo.init.call(this);
const original_fuel = [0, 256, 0, 0, 2];
for (const i in original_fuel) {
this.fuel[i] = original_fuel[i] * self.configuredOptions.cost_multiplier;
}
this.initHint();
}
},
{
class: Silo,
method: 'tap',
replacement: function() {
const original_value = .0625;
this.fill += (self.configuredOptions.silo1_ratio * original_value);
self.originalMethods.Silo.tap.call(this);
}
},
{
class: Silo2,
method: 'init',
replacement: function() {
self.originalMethods.Silo2.init.call(this);
const original_fuel = [0, 1024, 0, 0, 8, 16];
for (const i in original_fuel) {
this.fuel[i] = original_fuel[i] * self.configuredOptions.cost_multiplier;
}
this.initHint();
}
},
{
class: Silo2,
method: 'tap',
replacement: function() {
const original_value = .015625;
this.fill += (self.configuredOptions.silo2_ratio * original_value);
self.originalMethods.Silo2.tap.call(this);
}
},
];
};
};
/*
* Sixty-Four Mod: Individual Sounds Volume Control
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* ----------------------------------------------
*
* REQUIRES THE MOD AUTOLOADER
* See https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-_readme-md
*
* ----------------------------------------------
*
* Allows changing the volume for every sound in the game, individually.
* For example to lower the volume of the "block breaking" sound or
* increase the volume of the "silo empty" sound.
*/
module.exports = class IndividualVolume extends Mod
{
label = 'Individual Volume';
description = 'Allows you to set a volume scale for each individual sound in the game. Settings accept ' +
'decimal values, "0.1" means "10%".';
version = '1.0.0';
settings = {
tap1: {
default: 1.0,
label: 'Tap (1)'
},
tap2: {
default: 1.0,
label: 'Tap (2)'
},
tap3: {
default: 1.0,
label: 'Tap (3)'
},
tap4: {
default: 1.0,
label: 'Tap (4)'
},
tap5: {
default: 1.0,
label: 'Tap (5)'
},
tap6: {
default: 1.0,
label: 'Tap (6)'
},
tap7: {
default: 1.0,
label: 'Tap (7)'
},
break: {
default: 1.0,
label: 'Block breaking'
},
rumble: {
default: 1.0,
label: 'Channel working'
},
bubble: {
default: 1.0,
label: 'Charonite vat working'
},
geiger: {
default: 1.0,
label: 'Chromalit decay'
},
release: {
default: 1.0,
label: 'BP Oxidizer working'
},
hellbreak: {
default: 1.0,
label: 'Hell Gem annihilation'
},
horn: {
default: 1.0,
label: 'Hollow Rock clicked'
},
hollow: {
default: 1.0,
label: 'Hollow Stone tapped'
},
teleport: {
default: 1.0,
label: 'Waypoint clicked'
},
exhaust: {
default: 1.0
},
void: {
default: 1.0,
},
soul: {
default: 1.0,
label: 'Reality collected'
},
lightning: {
default: 1.0,
label: ''
},
silo: {
default: 1.0,
label: ''
},
silo2: {
default: 1.0,
label: ''
},
endingMusic: {
default: 1.0,
label: 'Music during credits'
},
collect: {
default: 1.0,
label: 'Collect "free" resource (surge)'
}
};
getMethodReplacements() {
const self = this;
return [
{
class: Game,
method: 'playSound',
replacement: function (id, panning, loudness, dark, forced) {
//console.log('Playing: ' + id);
if (typeof self.configuredOptions[id] !== "undefined") {
loudness *= self.configuredOptions[id];
} else {
console.log("Asked to play sound " + id + " but no individual volume was found");
}
return self.originalMethods.Game.playSound.call(this, id, panning, loudness, dark, forced);
}
},
{
class: Game,
method: 'startSound',
replacement: function (id, panning, loudness) {
//console.log('Playing: ' + id);
if (typeof self.configuredOptions[id] !== "undefined") {
loudness *= self.configuredOptions[id];
} else {
console.log("Asked to play sound " + id + " but no individual volume was found");
}
return self.originalMethods.Game.startSound.call(this, id, panning, loudness);
}
}
];
};
};
/*
* Sixty-Four Mod: Insight
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* ----------------------------------------------
*
* REQUIRES THE MOD AUTOLOADER
* See https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-_readme-md
*
* ----------------------------------------------
*
* Shows various useful information about entities. Press "V" to toggle on/off.
*/
module.exports = class Insight extends Mod
{
label = 'Insight';
description = 'Shows various useful information about entities when holding down the CTRL button';
settings = {
fontSize: {
default: 18,
label: 'Font size'
},
fontColor: {
default: '#FFF',
label: 'Font color',
description: 'Use 3 or 6 hex digits + 1 optional hex digit for transparency value.'
},
padding: {
default: 5,
label: 'Info box padding'
},
defaultColor: {
default: '#333D',
label: 'Box color, default',
description: 'Use 3 or 6 hex digits + 1 optional hex digit for transparency value.'
},
converterColor: {
default: '#33CD',
label: 'Box color, converters',
description: 'Use 3 or 6 hex digits + 1 optional hex digit for transparency value.'
},
preheaterColor: {
default: '#C33D',
label: 'Box color, pre-heaters',
description: 'Use 3 or 6 hex digits + 1 optional hex digit for transparency value.'
},
destabilizerColor: {
default: '#3C3D',
label: 'Box color, destabilizers',
description: 'Use 3 or 6 hex digits + 1 optional hex digit for transparency value.'
},
pumpColor: {
default: '#CC3D',
label: 'Box color, channels',
description: 'Use 3 or 6 hex digits + 1 optional hex digit for transparency value.'
},
entropicColor: {
default: '#3CCD',
label: 'Box color, channels',
description: 'Use 3 or 6 hex digits + 1 optional hex digit for transparency value.'
}
};
calculatedFontSize = this.fontSize;
calculatedPadding = this.padding;
shown = false;
init() {
addEventListener('keyup', e => {
// Show the insight boxes, but only if the "v" key wasn't pressed while the "Console" mod's input is shown.
if (e.key === 'v' && !(e.srcElement && e.srcElement.id === 'console')) this.shown = !this.shown;
})
};
getMethodReplacements() {
const self = this;
return [
{
class: Game,
method: 'renderEntities',
replacement: function(dt) {
self.originalMethods.Game.renderEntities.call(this, dt);
if (!self.shown) {
return;
}
const zoom = Math.max(this.zoom, 0.5);
self.calculatedFontSize = self.configuredOptions.fontSize * zoom;
self.calculatedPadding = self.configuredOptions.padding * zoom;
this.ctx.font = self.calculatedFontSize + 'px Montserrat';
for (const entity of this.stuff) {
const pos = entity.position;
if (this.isVisible(entity)) {
if (!this.plane) {
switch (entity.constructor.name) {
case 'Entropic':
case 'Entropic2':
case 'Entropic2a':
case 'Entropic3':
self.drawBox(pos, self.getEntropicInfo(entity), self.configuredOptions.entropicColor);
break;
case 'Consumer':
self.drawBox(pos, self.getRefineryInfo(entity));
break;
case 'Fruit':
self.drawBox(pos, self.getFruitInfo(entity));
break;
case 'Pump':
case 'Pump2':
self.drawBox(pos, self.getPumpInfo(entity), self.configuredOptions.pumpColor);
break;
case 'Cube':
self.drawBox(pos, self.getCubeInfo(entity));
break;
case 'Destabilizer':
self.drawBox(pos, [entity.constructor.name, 'Power: 1'], self.configuredOptions.destabilizerColor);
break;
case 'Destabilizer2':
self.drawBox(pos, [entity.constructor.name, 'Power: 2'], self.configuredOptions.destabilizerColor);
break;
case 'Destabilizer2a':
self.drawBox(pos, [entity.constructor.name, 'Power: 625 (0)'], self.configuredOptions.destabilizerColor);
break;
case 'Preheater':
self.drawBox(pos, self.getPreheaterInfo(entity), self.configuredOptions.preheaterColor);
break;
case 'Converter13':
case 'Converter32':
case 'Converter41':
case 'Converter64':
case 'Converter76':
self.drawBox(pos, self.getConversionInfo(entity), self.configuredOptions.converterColor);
break;
}
} else if (entity.soulPower > 0) {
self.drawBox(pos, [`Reality: ${entity.soulPower}`]);
}
}
}
}
}
]
};
getEntropicInfo(entity) {
const info = [
entity.constructor.name,
`Power: ${entity.power}`
];
if (entity.constructor.name === 'Entropic' || entity.constructor.name === 'Entropic2') {
info.push(`Speed: ${(1000 / entity.interval).toPrecision(2)}/s`)
}
return info;
};
getRefineryInfo(entity) {
const info = [
entity.constructor.name,
`Performance: ${Math.ceil(entity.multiplicator / entity.maxMultiplicator * 100)}%`,
`Fill: ${Math.ceil(entity.resourceCount / entity.maxResourceCount * 100)}%`
];
if (entity.timer > 0) {
info.push(`Resets in: ${Math.ceil(entity.timer / 1000)}s`)
}
return info;
};
getFruitInfo(entity) {
if (!entity.conversion) return [];
return [entity.constructor.name, `Conversion: ${Math.ceil(entity.conversion * 100)}%`];
};
getPumpInfo(entity) {
// Taken from Pump.prototype.update since speed calculation isn't stored as property.
let totalspeed = entity.active ? entity.pumpSpeed : 0
let auxSpeed = 0
let activeAuxes = []
if (entity.auxes?.length){
for (let i = 0; i < entity.auxes.length; i++){
const ping = entity.auxes[i].tap(0)
if (ping) {
activeAuxes.push(entity.auxes[i])
if (!entity.active) auxSpeed = Math.max(ping, auxSpeed)
}
}
}
totalspeed += auxSpeed * entity.pumpSpeed
return [
entity.constructor.name,
`Depth: ${Math.ceil(entity.depth * 10)}m`,
`Speed: ${totalspeed}`
];
};
getCubeInfo(entity) {
return [
entity.constructor.name,
`Breaking: ${entity.breakPower}`,
`Broken: ${Math.ceil(entity.broken * 100)}%`
];
};
getDestabilizerInfo(entity) {
return [
entity.constructor.name,
`Power: ${entity.breakPower}`
];
};
getPreheaterInfo(entity) {
return [
entity.constructor.name,
entity.multiplicator + 'x',
`Silo: ${entity.isNextToSilo ? 'yes' : 'no'}`
];
};
getConversionInfo(entity) {
let multiplicator = 1
for (let i = 0; i < entity.preheaters.length; i++){
multiplicator += entity.preheaters[i].multiplicator;
}
const texts = [
entity.constructor.name,
`Done: ${Math.floor(entity.conversion * 100)}%`,
`Silo: ${entity.isNextToSilo ? 'yes' : 'no'}`
];
if (entity.preheaters.length > 0) {
texts.push(`Pre-Heaters: ${entity.preheaters.length}`);
};
if (entity.constructor.name === 'Converter64') {
if (entity.reflectorCount > 0) {
texts.push(`Reflectors: ${entity.reflectorCount}`);
}
texts.push(`Output: ${(32768 + entity.reflectorCount * 8192) * 4}`);
} else {
texts.push(`${multiplicator}x`);
}
return texts;
};
drawBox(position, text, color) {
const ctx = game.ctx;
let width = 0;
let height = 0;
const heights = [];
for (const line of text) {
const m = ctx.measureText(line);
const lineHeight = (m.fontBoundingBoxAscent + m.fontBoundingBoxDescent);
width = Math.max(width, m.width);
height += lineHeight;
heights.push(lineHeight);
}
const boxWidth = width + (this.calculatedPadding * 2);
const boxHeight = height + (this.calculatedFontSize / 2);
const xy = game.uvToXY(position);
const dx = xy[0] - (boxWidth / 2);
const dy = xy[1] - (boxHeight / 2) - (this.calculatedFontSize * 2);
ctx.fillStyle = color ?? this.configuredOptions.defaultColor;
ctx.fillRect(dx, dy, boxWidth, boxHeight);
ctx.fillStyle = this.configuredOptions.fontColor ?? '#FFF';
let oy = dy + (this.calculatedFontSize / 2) + this.calculatedPadding;
for (const i in text) {
ctx.fillText(text[i], xy[0], oy);
oy += heights[i];
}
};
}
/*
* Sixty-Four Mod: Pause
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* ----------------------------------------------
*
* REQUIRES THE MOD AUTOLOADER
* See https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-_readme-md
*
* ----------------------------------------------
*
* Adds the ability to pause time in the game while still being able
* to build and destroy buildings.
*
* Press "P" to pause and resume.
*
* Note: this mod requires the "autoloader" mod unless you're on the
* "destabilized" beta branch.
*/
module.exports = class Pause extends Mod
{
label = 'Pause';
description = 'Lets you pause the game (but still build/delete/move structures) by pressing "P".';
version = '1.0.0';
paused = false;
getMethodReplacements() {
const self = this;
return [
{
class: Game,
method: 'updateLoop',
replacement: function() {
if (self.paused) {
return;
}
self.originalMethods.Game.updateLoop.call(this);
}
},
{
class: Game,
method: 'createResourceTransfer',
replacement: function(r,p,d,f,v) {
if (self.paused) {
// Make VFX not visible in main game plane.
v = [0];
if (typeof f === 'function') {
// The resource transfer has an oncomplete event, but due to having stopped the game loop,
// that event will never fire. So we fire it immediately instead (and just for good measure
// we convert it to a NOOP).
f();
f = _ => {};
}
}
self.originalMethods.Game.createResourceTransfer.call(this, r,p,d,f,v);
}
}
];
};
pause() {
this.paused = true;
};
resume() {
this.paused = false;
(game ?? window.game).time.lt = performance.now();
(game ?? window.game).clock.postMessage(true);
};
init() {
const self = this;
addEventListener('keydown', function (event) {
if (event.key !== 'p') {
return;
}
if (self.paused) {
self.resume();
} else {
self.pause();
}
});
};
};
/*
* Sixty-Four Mod: Reload
*
* https://sixtyfour.game-vault.net/wiki/Modding:Index
*
* ----------------------------------------------
*
* REQUIRES THE MOD AUTOLOADER
* See https://gist.github.com/NamelessCoder/26be6b5db7480de09f9dfb9e80dee3fe#file-_readme-md
*
* ----------------------------------------------
*
* Adds a "RELOAD" link to the main menu. Useful for mod authors; a reload is significantly
* faster than restarting the game.
*/
module.exports = class Reload extends Mod
{
label = 'Reload';
description = 'Lets you reload the game from the main menu. Useful for mod authors; a reload is significantly '
+ 'faster than restarting the game.';
version = '1.0.0';
getMethodReplacements() {
const self = this;
return [
{
class: Splash,
method: 'init',
replacement: function() {
console.log('Applying reload link');
self.originalMethods.Splash.init.call(this);
const menu = this.element.getElementsByClassName('menu')[0];
const quit = menu.removeChild(menu.lastChild);
const reset = menu.removeChild(menu.lastChild);
const reload = document.createElement('div');
reload.classList.add('menuItem');
reload.innerHTML = 'RELOAD';
reload.onclick = _ => {
document.location.reload();
};
menu.append(reload);
menu.append(reset);
menu.append(quit);
}
}
];
};
};
@YPetremann
Copy link

The autoloader strategy is quite interesting, I think we should start making more isolated mods:
Actually an IIFE with checking for c?.enabled is not really pretty

What I propose is that settings can be a function and there is a inject function instead of the IIFE, both function would have a meta argument:

label = 'Example Mod';
description = 'This is the description of anexample mod.';
function settings(meta) {
    return {
       speed: { default: 2, label: 'Speed', description: 'How quickly the Resonators attack adjacent blocks.' },
       power: {
           default: 1,
           label: 'Power',
           description: 'How hard each Resonator attack hits the adjacent block(s).'
       }
    }
}
function inject(meta, settings) {
  let _abstract_getCodex = abstract_getCodex;
  abstract_getCodex = function () {
    const dat = _abstract_getCodex();
    for (const name in dat.entities) {
      const ent = dat.entities[name];
      if (ent.priceExponent > 1) ent.priceExponent = 1;
    }
    return dat;
  };
}

Meta is an object:

  • mods: a frozen list of enabled mods
  • settings: every enabled mods settings (mods could be allowed to patch other mods settings)

@NamelessCoder
Copy link
Author

NamelessCoder commented Mar 25, 2024

I was thinking more about going in the direction of providing a base class, e.g. class Mod which implements a shared set of methods like getSettings(), getLabel() etc. and have those methods' default implementation return various properties of the class. Then, a module could define itself as a class extending the shared class and write a constructor that sets the desired internal properties such as label, settings etc.

Allowing a mod to read other mods' settings is reasonable but it should also receive the specific settings that applies to it (as in: we don't want each mod to have to dig through all settings to find the ones that apply to itself). It should take its own settings as first constructor parameter, and could take a full mod manifest (with names, labels, possible settings, current settings, enabled status etc. of every mod) as second argument. If it makes sense we could insert references to the instanced mods in this manifest so a mod that loads after another mod would be able to call methods on the already loaded mods' instances - but would not be able to do so for mods that load after itself (need dependency ordering, too).

The mod's class should then also have a method like init() which is called after all the built-in stuff is called, in case it needs to further initialize itself, so that initialization does not have to occur in the constructor. At the time init() is called, the mod manifest would also be complete, giving a mod a last-chance way to interact with even mods that are loaded after itself. And if a mod isn't enabled, it is still loaded and instanced so settings etc. could be extracted - but init() would not be called.

The class contained within the mod file should conform to the file name, so that a mod file named FasterResonators.js should contain a class named FasterResonators - and if it does not, the mod loader could either throw an error or assume the mod uses an IIFE (and trigger some legacy case that skips processing settings, label, etc.).

I also have some other ideas about letting such a mod class return functions that should override game class functions and let them specify if the function should completely replace the original, or should be called before or after calling the original method.

Feel free to catch me on the Discord for a more realtime discussion!

@NamelessCoder
Copy link
Author

@YPetremann I've updated the autoloader and all mods in this collection to showcase my proposal. Each mod is now a separate class based on a common class that's provided by the autoloader. The class is exported as a module and has various methods to retieve label, settings etc.

There is an API to define which prototype methods a mod should replace along with a formalized storage of the replaced methods so an override can call the original and all original methods are in a predictable scope. Each mod has a constructor where all attributes can be registered - and an init() method that is only fired if the module is enabled.

It should fit all of your ideas and more. And encapsulation is even better than simply declaring functions.

Unfortunately the modules are then no longer compatible with the old naive loading style but I think that's a worth-while tradeoff. I will also be proposing this pattern as an official feature for the game when it has spent a bit of time maturing and getting tested, so hopefully it becomes the default way of defining modules.

Still happy to take feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment