lib/general-purpose.js

/**
 * @module general-purpose
 */

const all_particles = require('../properties/particles');

/**
 * Offsets the camera by a position
 * @param {number} x X offset of camera
 * @param {number} y X offset of camera
 * @param {number} [duration=0] Duration that it takes for camera position to change
 */
let camera_offset = (x, y, duration = 0, easing = NONE) => {
    $.add(object({
        OBJ_ID: 1916,
        155: 1,
        MOVE_X: x * 3,
        MOVE_Y: y * 3,
        ACTIVE_TRIGGER: true,
        DURATION: duration,
        EASING_RATE: easing
    }));
    if (duration) wait(duration);
};
/**
 * Makes the camera static around a target object (group ID)
 * @param {group} group Group storing object to be the center of camera
 * @param {number} [duration=0] Duration that it takes for camera to be centered around object
 * @param {easing} [easing=NONE] How smoothly the camera moves to the object
 * @param {boolean} [exit_instant=false] Stops static instantly
 * @param {boolean} [exit_static=false] Stops static
 * @param {boolean} [smooth_vel=false] Makes transition to target adapt to current camera velocity (no easing recommended)
 * @param {number} [smooth_vel_mod=0] Modifier for smooth velocity
 * @param {boolean} [follow=false] Makes camera change according to object movement
 * @param {boolean} [x_only=false] Makes the camera only be static on X axis
 * @param {boolean} [x_only=false] Makes the camera only be static on Y axis
 */
let camera_static = (gr, duration = 0, easing = NONE, exit_instant = false, exit_static = false, smooth_vel = false, smooth_vel_mod = 0, follow = false, x_only = false, y_only = false) => {
    if (x_only && y_only) throw new Error("Only one of the x_only or y_only arguments must be true, but both values are true!");
    let axisval = !x_only && !y_only ? 0 : (x_only ? 1 : 2);
    $.add(object({
        OBJ_ID: 1914,
        155: 1,
        DURATION: duration,
        TARGET_POS_AXES: axisval,
        ACTIVE_OBJECT: true,
        TARGET_POS: gr,
        FOLLOW_GROUP: follow,
        EASING: easing,
        SMOOTH_VELOCITY: smooth_vel,
        SMOOTH_VELOCITY_MODIFIER: smooth_vel_mod,
        EXIT_INSTANT: exit_instant,
        EXIT_STATIC: exit_static
    }));
    if (duration) wait(duration);
};
/**
 * Makes the camera zoom in/out by a specific amount
 * @param {number} zoom_amount Amount to zoom the camera in by
 * @param {number} [duration=0] How long it takes for camera to zoom in
 * @param {easing} [easing=NONE] How smoothly the camera zooms in
 */
let camera_zoom = (zoom_am, duration = 0, easing = NONE) => {
    $.add(object({
        OBJ_ID: 1913,
        ZOOM: zoom_am,
        DURATION: duration,
        EASING: easing
    }));
};
/**
 * Toggles free mode
 * @param {boolean} [free_mode=true] Whether to toggle free mode on or off
 * @param {boolean} [disable_grid_snap=false] Removes default snapping to nearest grid space for the camera center
 * @param {boolean} [edit_cam=false] Whether to edit camera settings
 * @param {number} [easing=10] Easing for camera movement (requires edit_cam to be true)
 * @param {number} [padding=0.50] Padding for camera movement (requires edit_cam to be true)
 */
let camera_mode = (free_mode = true, disable_grid_snap = false, edit_cam = false, easing = 10, padding = 0.50) => {
    $.add(object({
        OBJ_ID: 2925,
        FREE_MODE: free_mode,
        EDIT_FREE_CAM_SETTINGS: edit_cam,
        FREE_CAM_EASING: easing,
        FREE_CAM_PADDING: padding,
        DISABLE_GRID_SNAP: disable_grid_snap
    }));
};
/**
 * Rotates camera
 * @param {number} degrees How many degrees to rotate camera by
 * @param {number} [move_time=0] How fast rotation happens
 * @param {easing} [easing=NONE] How smooth rotation happens
 * @param {boolean} [add=false] Adds input rotation to current camera rotation
 * @param {boolean} [snap360=false] Converts rotation to closest 360
 */
