/**
* @module index
* @exports index
* @requires group
* @requires color
* @requires block
*/
const {
on,
touch,
touch_end,
collision,
collision_exit,
death,
count,
x_position,
event,
gamescene,
frame
} = require('./lib/events');
const {
spawn_trigger,
remappable,
sequence,
call_with_delay,
equal_to,
less_than,
greater_than,
for_loop,
frame_loop,
frames
} = require('./lib/control-flow');
const {
item_edit,
item_comp,
timer,
compare
} = require('./lib/items');
const {
camera_offset,
camera_static,
camera_zoom,
camera_mode,
camera_rotate,
camera_edge,
song,
teleport,
move_trigger,
timewarp,
color_trigger,
toggle_on_trigger,
toggle_off_trigger,
hide_player,
gradient,
random,
advanced_random,
gravity,
options,
end,
player_control,
particle_system,
spawn_particle
} = require('./lib/general-purpose');
const {
shader_layers,
shader_layer,
sepia,
hue_shift,
grayscale,
pixelate,
chromatic,
glitch,
bulge,
split_screen
} = require('./lib/shaders.js');
const keyframe_system = require('./lib/keyframes.js');
const particle_props = require('./properties/particles.js');
const events = require('./properties/game_events.js');
const log = require('./lib/log.js');
const WebSocket = require('ws');
const crypto = require('crypto');
const LevelReader = require('./reader');
const $group = require('./types/group.js');
const $color = require('./types/color.js');
const $block = require('./types/block.js');
const d = require('./properties/obj_props.js');
const { counter, float_counter } = require('./lib/counter');
let explicit = {};
/**
* Extracts values from dictionary into global scope
* @param {dictionary} dict Dictionary to extract
*/
let extract = (x) => {
for (let i in x) {
global[i] = x[i];
}
};
global.all_known = {
groups: [],
colors: [],
blocks: []
}
let [unavailable_g, unavailable_c, unavailable_b] = [0, 0, 0];
let get_new = (n, prop, push = true) => {
let arr = all_known[prop];
if (arr.length == 0) {
arr.push(1);
return 1;
}
arr.sort((a, b) => a - b);
if (arr[0] > 1 && push) {
arr.unshift(1);
return 1;
}
let result;
for (let i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i - 1] + 1) {
result = arr[i - 1] + 1;
break;
}
}
if (!result) result = arr[arr.length - 1] + 1;
if (push) all_known[prop].push(result);
return result;
};
/**
* Creates and returns an unavailable group ID
* @returns {group} Resulting group ID
*/
let unknown_g = () => {
// todo: make this not use group 0
unavailable_g++;
unavailable_g = get_new(unavailable_g, 'groups');
return new $group(unavailable_g);
};
/**
* Creates and returns an unavailable color ID
* @returns {color} Resulting color ID
*/
let unknown_c = () => {
unavailable_c++;
unavailable_c = get_new(unavailable_c, 'colors');
return new $color(unavailable_c);
};
/**
* Creates and returns an unavailable block ID
* @returns {block} Resulting block ID
*/
let unknown_b = () => {
unavailable_b++;
unavailable_b = get_new(unavailable_b, 'blocks');
return new $block(unavailable_b);
};
/**
* @typedef {object} context
* @property {string} name Name of the current context
* @property {group} group Group representing the current context
* @property {array} objects All objects in the current context
* @property {array} children Child contexts
*/
/**
* Class allowing you to interfere with contexts, which are wrappers that assign groups to different objects
* @class
* @constructor
* @public
*/
class Context {
/**
* Creates a new context
* @param {string} name Name of context
* @param {boolean} [setToDefault=false] Whether to automatically switch to the context
* @param {group} [group=unknown_g()] The group to give to the context
*/
constructor(name, setToDefault = false, group = unknown_g()) {
this.name = name;
this.group = group;
this.objects = [];
this.children = [];
Context.last_contexts[name] = name;
if (setToDefault) Context.set(name);
Context.add(this);
}
static last_contexts = {};
static last_context_children = {};
/**
* The name of the current context
*/
static current = "global";
/**
* A list of all contexts added
*/
static list = {};
/**
* Switches the context
* @param {string|group} name Name or group of context to switch to
*/
static set(name) {
if (typeof name == 'object' && name?.value) {
Context.current = Context.findByGroup(name).name;
return;
};
Context.current = name;
}
/**
* Converts an object into a context
* @param {context} context Object to convert into a context
*/
static add(context) {
Context.list[context.name] = context;
}
/**
* Adds an object into the current context
* @param {object} objectToAdd Object to add into current context
*/
static addObject(objectToAdd) {
if (objectToAdd.type == "object") {
objectToAdd = callback_objects_fn(objectToAdd) || objectToAdd;
Context.findByName(Context.current).objects.push(objectToAdd.obj_props);
return;
}
Context.findByName(Context.current).objects.push(objectToAdd);
}
/**
* Links an existing context into the current one, allowing you to find the parent context of another context
* @param {context} context Context to link into current
* @param {string} ctxLink Optional context that should be the parent of input context
*/
static link(context, ctxLink = undefined) {
let input_context = Context.findByName(context);
let curr_ctx = !ctxLink ? Context.findByName(Context.current) : Context.findByName(ctxLink);
if (Context.isLinked(input_context)) {
input_context.linked_to = curr_ctx.linked_to;
Context.last_context_children[input_context.linked_to] = context;
} else {
input_context.linked_to = curr_ctx.name;
Context.last_contexts[context] = input_context.name;
Context.last_context_children[context] = curr_ctx.name;
}
curr_ctx.children.push(context);
}
/**
* Checks if a context has a parent
* @param {context} ctx Context to check for parent
* @returns {boolean} Whether context has a parent
*/
static isLinked(ctx) {
return 'linked_to' in ctx;
}
/**
* Finds a context based off of its assigned group
* @param {group} groupToSearch
* @returns {context} Found context
*/
static findByGroup(groupToSearch) {
if (typeof groupToSearch == "number") {
groupToSearch = group(groupToSearch);
} else if (!groupToSearch instanceof $group) {
throw new Error(`Expected number or $group instance, got ${groupToSearch} with type ${typeof groupToSearch}`)
}
for (const key in Context.list) {
if (Context.list[key].group.value == groupToSearch.value) {
return Context.list[key];
}
}
}
/**
* Finds a context based off of its name
* @param {string} name Name of the context
* @returns {context} Found context
*/
static findByName(name) {
return Context.list[name];
}
}
Context.add(new Context("global"))
let findDeepestChildContext = (name) => {
let cond = true;
let res_name = name;
while (cond) {
cond = !!Context.last_context_children[name];
if (cond) {
res_name = Context.last_context_children[res_name];
cond = !!Context.last_context_children[res_name];
} else { break };
}
return res_name;
};
/**
* Creates a repeating trigger system that repeats while a condition is true
* @param {condition} condition Condition that defines whether the loop should keep on running (less_than/equal_to/greater_than(counter, number))
* @param {function} func Function to run while the condition is true
* @param {number} delay Delay between each cycle
*/
let while_loop = (r, triggerFunction, del = 0.05) => {
let { count, comparison, other } = r;
let oldContextName = Context.current;
let newContext = new Context(crypto.randomUUID());
let check_func;
if (oldContextName == "global") {
check_func = trigger_function(() => {
compare(count, comparison, other, newContext.group);
});
} else {
compare(count, comparison, other, newContext.group);
}
Context.set(newContext.name);
triggerFunction(newContext.group);
Context.set(oldContextName);
triggerFunction = newContext.group;
let context = Context.findByGroup(triggerFunction);
let currentG = Context.findByName(findDeepestChildContext(context.name)).group;
if (!currentG) {
currentG = triggerFunction;
}
$.extend_trigger_func(currentG, () => {
// Context.findByName(oldContextName)
oldContextName == "global" ? check_func.call(del) : Context.findByName(oldContextName).group.call(del);
});
if (check_func) check_func.call(del);
};
let writeClasses = (arr) => {
arr.forEach((class_) => {
let clases = class_.split('/');
let clas = clases.shift();
clases.forEach((expl) => {
if (explicit[expl]) {
explicit[expl] = [explicit[expl], clas];
return;
}
explicit[expl] = clas;
});
});
/**
* Converts a number to a group
* @global
* @param {number} x - The number to convert to a group.
* @returns {group}
*/
global.group = (x) => new $group(x);
/**
* Converts a number to a color
* @global
* @param {number} x - The number to convert to a color.
* @returns {color}
*/
global.color = (x) => new $color(x);
/**
* Converts a number to a block
* @global
* @param {number} x - The number to convert to a block.
* @returns {block}
*/
global.block = (x) => new $block(x);
}
writeClasses([
'group/TARGET/GROUPS/GR_BL/GR_BR/GR_TL/GR_TR/TRUE_ID/FALSE_ID/ANIMATION_GID/TARGET_POS/FOLLOW/CENTER/TARGET_DIR_CENTER/SHADER_CENTER_ID',
'color/TARGET/TARGET_COLOR/COLOR/COLOR_2/SHADER_TINT_CHANNEL',
'block/BLOCK_A/BLOCK_B',
]);
/**
* @typedef {dictionary} object
* @property {string} type String dictating that the type of the resulting dictionary is an object
* @property {dictionary} obj_props Dictionary inside of object holding the actual object properties of the object
* @property {function} with Modifies/adds an object property (e.g. `object.with(obj_props.X, 15)`)
* @property {function} add Adds the object
*/
/**
* Takes a dictionary with object props & converts into an object
* @param {dictionary} dict Dictionary to convert to object
* @returns {object}
*/
let object = (dict) => {
let return_val = {
type: 'object',
obj_props: dict,
with: (prop, val) => {
if (typeof prop == "string" && isNaN(parseInt(prop))) {
dict[prop] = val;
return return_val;
}
dict[d[prop] || prop] = val;
return return_val;
},
// copied old $.add code here so I can migrate to enforcing object() usage in the future
add: () => Context.addObject(return_val)
};
return return_val;
};
/**
* Creates a "trigger function" in which triggers can be stored inside of a single group
* @param {function} callback Function storing triggers to put inside of group
* @returns {group} Group ID of trigger function
*/
let trigger_function = (cb) => {
let oldContext = Context.current;
let newContext = new Context(crypto.randomUUID(), true);
cb(newContext.group);
Context.set(oldContext);
return newContext.group;
};
/**
* Waits for a specific amount of seconds
* @param {number} time How long to wait
*/
let wait = (time) => {
if (time == 0) return;
let id = crypto.randomUUID();
let oldContext = Context.current;
let newContext = new Context(id);
$.add(spawn_trigger(newContext.group, time));
Context.set(id);
Context.link(newContext.name, oldContext);
};
let reverse = {};
for (var i in d) {
reverse[d[i]] = i;
}
// stuff for custom things
let dot_separated_keys = [57, 442];
dot_separated_keys = dot_separated_keys.map(x => x.toString())
let levelstring_to_obj = (string) => {
let objects = [];
string
.split(';')
.slice(0, -1)
.forEach((x) => {
let r = {};
let spl = x.split(',');
spl.forEach((x, i) => {
if (!(i % 2)) {
let obj_prop = parseInt(x);
let value = spl[i + 1];
if (value.includes('.') && dot_separated_keys.includes(obj_prop)) value = value.split('.').map(x => parseInt(x));
if (!isNaN(parseInt(value))) value = parseInt(value);
r[d[obj_prop] || obj_prop] = value;
}
});
objects.push(r);
});
return objects;
};
/**
* Helper functions and variables holding existing level info.
* @namespace level
*/
let level = {
/**
* Array of all objects in the level.
* @type {Array<Object>}
*/
objects: [],
/**
* Raw level string of the current level.
* @type {string}
*/
raw_levelstring: '',
/**
* Returns an array of all the objects in the level with a property whose value matches the pattern.
* @param {string|number} prop - The property to check in each object.
* @param {Function} pattern - The function to test the property value.
* @returns {Array<Object>} An array of objects that match the given property and pattern.
*/
get_objects: function (prop, pattern) {
let level_arr = levelstring_to_obj(this.raw_levelstring);
return level_arr.filter(o => {
let cond_1 = prop in o, cond_2;
if (cond_1) cond_2 = pattern(o[prop]);
return cond_1 && cond_2;
});
}
};
let mappings = {
696969: '80',
420420: '80',
6942069: '95',
6969: '51',
42069420: '88',
32984398: '51',
584932: '71',
78534: '480',
45389: '481',
93289: '482',
8754: '51',
8459: '71',
3478234: '71',
45893: '392',
237894: '10',
347832: '70',
34982398: '188',
4895490381243: '51',
45890903: '290',
487999230: '179',
40943900394: '191',
93423877: '181',
8765437289: '182',
645789320: '180',
8765434: '188',
21678934: '190'
};
let find_free = (str) => {
let startIndex = 0;
let endIndex;
while ((endIndex = str.indexOf(';', startIndex)) !== -1) {
let segment = str.substring(startIndex, endIndex);
startIndex = endIndex + 1;
if (!segment) continue;
level.objects.push(segment);
level.raw_levelstring += segment + ';';
let ro = segment.split(',');
for (let i = 0; i < ro.length; i += 2) {
let key = ro[i];
let value = ro[i + 1];
switch (key) {
case "57":
let detected_groups = value.split('.').map(Number).filter(num => num !== remove_group);
for (let group of detected_groups) {
if (!all_known.groups.includes(group)) all_known.groups.push(group);
unavailable_g = get_new(group, 'groups', false);
}
break;
case "21":
case "22":
let detected_color = parseInt(value);
if (!all_known.colors.includes(detected_color)) all_known.colors.push(detected_color);
unavailable_c = get_new(detected_color, 'colors', false);
break;
case "80":
case "95":
let detected_block = parseInt(value);
if (!all_known.blocks.includes(detected_block)) all_known.blocks.push(detected_block);
unavailable_b = get_new(detected_block, 'blocks', false);
break;
}
}
}
};
let obj_to_levelstring = (l) => {
let res = '';
// { x: 15, Y: 10 };
for (var d_ in l) {
let val = l[d_];
let key = reverse[d_];
if (!isNaN(parseInt(d_))) key = d_
if (typeof val == 'boolean') val = +val;
if (explicit[d_] && !val.hasOwnProperty('value')) { // if type is explicitly required for current object property and it is not a group/color/block
if (typeof val == 'object' && dot_separated_keys.includes(key)) { // if val is an array and it is dot separated
val = val.map((x) => x.value).filter(x => x && x != '').join('.');
} else {
throw `Expected type "${explicit[d[parseInt(key)]]
}", got "${typeof val}"`;
}
} else if (explicit[d_] && val.value) {
let cond = typeof explicit[d_] == "string" ? (val.type == explicit[d_]) : (explicit[d_].includes(val.type));
if (cond) {
val = val.value;
} else {
throw `Expected type "${explicit[d_]}", got "${val.type}"`;
}
}
if (mappings.hasOwnProperty(key)) {
key = mappings[key];
}
res += `${key},${val},`;
}
return res.slice(0, -1) + ';';
};
let resulting = '';
let add = (...objects) => {
objects.forEach(o => {
if (o?.type !== "object") {
process.emitWarning('Using plain dictionaries as an argument to $.add is deprecated and using the object() function will be enforced in the future.', {
type: 'DeprecationWarning',
detail: 'Wrap the object() function around the dictionary as an argument to $.add instead of using plain dictionaries.'
});
}
if (o?.type == "object") {
o.add(); // does the same thing as below, only reason $.add is not removed is so I can customize $.add in the future
return;
};
let newo = o;
if (newo.with) delete newo.with;
Context.addObject(newo);
});
};
let remove_group = 9999;
let already_prepped = false;
let indexOfFrom = (array, value, startIndex) => {
let newArr = array.slice(startIndex);
return newArr.indexOf(value) !== -1 ? newArr.indexOf(value) + (array.length - newArr.length) : -1;
}
let ifInGroups = (groupsArr, group) => {
groupsArr?.value == undefined ? (groupsArr.map(x => x.value).includes(group.value)) : groupsArr.value == group.value;
}
// removes contexts if they are empty + any unnecessary triggers associating with them
let optimize = () => {
for (let child in Context.last_context_children) {
let parent = Context.list[Context.last_context_children[child]];
let childCtx = Context.list[child];
// handles empty contexts
if (childCtx.objects.length === 0) {
delete Context.list[child];
let emptyCallIndex = parent.objects.map(x => {
if (!x?.TARGET) return false;
return x.TARGET.value == childCtx.group.value;
});
parent.objects = parent.objects.filter((_, i) => i !== indexOfFrom(emptyCallIndex, true, i));
}
};
};
let prep_lvl = (optimize_op = true, replace = true) => {
if (already_prepped) return;
if (optimize_op) optimize();
let name = 'GLOBAL_FULL';
Context.add(new Context(name, true, group(0)))
Context.last_contexts[name] = name;
// contexts.global.group.call();
for (let i in Context.list) {
// undefined prop filter
for (let object in Context.findByName(i).objects) {
object = Context.findByName(i).objects[object];
for (let prop in object) {
if (object[prop] == undefined) delete object[prop];
};
}
// main preparation
if (!(+(i !== 'GLOBAL_FULL') ^ +(i !== 'global'))) { // XOR if it was logical
let context = Context.findByName(i);
// gives groups to objects in context
let objects = context.objects;
for (let i = 0; i < objects.length; i++) {
let object = objects[i];
if (replace) {
if (!object.GROUPS) {
object.GROUPS = [context.group, group(remove_group)];
} else {
if (Array.isArray(object.GROUPS)) {
object.GROUPS.push(context.group, group(remove_group));
} else {
object.GROUPS = [object.GROUPS, context.group, group(remove_group)];
}
}
} else {
if (!object.GROUPS) {
object.GROUPS = [context.group];
} else {
if (Array.isArray(object.GROUPS)) {
object.GROUPS.push(context.group);
} else {
object.GROUPS = [object.GROUPS, context.group];
}
}
}
if (!(object.hasOwnProperty("SPAWN_TRIGGERED") || object.hasOwnProperty(obj_props.SPAWN_TRIGGERED))) {
object.SPAWN_TRIGGERED = 1;
}
if (!(object.hasOwnProperty("MULTI_TRIGGER") || object.hasOwnProperty(obj_props.MULTI_TRIGGER))) {
object.MULTI_TRIGGER = 1;
}
// end
}
} else {
let context = Context.findByName(i);
let objects = context.objects;
for (let i = 0; i < objects.length; i++) {
let object = objects[i];
if (replace) {
if (!object.GROUPS) {
object.GROUPS = group(remove_group);
} else {
if (Array.isArray(object.GROUPS)) {
object.GROUPS.push(group(remove_group));
} else {
object.GROUPS = [object.GROUPS, group(remove_group)];
}
}
}
}
}
for (let x in Context.findByName(i).objects) {
let r = obj_to_levelstring(Context.findByName(i).objects[x]);
resulting += r;
}
}
already_prepped = true;
};
let limit = remove_group;
let warn_lvlstr = true;
let getLevelString = (options = {}) => {
if (warn_lvlstr) process.emitWarning('Using $.getLevelString is deprecated and will be removed in the future.', {
type: 'DeprecationWarning',
detail: `Migrate by using \`await $.exportConfig({ type: 'levelstring', options: <your options here> })\`. Note that instead of using $.exportConfig at the end of the program, use it at the top, below the G.js import but above all other code.`
});
prep_lvl();
if (unavailable_g <= limit) {
if (options.info) {
console.log('Finished, result stats:');
console.log('Object count:', resulting.split(';').length - 1);
console.log('Group count:', unavailable_g - 1);
console.log('Color count:', unavailable_c);
}
} else {
if (
(options.hasOwnProperty('group_count_warning') &&
options.group_count_warning == true) ||
!options.hasOwnProperty('group_count_warning')
)
throw new Error(`Group count surpasses the limit! (${unavailable_g}/${limit})`);
}
return resulting;
};
/**
* @typedef {object} easing
* @property {number} EASE_IN_OUT Ease in out easing
* @property {number} EASE_IN Ease in easing
* @property {number} EASE_OUT Ease out easing
* @property {number} EXPONENTIAL_IN_OUT Exponential in out easing
* @property {number} EXPONENTIAL_IN Exponential in easing
* @property {number} EXPONENTIAL_OUT Exponential out easing
* @property {number} SINE_IN_OUT Sine in out easing
* @property {number} SINE_IN Sine in easing
* @property {number} SINE_OUT Sine out easing
* @property {number} ELASTIC_IN_OUT Elastic in out easing
* @property {number} ELASTIC_IN Elastic in easing
* @property {number} ELASTIC_OUT Elastic out easing
* @property {number} BACK_IN_OUT Back in out easing
* @property {number} BACK_IN Back in easing
* @property {number} BACK_OUT Back out easing
* @property {number} BOUNCE_IN_OUT Bounce in out easing
* @property {number} BOUNCE_IN Bounce in easing
* @property {number} BOUNCE_OUT Bounce out easing
*/
let easings = {
ELASTIC_OUT: 6,
BACK_IN_OUT: 16,
BOUNCE_IN: 8,
BACK_OUT: 18,
EASE_OUT: 3,
EASE_IN: 2,
EASE_IN_OUT: 1,
ELASTIC_IN_OUT: 4,
BOUNCE_OUT: 9,
EXPONENTIAL_IN: 11,
EXPONENTIAL_OUT: 12,
SINE_IN_OUT: 13,
BOUNCE_IN_OUT: 7,
SINE_IN: 14,
ELASTIC_IN: 5,
SINE_OUT: 15,
EXPONENTIAL_IN_OUT: 10,
BACK_IN: 17,
NONE: 0,
};
extract(easings);
global.obj_props = reverse;
let callback_objects_fn = x => x;
let callback_objects = (cb) => {
callback_objects_fn = cb
};
let extend_trigger_func = (t, cb) => {
const context = Context.findByGroup(t);
const oldContext = Context.current;
Context.set(Context.last_contexts[context.name]);
cb(context.group);
Context.set(oldContext);
};
let remove_past_objects = (lvlstring) => {
// remove_group
return lvlstring.split(';').filter(x => {
let keep = true;
let spl = x.split(',');
spl.forEach((z, i) => {
if (!(i % 2)) {
if (z == "57") {
let groups = spl[i + 1]
if (groups.includes('.')) {
groups = groups.split('.');
if (groups.includes(remove_group.toString())) {
keep = false;
}
} else {
if (groups == remove_group) {
keep = false;
}
}
}
}
})
return keep;
}).join(';');
}
let exportToSavefile = (options = {}) => {
process.emitWarning('Using $.exportToSavefile is deprecated and will be removed in the future.', {
type: 'DeprecationWarning',
detail: `Migrate by using \`await $.exportConfig({ type: 'savefile', options: <your options here> })\`. Note that instead of using $.exportConfig at the end of the program, use it at the top, below the G.js import but above all other code.`
});
(async () => {
const level = await new LevelReader(options.level_name, options.path);
let last = remove_past_objects(level.data.levelstring, level.data.name);
prep_lvl();
if (unavailable_g <= limit) {
if (options.info) {
console.log(`Writing to level: ${level.data.name}`);
console.log('Finished, result stats:');
console.log('Object count:', resulting.split(';').length - 1);
console.log('Group count:', unavailable_g);
console.log('Color count:', unavailable_c);
}
} else {
if (
(options.hasOwnProperty('group_count_warning') &&
options.group_count_warning == true) ||
!options.hasOwnProperty('group_count_warning')
)
throw new Error(`Group count surpasses the limit! (${unavailable_g}/${limit})`);
}
last += resulting;
level.set(last);
await level.save();
})()
};
/**
* @typedef {Object} export_config
* @property {string} type Type of export (can be "levelstring", "savefile" or "live_editor")
* @property {save_config} options Configuration for specific export type
*/
/**
* One-size-fits-all function for exporting a level to GD
* @param {export_config} conf Configuration for exporting level
* @returns {null|string} Levelstring if using "levelstring" type, otherwise null
*/
let exportConfig = (conf) => {
return new Promise(async (resolve) => {
let options = conf.options;
if (conf?.options?.replacePastObjects == undefined) {
conf.options.replacePastObjects = true;
options.replacePastObjects = true;
}
if (conf?.options?.removeGroup !== undefined) {
remove_group = typeof conf.options.removeGroup == "number" ? conf.options.removeGroup : conf.options.removeGroup?.value
};
switch (conf.type) {
case "levelstring":
prep_lvl(conf?.options?.optimize, conf?.options?.replacePastObjects);
if (unavailable_g <= limit) {
if (options?.info) {
console.log('Finished, result stats:');
console.log('Object count:', resulting.split(';').length - 1);
console.log('Group count:', unavailable_g);
console.log('Color count:', unavailable_c);
}
} else {
if (
(options?.hasOwnProperty('group_count_warning') &&
options?.group_count_warning == true) ||
!options?.hasOwnProperty('group_count_warning')
)
throw new Error(`Group count surpasses the limit! (${unavailable_g}/${limit})`);
}
resolve(resulting);
break;
case "savefile":
const sf_level = await new LevelReader(options?.level_name, options?.path, options?.reencrypt);
if (!sf_level.data.levelstring) throw new Error(`Level "${sf_level.data.name}" has not been initialized, add any object to initialize the level then rerun this script`);
let last = conf?.options?.replacePastObjects ? remove_past_objects(sf_level.data.levelstring, sf_level.data.name) : sf_level.data.levelstring;
find_free(last);
resolve(true);
process.on('beforeExit', error => {
if (!error) {
prep_lvl(conf?.options?.optimize, conf?.options?.replacePastObjects);
if (unavailable_g <= limit) {
if (options?.info) {
console.log(`Writing to level: ${sf_level.data.name}`);
console.log('Finished, result stats:');
console.log('Object count:', resulting.split(';').length - 1);
console.log('Group count:', unavailable_g);
console.log('Color count:', unavailable_c);
}
} else {
if (
(options.hasOwnProperty('group_count_warning') &&
options.group_count_warning == true) ||
!options.hasOwnProperty('group_count_warning')
)
throw new Error(`Group count surpasses the limit! (${unavailable_g}/${limit})`);
}
last += resulting;
sf_level.set(last);
sf_level.save();
process.exit(0);
}
});
break;
case "live_editor":
let socket = new WebSocket('ws://127.0.0.1:1313');
socket.addEventListener('message', (event) => {
event = JSON.parse(event.data);
if (event.response) {
find_free(event.response.split(';').slice(1).join(';'));
level.raw_levelstring = event.response;
resolve(true);
}
if (event.status !== "successful") throw new Error(`Live editor failed, ${event.error}: ${event.message}`)
});
socket.addEventListener('open', (event) => {
if (conf?.options?.replacePastObjects) {
socket.send(JSON.stringify({
action: 'REMOVE_OBJECTS',
group: remove_group,
})); // clears extra objects
}
socket.send(JSON.stringify({
action: 'GET_LEVEL_STRING',
close: true
})); // thing to get free groups
process.on('beforeExit', error => {
if (!error) {
let socket2 = new WebSocket('ws://127.0.0.1:1313');
socket2.addEventListener('message', (event) => {
event = JSON.parse(event.data);
if (event.response) {
find_free(event.response.split(';').slice(1).join(';'));
}
if (event.status !== "successful") throw new Error(`Live editor failed, ${event.error}: ${event.message}`)
});
socket2.addEventListener('open', async (event) => {
let pre_lvlstr = await exportConfig({ type: "levelstring", options });
let lvlString = group_arr(pre_lvlstr.split(';'), 250).map(x => x.join(';'));
if (!error) {
lvlString.forEach((chunk, i) => {
setTimeout(() => {
socket2.send(JSON.stringify({
action: 'ADD_OBJECTS',
objects: chunk + ';',
close: i == lvlString.length - 1
}));
if (i == lvlString.length - 1) process.exit(0);
}, i * 75);
});
}
});
}
});
});
socket.addEventListener('error', () => {
throw new Error(`Connecting to WSLiveEditor failed! Make sure you have installed the WSLiveEditor mod inside of Geode and have the editor open!`);
});
break;
default: throw new Error(`The "${conf.type}" configuration type is not valid!`)
}
});
};
const group_arr = (arr, x) => arr.reduce((acc, _, i) => (i % x ? acc[acc.length - 1].push(arr[i]) : acc.push([arr[i]]), acc), []);
let liveEditor = (conf) => {
process.emitWarning('Using $.liveEditor is deprecated and will be removed in the future.', {
type: 'DeprecationWarning',
detail: `Migrate by using \`await $.exportConfig({ type: 'live_editor', options: <your options here> })\`. Note that instead of using $.exportConfig at the end of the program, use it at the top, below the G.js import but above all other code.`
});
const socket = new WebSocket('ws://127.0.0.1:1313');
warn_lvlstr = false;
let lvlString = group_arr($.getLevelString(conf).split(';'), 250).map(x => x.join(';'));
socket.addEventListener('message', (event) => {
event = JSON.parse(event.data);
if (event.status !== "successful") throw new Error(`Live editor failed, ${event.error}: ${event.message}`)
});
socket.addEventListener('open', (event) => {
socket.send(JSON.stringify({
action: 'REMOVE',
group: remove_group,
})); // clears extra objects
lvlString.forEach((chunk, i) => {
setTimeout(() => {
socket.send(JSON.stringify({
action: 'ADD',
objects: chunk + ';',
close: i == lvlString.length - 1
}));
}, i * 75);
});
});
socket.addEventListener('error', () => {
throw new Error(`Connecting to WSLiveEditor failed! Make sure you have installed the WSLiveEditor mod inside of Geode!`);
});
};
/**
* Configuration for exporting levels.
* @typedef {object} save_config
* @property {boolean} [info=false] Whether to log information to console when finished
* @property {boolean} [group_count_warning=true] Whether to warn that group count is surpassed (only useful if in future updates the group count is increased)
* @property {string} [level_name=by default, it writes to your most recent level/topmost level] Name of level (only for exportToSavefile)
* @property {string} [path=path to savefile automatically detected based off of OS] Path to CCLocalLevels.dat savefile (only for exportToSavefile)
* @property {boolean} [reencrypt=true] Whether to reencrypt savefile after editing it, or to let GD encrypt it
* @property {boolean} [optimize=true] Whether to optimize unused groups & triggers that point to unused groups
* @property {boolean} [replacePastObjects=true] Whether to delete all objects added by G.js in the past & replace them with the new objects
* @property {number|group} [removeGroup=9999] Group to use to mark objects to be automatically deleted when re-running the script (default is 9999)
*/
/**
* Core type holding important functions for adding to levels, exporting, and modifying scripts.
* @namespace $
*/
let $ = {
/**
* Adds an object.
* @param {object} object - Object to add (wrap `object()` function around a dictionary).
*/
add,
/***
* Configures how to export the script to Geometry Dash (must be used at the top of the script)
* @param {export_config} config Configuration for exporting
*/
exportConfig,
/**
* Prints to console.
* @param {*} value - Value to print.
*/
print: (...args) => console.log(...args),
/**
* Returns level string of the script.
* @deprecated Replaced by `$.exportConfig({ type: "levelstring", options: ... })`
* @param {save_config} config - Configuration for exporting to level string.
* @returns {string} Resulting level string.
*/
getLevelString,
/**
* Exports script to savefile.
* @deprecated Replaced by `$.exportConfig({ type: "savefile", options: ... })`
* @param {save_config} config - Configuration for exporting to savefile.
*/
exportToSavefile,
/**
* Exports script to live editor using WSLiveEditor (requires Geode).
* @deprecated Replaced by `$.exportConfig({ type: "live_editor", options: ... })`
* @param {save_config} config - Configuration for exporting to live editor.
*/
liveEditor,
/**
* Maps every trigger that gets added to the level
* @param {function} callback - Function that maps triggers added to the level.
*/
callback_objects,
/**
* Extends a trigger function by adding more triggers to it.
* @param {group} trigger_func - Trigger function to extend.
* @param {function} callback - Function that adds more triggers to `trigger_func`.
*/
extend_trigger_func,
/**
* Returns group of current trigger function context.
* @returns {group} Group of current trigger function context.
*/
trigger_fn_context: () => Context.findByName(Context.current).group
};
/**
* Ignores context changes inside of a function
* @param {function} fn Function containing code where context changes should be ignored
*/
let ignore_context_change = (fn) => {
let old_context = Context.current;
fn();
Context.set(old_context);
};
/**
* Generates an array holding a sequence of numbers starting at the "start" parameter, ending at the "end" parameter and incrementing by "step"
* @param {number} start What number to start at
* @param {number} end What number to end at
* @param {number} step What number to increment by
* @returns {array} Resulting sequence
*/
function range(start, end, step = 1) {
let sw = false;
if (start > end) {
sw = true;
[start, end] = [end, start]; // Swap start and end
step = Math.abs(step); // Ensure step is positive
}
let result = Array.from({ length: Math.ceil((end - start) / step) }, (_, i) => start + i * step);
if (sw) result = result.reverse();
return result;
};
let refs = {
types: [null, "ITEM", "TIMER", "POINTS", "TIME", "ATTEMPT"],
ops: ["EQ", "ADD", "SUB", "MUL", "DIV"],
compare_ops: [null, "GREATER", "GREATER_OR_EQ", "LESS", "LESS_OR_EQ", "NOT_EQ"],
absneg: [null, "ABS", "NEG"],
rfc: [null, "RND", "FLR", "CEI"],
};
for (let i in refs) {
i = refs[i];
i.forEach((x, i) => {
if (x) {
global[x] = i;
}
})
}
String.prototype.to_obj = function () {
let or = object({
OBJ_ID: 914,
TEXT: btoa(this)
});
return or;
};
/**
* @typedef {Object} special_objects
* @property {number} USER_COIN - Identifier for user coin
* @property {number} H_BLOCK - Identifier for H block
* @property {number} J_BLOCK - Identifier for J block
* @property {number} TEXT - Identifier for text
* @property {number} S_BLOCK - Identifier for S block
* @property {number} ITEM_DISPLAY - Identifier for item display
* @property {number} D_BLOCK - Identifier for D block
* @property {number} COLLISION_BLOCK - Identifier for collision block
*/
/**
* @typedef {Object} trigger_ids
* @property {number} SPAWN - Identifier for spawn trigger
* @property {number} ON_DEATH - Identifier for on-death trigger
* @property {number} ROTATE - Identifier for rotate trigger
* @property {number} COUNT - Identifier for count trigger
* @property {number} DISABLE_TRAIL - Identifier for disable trail trigger
* @property {number} HIDE - Identifier for hide trigger
* @property {number} PICKUP - Identifier for pickup trigger
* @property {number} COLLISION - Identifier for collision trigger
* @property {number} ENABLE_TRAIL - Identifier for enable trail trigger
* @property {number} ANIMATE - Identifier for animate trigger
* @property {number} TOUCH - Identifier for touch trigger
* @property {number} INSTANT_COUNT - Identifier for instant count trigger
* @property {number} BG_EFFECT_OFF - Identifier for BG effect off trigger
* @property {number} TOGGLE - Identifier for toggle trigger
* @property {number} MOVE - Identifier for move trigger
* @property {number} ALPHA - Identifier for alpha trigger
* @property {number} SHOW - Identifier for show trigger
* @property {number} STOP - Identifier for stop trigger
* @property {number} FOLLOW - Identifier for follow trigger
* @property {number} PULSE - Identifier for pulse trigger
* @property {number} BG_EFFECT_ON - Identifier for BG effect on trigger
* @property {number} SHAKE - Identifier for shake trigger
* @property {number} FOLLOW_PLAYER_Y - Identifier for follow player Y trigger
* @property {number} COLOR - Identifier for color trigger
*/
/**
* @typedef {Object} portal_ids
* @property {number} SPEED_GREEN - Identifier for green speed portal
* @property {number} TELEPORT - Identifier for teleport portal
* @property {number} CUBE - Identifier for cube portal
* @property {number} MIRROR_OFF - Identifier for mirror off portal
* @property {number} WAVE - Identifier for wave portal
* @property {number} SPIDER - Identifier for spider portal
* @property {number} SPEED_RED - Identifier for red speed portal
* @property {number} GRAVITY_DOWN - Identifier for gravity down portal
* @property {number} SPEED_BLUE - Identifier for blue speed portal
* @property {number} UFO - Identifier for UFO portal
* @property {number} ROBOT - Identifier for robot portal
* @property {number} MIRROR_ON - Identifier for mirror on portal
* @property {number} GRAVITY_UP - Identifier for gravity up portal
* @property {number} DUAL_ON - Identifier for dual on portal
* @property {number} SIZE_MINI - Identifier for mini size portal
* @property {number} BALL - Identifier for ball portal
* @property {number} SIZE_NORMAL - Identifier for normal size portal
* @property {number} SHIP - Identifier for ship portal
* @property {number} SPEED_PINK - Identifier for pink speed portal
* @property {number} SPEED_YELLOW - Identifier for yellow speed portal
* @property {number} DUAL_OFF - Identifier for dual off portal
*/
/**
* @typedef {Object} obj_ids
* @property {special_objects} special Special object IDs
* @property {trigger_ids} triggers Trigger object IDs
* @property {portal_ids} portals Portal object IDs
*/
/**
* Object containing various IDs for objects, triggers, and portals.
* @type {obj_ids}
*/
let obj_ids = {
special: {
USER_COIN: 1329,
H_BLOCK: 1859,
J_BLOCK: 1813,
TEXT: 914,
S_BLOCK: 1829,
ITEM_DISPLAY: 1615,
D_BLOCK: 1755,
COLLISION_BLOCK: 1816,
},
triggers: {
SPAWN: 1268,
ON_DEATH: 1812,
ROTATE: 1346,
COUNT: 1611,
DISABLE_TRAIL: 33,
HIDE: 1612,
PICKUP: 1817,
COLLISION: 1815,
ENABLE_TRAIL: 32,
ANIMATE: 1585,
TOUCH: 1595,
INSTANT_COUNT: 1811,
BG_EFFECT_OFF: 1819,
TOGGLE: 1049,
MOVE: 901,
ALPHA: 1007,
SHOW: 1613,
STOP: 1616,
FOLLOW: 1347,
PULSE: 1006,
BG_EFFECT_ON: 1818,
SHAKE: 1520,
FOLLOW_PLAYER_Y: 1814,
COLOR: 899,
},
portals: {
SPEED_GREEN: 202,
TELEPORT: 747,
CUBE: 12,
MIRROR_OFF: 46,
WAVE: 660,
SPIDER: 1331,
SPEED_RED: 1334,
GRAVITY_DOWN: 10,
SPEED_BLUE: 201,
UFO: 111,
ROBOT: 745,
MIRROR_ON: 45,
GRAVITY_UP: 11,
DUAL_ON: 286,
SIZE_MINI: 101,
BALL: 47,
SIZE_NORMAL: 99,
SHIP: 13,
SPEED_PINK: 203,
SPEED_YELLOW: 200,
DUAL_OFF: 287,
},
};
/**
* @typedef {Object} big_beast_animations
* @property {number} bite - Identifier for the bite animation
* @property {number} attack01 - Identifier for the first attack animation
* @property {number} attack01_end - Identifier for the end of the first attack animation
* @property {number} idle01 - Identifier for the first idle animation
*/
/**
* @typedef {Object} bat_animations
* @property {number} idle01 - Identifier for the first idle animation
* @property {number} idle02 - Identifier for the second idle animation
* @property {number} idle03 - Identifier for the third idle animation
* @property {number} attack01 - Identifier for the first attack animation
* @property {number} attack02 - Identifier for the second attack animation
* @property {number} attack02_end - Identifier for the end of the second attack animation
* @property {number} sleep - Identifier for the sleep animation
* @property {number} sleep_loop - Identifier for the sleep loop animation
* @property {number} sleep_end - Identifier for the end of the sleep animation
* @property {number} attack02_loop - Identifier for the loop of the second attack animation
*/
/**
* @typedef {Object} spikeball_animations
* @property {number} idle01 - Identifier for the first idle animation
* @property {number} idle02 - Identifier for the second idle animation
* @property {number} toAttack01 - Identifier for the transition to the first attack animation
* @property {number} attack01 - Identifier for the first attack animation
* @property {number} attack02 - Identifier for the second attack animation
* @property {number} toAttack03 - Identifier for the transition to the third attack animation
* @property {number} attack03 - Identifier for the third attack animation
* @property {number} idle03 - Identifier for the third idle animation
* @property {number} fromAttack03 - Identifier for the transition from the third attack animation
*/
/**
* @typedef {Object} animations
* @property {big_beast_animations} big_beast - Animation identifiers for big beast
* @property {bat_animations} bat - Animation identifiers for bat
* @property {spikeball_animations} spikeball - Animation identifiers for spikeball
*/
/**
* Object containing various animation IDs for big beast, bat, and spikeball.
* @type {animations}
*/
let animations = {
big_beast: {
bite: 0,
attack01: 1,
attack01_end: 2,
idle01: 3
},
bat: {
idle01: 0,
idle02: 1,
idle03: 2,
attack01: 3,
attack02: 4,
attack02_end: 5,
sleep: 6,
sleep_loop: 7,
sleep_end: 8,
attack02_loop: 9
},
spikeball: {
idle01: 0,
idle02: 1,
toAttack01: 2,
attack01: 3,
attack02: 4,
toAttack03: 5,
attack03: 6,
idle03: 7,
fromAttack03: 8
}
};
let exps = {
// constants
EQUAL_TO: 0,
LARGER_THAN: 1,
SMALLER_THAN: 2,
BG: color(1000),
GROUND: color(1001),
LINE: color(1002),
_3DLINE: color(1003),
OBJECT: color(1004),
GROUND2: color(1009),
MODE_STOP: 0,
MODE_LOOP: 1,
MODE_LAST: 2,
LEFT_EDGE: 1,
RIGHT_EDGE: 2,
UP_EDGE: 3,
DOWN_EDGE: 4,
obj_ids,
animations,
// functions & objects
$,
counter,
spawn_trigger,
color_trigger,
move_trigger,
trigger_function,
item_comp,
compare,
on,
touch,
touch_end,
collision,
collision_exit,
death,
count,
x_position,
wait,
range,
Context,
extract,
while_loop,
greater_than,
equal_to,
less_than,
unknown_g,
unknown_c,
unknown_b,
toggle_on_trigger,
toggle_off_trigger,
item_edit,
hide_player,
gamescene,
keyframe_system,
obj_to_levelstring,
teleport,
camera_offset,
camera_static,
camera_zoom,
camera_mode,
camera_rotate,
camera_edge,
timer,
song,
call_with_delay,
for_loop,
gradient,
random,
advanced_random,
gravity,
options,
sequence,
remappable,
particle_system,
end,
player_control,
timewarp,
event,
events,
particle_props,
object,
Context,
ignore_context_change,
level,
shader_layers,
shader_layer,
sepia,
hue_shift,
grayscale,
pixelate,
chromatic,
glitch,
bulge,
split_screen,
float_counter,
frame_loop,
frames,
frame,
log,
spawn_particle,
reverse: () => {
$.add(object({
OBJ_ID: 1917
}));
},
rgb: (r, g, b) => [r, g, b],
rgba: (r, g, b, a) => [r, g, b, a],
};
extract(exps);