LemonCake

A bot API for Starblast's modding framework, developed by dankdmitron.

Intro

lemoncake.js, the secret sauce letting you add bots to your mod, is loaded externally from https://starblast.dankdmitron.dev/js/lemoncake.js.

This is done so two main reasons:

  • It lets me push updates to the library without anyone needing to change their mod code
  • It's a messy, minified/obfuscated mess of a file, pasting it into mod console will degrade editor performance

Disclaimer

  • Unsupported features of the game are used to make bots possible and this library can break at any time.
  • Due to the rather fragile nature of these features, lemoncake.js is heavily obfuscated in order to prevent abuse (like spamming a game with 1000 bots).
  • It's good practice to know what someone's code does before using it. In this case however, you have little ways of knowing exactly how LemonCake works because I deliberately obfuscated it so ultimately, it becomes a matter of trusting the author. For the record, LemonCake does not do anything malicious, but you should still keep in mind you're loading someone else's code into your program.
  • Thus, for security reasons I strongly recommend only loading LemonCake from the endpoint https://starblast.dankdmitron.dev/js/lemoncake.js.

Installation

ok, how do I get this thing?

The recommended way to load LemonCake is by putting this code at the top of your mod code:

const include = async function(url) {
    let response = await fetch(url);
    let text = await response.text();
    (1, eval)(text);
    return text;
}

if (!window.lemonCake) {
    include("https://starblast.dankdmitron.dev/js/lemoncake.js")
}

include is just a neat little async function allowing you to load external JS into your mod. LemonCake must be loaded before the mod starts, so in some cases you might need to relaunch your mod twice — first to load LemonCake, then to actually get to use it (global variables persist across mod launches).

API

ok, how do I use this thing?

lemonCake

  • .createBot(Object options): returns a new Bot object, configured with options.
let myBot = lemonCake.createBot({
    name: "MY BOT", // The name of the bot ship
    // by default sets it to BOT-GHD where GHD is a string of random letters
    hue: 30, // hue color, default 60
    team: 0, // team id, default 0
    bonus: true, // whether or not the starting ship of the bot is maxed
    // similar to how you get a maxed fly if you have ECP 
    // or shared the game on Twitter or Facebook
});

// I recommend making an array to keep track of your bots, but you don't need to
game.bots.push(myBot); 

bot

You can add up to 8 bots to your mod

  • connect(): makes the bot attempt to join the game. If successful, bot.connected will be true. For now, this function has no Promise or direct return value, so you'll have to run checks on bot.connected.
  • disconnect(): disconnects the bot, equivalent to closing the tab while playing. Sets bot.connected to false.
  • update(Object options): sends an update packet with the given options.
bot.update({
    angle: 180, // the direction the bot is facing, in degrees. 0 is right, 90 is up
    // this won't let you get perfect turn rate on your bot
    // a packet of 0 followed by 180 will make the ship take time to turn around
    moving: true, // whether the bot is moving in the direction it is facing, default false
    firing: true, // whether the bot is firing in the direction is is facing, default false
    rcs: true, // if RCS is engaged or not. RCS: false will make the bot drift. defaul true
    // ^^^^ DONT GET CONFUSED
    donating: false // whether or not the bot is releasing gems by pressing V. default false
});

// this is an UPDATE packet, sending one packet with moving: true will keep the bot moving
// until you send a packet with moving: false
  • say(String sentence): makes the bot say something. "GGGG" will make the bot say 4 GG's.
  • respawn(): sends a respawn packet, self explanatory. Bots are not bound by respawn delays like ads and can respawn immediately.
  • getShip(): iterates through game.ships and returns the ship corresponding to the bot.
  • upgradeShip(Number type): if it can, makes the bot upgrade to a ship of specified type.
// Suppose bot is a Fly (type 101) with 20 gems
bot.upgradeShip(202) // will make it upgrade to a Trident
  • buyLife(): makes the bot buy a ship life, if it can.
  • startTransfer(): makes the bot begin converting gems to credits, if it can.
  • stopTransfer(): makes the bot stop conversion.
  • buySecondary(Number type): makes the bot buy a secondary of specified type, if it can.
// Suppose we want to buy a missile (type 11), and we have 250 credits
bot.buySecondary(11) // makes the bot buy a secondary
  • sellSecondary(Number slot): makes the bot sell a secondary from a specified slot.
// Suppose we have a missile pack at the very top of our weapon slots
bot.sellSecondary(0) // makes the bot sell that missile
  • fireSecondary(Number slot, Number type): makes the bot fire the secondary from the specified slot of the specified type. No clue why the type is also required, keep track of your weapon slots I guess.
  • upgradeStat(Number index): upgrades the stat of the specified index.
// Suppose we're a Delta-Fighter with gems to spare
bot.upgradeStat(0) // will upgrade our shield capacity
bot.upgradeStat(5) // will upgrade our laser speed

Note that reading the bot's surroundings is up to you, variables to see ship stats like gems, shield, coordinates are present in game.ships. This API is mostly an input, not an output of this data. I also recommend managing things like ship type, crystals, upgrades and whatnot using ship.set() in the default modding engine — better to use officially supposed functions if you can.