let camera_rotate = (degrees, move_time = 0, easing = NONE, add = false, snap360 = false) => {
    $.add(object({
        OBJ_ID: 2015,
        DURATION: move_time,
        EASING: easing,
        ROTATE_DEGREES: degrees,
        ADD: add,
        SNAP_360: snap360
    }));
};

/**
 * Makes one of the camera's edges a specific target object
 * @param {group} id Group ID of target object
 * @param {edge} edge Defines the edge to set (LEFT_EDGE, RIGHT_EDGE, UP_EDGE, DOWN_EDGE)
 */
let camera_edge = (id, edge) => {
    $.add(object({
        OBJ_ID: 2062,
        TARGET: id,
        CAMERA_EDGE: edge
    }));
};

/**
* Represents a song trigger in GD
* @typedef {object} song
* @property {start_song} start Starts song
* @property {edit} edit Edit Song trigger implementation
* @property {stop} stop Stops playing the song
*/
/**
 * Starts playing the song
 * @callback start_song
 */
/**
 * Implementation of Edit Song trigger
 * @callback edit
 * @param {number} new_volume 
 * @param {number} new_speed 
 * @param {number} duration 
 * @param {boolean} stop 
 * @param {boolean} stop_loop 
 * @param {group} gid_1 
 * @param {group} gid_2 
 * @param {number} vol_near 
 * @param {number} vol_med 
 * @param {number} vol_far 
 * @param {number} min_dist 
 * @param {number} dist_2 
 * @param {number} dist_3 
 * @param {boolean} p1 
 * @param {boolean} p2 
 * @param {boolean} cam 
 * @param {0} vol_dir 
 */
/**
 * Stops song
 * @callback stop
 */
/**
 * Implementation of song trigger in GD
 * @param {number} song_id ID of song in-game
 * @param {boolean} [loop=false] Whether to loop the song
 * @param {boolean} [preload=true] Whether to preload the song first before playing
 * @param {number} [channel=0] What channel to put the song on
 * @param {number} [volume=1] Volume of song
 * @param {number} [speed=0] Speed of song
 * @param {number} [start=0] Where the song should start in MS
 * @param {number} [end=0] Where the song should end in MS
 * @param {number} [fadein=0] When to fade the song in
 * @param {number} [fadeout=0] When to fade the song out
 * @returns {song}
 */
let song = (song_id, loop = false, preload = true, channel = 0, volume = 1, speed = 0, start = 0, end = 0, fadein = 0, fadeout = 0) => {
    if (preload) {
        let m_obj = {
            OBJ_ID: 1934,
            SONG_ID: song_id,
            SONG_CHANNEL: channel,
            SONG_VOLUME: volume,
            SONG_SPEED: speed,
            SONG_START: start,
            SONG_END: end,
            SONG_FADE_IN: fadein,
            SONG_FADE_OUT: fadeout,
            SONG_LOOP: loop,
            PREP: true
        };
        $.add(object(m_obj));
        let al_load = false;
        let exp = {
            start: () => {
                if (al_load) $.add(object(m_obj));
                $.add(object({
                    OBJ_ID: 1934,
                    SONG_CHANNEL: channel,
                    LOAD_PREP: true
                }));
                if (!al_load) al_load = true;
            },
            edit: (new_volume = volume, new_speed = speed, duration = 0.5, stop = false, stop_loop = false, gid_1 = group(0), gid_2 = group(0), vol_near = 1, vol_med = 0.5, vol_far = 0, min_dist = 0, dist_2 = 0, dist_3 = 0, p1 = false, p2 = false, cam = false, vol_dir = 0) => {
                $.add(object({
                    OBJ_ID: 3605,
                    DURATION: duration,
                    SONG_CHANNEL: channel,
                    SONG_SPEED: new_speed,
                    SONG_VOLUME: new_volume,
                    SONG_STOP: stop,
                    STOP_LOOP: stop_loop,
                    CHANGE_SPEED: new_speed !== speed,
                    CHANGE_VOLUME: new_volume !== volume,
                    GROUP_ID_1: gid_1,
                    GROUP_ID_2: gid_2,
                    VOL_NEAR: vol_near,
                    VOL_MED: vol_med,
                    VOL_FAR: vol_far,
                    MIN_DIST: min_dist,
                    DIST_2: dist_2,
                    DIST_3: dist_3,
                    P1: p1,
                    P2: p2,
                    CAM: cam,
                    VOLUME_DIRECTION: vol_dir
                }));
            },
            stop: () => {
                exp.edit(volume, speed, 0.5, true, true);
                // loop ? false : true, loop ? true : false
            }
        };
        return exp;
    }
    $.add(object({
        OBJ_ID: 1934,
        SONG_ID: song_id,
        SONG_CHANNEL: channel,
        SONG_VOLUME: volume,
        SONG_SPEED: speed,
        SONG_START: start,
        SONG_END: end,
        SONG_FADE_IN: fadein,
        SONG_FADE_OUT: fadeout,
        SONG_LOOP: loop
    }));
}

/**
* Teleports the player to a specific target object
* @param {group} id Group ID of target object
*/
let teleport = (g) => {
    if (g?.length) {
        $.add(object({
            OBJ_ID: 3022,
            X: g[0],
            Y: g[1],
            155: 1,
            13: 1,
            ACTIVE_TRIGGER: 1,
            350: 1
        }));
        return;
    }
    $.add(object({
        OBJ_ID: 3022,
        155: 1,
        13: 1,
        ACTIVE_TRIGGER: 1,
        TARGET: g,
        350: 1
    }));
};

/**
 * Adds a move trigger and returns it
 * @param {group} id Group ID of target object
 * @param {number} x X amount of how much to move the object by
 * @param {number} Y Y amount of how much to move the object by
 * @returns {object} Returned object
 */
let move_trigger = (group, x, y) => {
    return object({
        OBJ_ID: 901,
        TARGET: group,
        MOVE_X: x * 3,
        MOVE_Y: y * 3,
    });
};
/**
 * Warps all time by given amount
 * @param {number} value How much to warp time by
 */
let timewarp = (val) => {
    $.add(object({
        OBJ_ID: 1935,
        TIMEWARP_TIME_MOD: val
    }));
};

/**
* Creates color trigger
* @param {color} channel Color channel to set
* @param {number} r Red value in RGB to set
* @param {number} g Green value in RGB to set
* @param {number} b Blue value in RGB to set
* @param {number} [duration=0] Duration that it takes for color to change
* @param {number} [opacity=1] Opacity of color (1 = visible, 0 = invisible)
* @param {boolean} [blending=false] Whether to blend color with others
* @returns {object} Resulting color trigger
*/
let color_trigger = (
    channel,
    r,
    g,
    b,
    duration = 0,
    opacity = 1,
    blending = false
) => {
    return object({
        OBJ_ID: 899,
        DURATION: duration,
        TRIGGER_RED: r,
        TRIGGER_GREEN: g,
        TRIGGER_BLUE: b,
        OPACITY: opacity,
        BLENDING: blending,
        TARGET_COLOR: channel,
    });
};
/**
* Returns an activated toggle trigger
* @param {group} group Group of object
* @returns {object} Resulting object
*/
let toggle_on_trigger = (group) => {
    return object({
        OBJ_ID: 1049,
        TARGET: group,
        ACTIVATE_GROUP: true,
    });
};

/**
 * Returns an inactive toggle trigger
 * @param {group} group Group of object
 * @returns {object} Resulting object
 */
let toggle_off_trigger = (group) => {
    return object({
        OBJ_ID: 1049,
        TARGET: group,
        ACTIVATE_GROUP: false,
    });
};

/**
* Hides player
*/
let hide_player = () => {
    $.add(object({
        OBJ_ID: 1612,
    }));
};

let gradient_id = 0;
/**
* Creates a gradient trigger and returns it
* @param {color} color1 First color of gradient
* @param {color} color2 Second color of gradient
* @param {group} bl Bottom left vertex
* @param {group} br Bottom right vertex
* @param {group} tl Top left vertex
* @param {group} tr Top right vertex
* @param {boolean} [vertex_mode=true] Whether to use vertex mode
* @param {boolean} [blending=false] Whether to make the gradient blending
* @param {number} [layer=0] Layer of gradient (0-15)
* @returns {object} Resulting gradient trigger
*/
let gradient = (col, col2, bl, br, tl, tr, vertex_mode = true, blending = false, layer = 0) => {
    return object({
        OBJ_ID: 2903,
        GR_BL: bl,
        GR_BR: br,
        GR_TL: tl,
        GR_TR: tr,
        GR_ID: gradient_id++,
        COLOR: col,
        COLOR_2: col2,
        GR_VERTEX_MODE: vertex_mode,
        GR_BLENDING: blending,
        GR_LAYER: layer
    });
};