Helper Tools

  • getMap(Number mapID, Number mapSize, Number gameMode): returns a map string based on the given parameters. Useful for pathfinding in randomly generated maps. May be inaccurate, this was pulled from the game's code and jury rigged.
console.log(getMap(475, 30, "survival"))
/* outputs
405000000000000000000000903040
000000000000000000000000809020
000000000000000000000000600000
000000000000000000000000000007
000000000000000000000000000405
000000000000000000000000000200
000000000000000000000000000000
700000000000000000000000000000
000000000000000000000000000000
050000000000000000000000000000
030000000000000000000000000000
000000000000000000000000000000
000000003000000000000000000000
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000060
000000000000000000000000000000
000000000000000000000000000003
000000000000000000000000000009
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000000
*/

Examples

This mod will spawn a horde of bots that will target you as soon as you join. Change the ship targeted by changing game.botTargetIndex from console later on.

// Example bot mod by dankdmitron

const include = async function(url) {
    let response = await fetch(url);
    let text = await response.text();
    (1, eval)(text);
    return text;
}

// Note: you will likely need to run the mod twice, first to load lemoncake, 
// then to actually run the mod. On fast connections lemoncake might get loaded 
// BEFORE mod starts, thus working first try. 
if (!window.lemonCake) {
    include("https://starblast.dankdmitron.dev/js/lemoncake.js")
}

let calcAngleDegrees = function(x, y) {
    let degrees = Math.atan2(y, x) * 180 / Math.PI;
    if (degrees < 0) {
        return 360 + degrees;
    }
    return degrees;
}

// The following two functions are taken from the Prototypes mod

// Self explanatory
let sqrDist = function(x, y) {
    return x*x+y*y;
};

// Given a pair of coordinates, returns the shortest [deltaX, deltaY] pair with consideration to the game's map looping around
let shortestPath = function(x1, y1, x2, y2, mapSize, wrapV = true, wrapH = true) {
    let shortestDist = 10000;
    let map_size = mapSize*5;
    let coords = [];
    let xx = x2-x1;
    let yy = y2-y1;
    if (!wrapH&&!wrapV) return [xx, yy];
    coords.push(xx, yy);
    let shortest = [xx, yy];
    if(wrapH){
        xx = x2+map_size*2-x1;
        yy = y2-y1;
        coords.push(xx, yy);
        xx = x2-map_size*2-x1;
        yy = y2-y1;
        coords.push(xx, yy);
    }
    if(wrapV){
        xx = x2-x1;
        yy = y2+map_size*2-y1;
        coords.push(xx, yy);
        xx = x2-x1;
        yy = y2-map_size*2-y1;
        coords.push(xx, yy);
    }
    if(wrapV&&wrapH){
        xx = x2+map_size*2-x1;
        yy = y2+map_size*2-y1;
        coords.push(xx, yy);
        xx = x2+map_size*2-x1;
        yy = y2-map_size*2-y1;
        coords.push(xx, yy);
        xx = x2-map_size*2-x1;
        yy = y2+map_size*2-y1;
        coords.push(xx, yy);
        xx = x2-map_size*2-x1;
        yy = y2-map_size*2-y1;
        coords.push(xx, yy);
    }
    for(let i = 0; i<9; i++){
        let dist = sqrDist(coords[i*2], coords[i*2+1]);
        if(dist<shortestDist){
            shortestDist = dist;
            shortest = [coords[i*2], coords[i*2+1]];
        }
    }
    return shortest;
};

this.options = {
    // see documentation for options reference
    root_mode: "survival",
    map_size: 30,
    asteroids_strength: 0,
    radar_zoom: 1,
    power_regen_factor: 100,
    shield_regen_factor: 100,
    max_players: 20
};

let gameInitialized = false;
this.tick = function(game) {
    if (!gameInitialized) { // crude init function
        gameInitialized = true;
        game.bots = [];
        for (let x of Array(8)) { // create 8 bots
            let bot = lemonCake.createBot(); // create the bot object
            game.bots.push(bot); 
            bot.connect(); // connect the bot to the game
        }
        game.botTargetIndex = 8; // index of target ship. change with game.botTargetIndex = urIndex
    }
    if (game.step % 6 === 0) {
        for (let ship of game.ships) {
            for (let bot of game.bots) {
                let self = bot.getShip(); // get own ship
                let target = game.ships[game.botTargetIndex]; // get target shup
                if (target && self) {
                    // find shortest deltaX, deltaY considering crossing map boundaries
                    let path = shortestPath(self.x, self.y, target.x, target.y, this.options.map_size);
                    let deltaX = path[0];
                    let deltaY = path[1];
                    let direction = Math.round(calcAngleDegrees(deltaX, deltaY)); // compute direction using reverse tan function
                    if (bot && bot.connected) {
                        bot.update({ // update with the bot pointing in the direction while moving and shooting
                            angle: direction,
                            firing: true,
                            moving: true
                        });
                        bot.say("B"); // make the bot say "Bye" constantly
                    }
                }
            }
        }
    }
}