/**
 * Creates a particle system
 * @param {dictionary} props Dictionary holding particle properties (check {@tutorial Particles} for more info)
 * @param {boolean} [use_obj_color=false] Whether to make the particle system use the object color
 * @param {boolean} [animate_on_trigger=false] Whether to only start the particle system when the Animate trigger is used on the particle system instead of immediately
 * @param {boolean} [animate_active_only=false] Only makes animate_on_trigger true if the object is active 
 * @param {boolean} [quick_start=false] Makes normal movement be achieved instantly instead of gradually
 * @returns {object} Returned particle system
 */
let particle_system = (props, use_obj_color = false, animate_on_trigger = false, animate_active_only = false, quick_start = false) => {
    let datalist = Array(72).fill(0);
    for (let i in props) {
        let x = props[i];
        if (typeof x == "boolean") x = +x;
        datalist[all_particles[i]] = x;
    };
    datalist = datalist.join('a');
    return object({
        OBJ_ID: 2065,
        PARTICLE_DATA: datalist,
        USE_OBJ_COLOR: use_obj_color,
        UNIFORM_OBJ_COLOR: "UNIFORM_OBJ_COLOR" in props ? props.UNIFORM_OBJ_COLOR : false,
        ANIMATE_ON_TRIGGER: animate_on_trigger,
        ANIMATE_ACTIVE_ONLY: animate_active_only,
        QUICK_START: quick_start
    });
};

/**
 * Implementation of Spawn Particle trigger
 * @param {group} particle_group Group ID of particle system
 * @param {group} pos_group Target location to spawn trigger system in
 * @param {number} offset_x How much to to offset the particle system from the target on the X axis
 * @param {number} offset_y How much to to offset the particle system from the target on the Y axis
 * @param {number} scale Scale of particle system
 * @param {number} scale_var Value to randomly add or decrease to scale
 * @param {number} rotation How many angles the system is rotated by
 * @param {number} rotation_var Value to randomly add or decrease to rotation
 * @param {number} offvar_x Area to randomly spawn particles in on X axis
 * @param {number} offvar_y Area to randomly spawn particles in on Y axis
 * @param {boolean} match_rot Makes the rotation of several particles match
 */
let spawn_particle = (particle_group, pos_group = group(0), offset_x = 0, offset_y = 0, scale = 1, scale_var = 0, rotation = 0, rotation_var = 0, offvar_x = 0, offvar_y = 0, match_rot = false) => {
    $.add(object({
        OBJ_ID: 3608,
        TARGET: particle_group,
        TARGET_POS: pos_group,
        547: offset_x,
        548: offset_y,
        549: offvar_x,
        550: offvar_y,
        551: match_rot,
        552: rotation,
        553: rotation_var,
        554: scale,
        555: scale_var,
    }));
};

/**
 * Implementation of random trigger
 * @param {group} gr1 Group 1
 * @param {group} gr2 Group 2
 * @param {number} chance Chance of either group being called
 */
let random = (gr1, gr2, chance) => {
    $.add(object({
        OBJ_ID: 1912,
        GROUP_ID_1: gr1,
        GROUP_ID_2: gr2,
        CHANCE: chance
    }));
};
/**
 * Implementation of advanced random trigger
 * @param {array} chances Chances of each group being called (e.g. [[group(1), 10], [group(2), 10]] is a valid input)
 */
let advanced_random = (...chances) => {
    $.add(object({
        OBJ_ID: 2068,
        ADV_RAND_STRING: chances.map(x => x[0].value + '.' + x[1]).join('.')
    }));
}

/**
* Implementation of gravity trigger
* @param {number} gravity Gravity magnitude
* @param {boolean} p1 Only affect player 1
* @param {boolean} p2 Only affect player 2
* @param {boolean} pt Only affect player that touches trigger
*/
let gravity = (grav, p1 = false, p2 = false, pt = false) => {
    $.add(object({
        OBJ_ID: 2066,
        GRAVITY: grav,
        PLAYER_1: p1,
        PLAYER_2: p2,
        _PT: pt
    }))
};

/**
 * Represents an options trigger
 * @typedef {object} options
 * @property {function} STREAK_ADDITIVE Streak additive (arg = boolean, optional)
 * @property {function} HIDE_GROUND Hide ground (arg = boolean, optional)
 * @property {function} HIDE_MG Hide middle ground (arg = boolean, optional)
 * @property {function} HIDE_P1 Hide player 1 (arg = boolean, optional)
 * @property {function} HIDE_P2 Hide player 2 (arg = boolean, optional)
 * @property {function} DISABLE_CONTROLS_P1 Disable player 1 controls (arg = boolean, optional)
 * @property {function} DISABLE_CONTROLS_P2 Disable player 2 controls (arg = boolean, optional)
 * @property {function} UNLINK_DUAL_GRAVITY Unlink dual gravity (arg = boolean, optional)
 * @property {function} HIDE_ATTEMPTS Hide attempts (arg = boolean, optional)
 * @property {function} AUDIO_ON_DEATH Audio on death (arg = boolean, optional)
 * @property {function} NO_DEATH_SFX No death SFX (arg = boolean, optional)
 * @property {function} RESPAWN_TIME Respawn time (arg = number, required)
 * @property {function} add Adds options trigger
 */
/**
 * Implementation of options trigger
 * @returns {options} Options trigger
 */
let options = () => {
    let ob = {
        OBJ_ID: 2899
    };
    return {
        STREAK_ADDITIVE: (v = true) => ob.STREAK_ADDITIVE = v ? 1 : -1,
        HIDE_GROUND: (v = true) => ob.HIDE_GROUND = v ? 1 : -1,
        HIDE_MG: (v = true) => ob.HIDE_MG = v ? 1 : -1,
        HIDE_P1: (v = true) => ob.HIDE_P1 = v ? 1 : -1,
        HIDE_P2: (v = true) => ob.HIDE_P2 = v ? 1 : -1,
        DISABLE_CONTROLS_P1: (v = true) => ob.DISABLE_CONTROLS_P1 = v ? 1 : -1,
        DISABLE_CONTROLS_P2: (v = true) => ob.DISABLE_CONTROLS_P2 = v ? 1 : -1,
        UNLINK_DUAL_GRAVITY: (v = true) => ob.UNLINK_DUAL_GRAVITY = v ? 1 : -1,
        HIDE_ATTEMPTS: (v = true) => ob.HIDE_ATTEMPTS = v ? 1 : -1,
        AUDIO_ON_DEATH: (v = true) => ob.AUDIO_ON_DEATH = v ? 1 : -1,
        NO_DEATH_SFX: (v = true) => ob.NO_DEATH_SFX = v ? 1 : -1,
        RESPAWN_TIME: (v) => {
            ob.EDIT_RESPAWN_TIME = 1;
            ob.RESPAWN_TIME = v;
        },
        add: () => $.add(object(ob))
    };
};

/**
 * Ends level
 * @param {boolean} instant_end Whether to end level instantly
 * @param {boolean} no_effects Whether to remove effects
 * @param {boolean} no_sfx Whether to remove SFX
 * @param {group} spawn_id Group to spawn on end
 * @param {group} target_pos Object defining end position
 */
let end = (instant_end = false, no_effects = false, no_sfx = false, spawn_id = group(0), target_pos = group(0)) => {
    $.add(object({
        OBJ_ID: 3600,
        GROUP_ID_1: spawn_id,
        GROUP_ID_2: target_pos,
        NO_EFFECTS: no_effects,
        NO_SFX: no_sfx,
        INSTANT_END: instant_end
    }));
};
/**
 * Implementation of player control trigger
 * @param {boolean} p1 Only controls P1
 * @param {boolean} p2 Only controls P2
 * @param {boolean} stop_jump Stops player from jumping
 * @param {boolean} stop_move Stops player from moving
 * @param {boolean} stop_rot Stops player from rotating
 * @param {boolean} stop_slide Stops player from sliding
 */
let player_control = (p1 = false, p2 = false, stop_jump = false, stop_move = false, stop_rot = false, stop_slide = false) => {
    $.add(object({
        OBJ_ID: 1932,
        PLAYER_1: p1,
        PLAYER_2: p2,
        STOP_JUMP: stop_jump,
        STOP_MOVE: stop_move,
        STOP_ROT: stop_rot,
        STOP_SLIDE: stop_slide
    }));
}

module.exports = {
    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
};