105 changes: 105 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/_events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@

/*
* This file includes event definitions only.
*
*/

function eventStartLevel() {
queue("setTimers", me * 100);
if (alliancesType === ALLIANCES_TEAMS) {
// initialize subpersonality pseudo-randomly here
// to make sure teammates have the same personality
var j = 1, s = 0;
for (var i = 0; i < maxPlayers; ++i) {
if (allianceExistsBetween(me, i))
s += j;
j *= 2;
}
// the random "s" number obtained here is the same for all players in any team
var s = s + (new Date()).getMinutes();
j = 0;
for (var i in subpersonalities) // count the amount of subpersonalities
++j;
s = s % j;
j = 0;
for (var i in subpersonalities) {
if (j === s)
personality = subpersonalities[i];
++j;
}
} else {
// if teams are not sharing research, or there are no teams at all,
// initialize the subpersonality randomly and don't care
personality = randomItem(subpersonalities);
}
// the following code is necessary to avoid some strange game bug when droids that
// are initially buried into the ground fail to move out of the way when a building
// is being placed right above them
enumTrucks().forEach(function(droid) {
orderDroidLoc(droid, DORDER_MOVE, droid.x + random(3) - 1, droid.y + random(3) - 1);
});
}

function eventDroidBuilt(droid, structure) {
groupDroid(droid);
}

function eventStructureBuilt(structure) {
queue("checkConstruction");
}

function eventAttacked(victim, attacker) {
if (attacker === null)
return; // no idea why it happens sometimes
if (isAlly(attacker.player))
return; // don't respond to accidental friendly fire
if (victim.type === DROID) {
if (!isVTOL(victim) && defined(victim.group)) {
fallBack(victim, attacker);
setTarget(attacker, victim.group);
touchGroup(victim.group);
} else if (isVTOL(victim) && victim.player == me && !throttled(5000, victim.id)) {
orderDroidObj(victim, DORDER_ATTACK, attacker);
pushVtols(attacker);
}
} else if (victim.type === STRUCTURE) {
if (throttled(5000) && victim.player != me)
return;
if (inPanic())
for (var i = 0; i < MAX_GROUPS; ++i)
if (groupSize(i) > 0)
setTarget(attacker, i);
setTarget(attacker, miscGroup);
setTarget(attacker);
}
}

function eventStructureReady(structure) {
fireLassat(structure);
}

function eventChat(from, to, message) {
// we are not case-sensitive
message = message.toLowerCase();
handleChatMessage(from, to, message)
}

function eventObjectTransfer(object, from) {
if (object.player !== me)
return; // object was transferred from me, not to me
if (object.type === DROID)
groupDroid(object);
}

function eventBeacon(x, y, from, to) {
noticeBeacon(x, y, from);
}

function eventBeaconRemoved(from, to) {
unnoticeBeacon(from);
}

function eventDestroyed(object) {
if (isEnemy(object.player))
pushVtols(object);
}
107 changes: 107 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/_head.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@

/*
* This file defines generic things that should be defined
* prior to defining a ruleset.
*
* NOTE: This file is not included from main.js .
*
*/

// weapon path role
const ROLE = {
AT: 0,
AP: 1,
AS: 2,
AA: 3,
LENGTH: 4, // number of items in this enum
}

// something dual to the previous enum
const OBJTYPE = {
TANK: 0,
BORG: 1,
DEFS: 2,
VTOL: 3,
LENGTH: 4, // number of items in this enum; should be equal to ROLE.LENGTH
}

// this controls body and weapon compatibility.
// A little explanation:
// w \ b | L | M | H bodies can't be "ultra-",
// UL | + | - | - ultra-light weapons are for light bodies only,
// L | + | + | - light weapons are for light or medium bodies,
// M | + | + | + medium weapons are for all bodies,
// H | - | + | + heavy weapons are for medium or heavy bodies,
// UH | - | - | + ultra-heavy weapons are for heavy bodies only.
const WEIGHT = {
ULTRALIGHT: 0,
LIGHT: 1,
MEDIUM: 2,
HEAVY: 3,
ULTRAHEAVY: 4,
}

// what to use this defensive structure for
const DEFROLE = {
STANDALONE: 0,
GATEWAY: 1,
ARTY: 2,
ANTIAIR: 3,
FORTRESS: 4,
}

// return values of build order calls
const BUILDRET = {
SUCCESS: 0,
UNAVAILABLE: 1,
FAILURE: 2,
}

// should we execute a build call in an unsafe location?
const IMPORTANCE = {
PEACETIME: 0,
MANDATORY: 1,
}

// aspects of every research path
const RESASPECTS = {
WEAPONS: 0,
DEFENSES: 1,
VTOLS: 2,
EXTRAS: 3,
LENGTH: 4, // number of items in this enum
}

const PROPULSIONUSAGE = {
GROUND : 1,
HOVER: 2,
VTOL: 4
}

// what to use this body for? (bit field)
const BODYUSAGE = {
GROUND: 1, // for tanks
AIR: 2, // for VTOLs
COMBAT: 3, // GROUND | AIR
TRUCK: 4, // trucks, sensors and repair droids
UNIVERSAL: 7, // COMBAT | TRUCK
}

// what sort of weapons this body is resistant to?
const BODYCLASS = {
KINETIC: 1,
THERMAL: 2,
}

// should this module be prioritized when not having enough power?
const MODULECOST = {
CHEAP: 0,
EXPENSIVE: 1,
}

// how to micromanage tanks with this sort of weapon
const MICRO = {
RANGED: 0, // normal weapons, such as cannons
MELEE: 1, // weapons that need to get very close to the enemy, such as flamers
DUMB: 2, // weapons that can't fire while moving
}
26 changes: 26 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/_main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

/*
* This file connects all remaining pieces of AI code together.
* It shouldn't contain any code itself.
*
* NOTE: order matters!
*
*/

var personality;

include(NB_INCLUDES + "math.js");
include(NB_INCLUDES + "intensity.js");
include(NB_INCLUDES + "misc.js");
include(NB_INCLUDES + "stats.js");

include(NB_INCLUDES + "adapt.js");
include(NB_INCLUDES + "build.js");
include(NB_INCLUDES + "research.js");
include(NB_INCLUDES + "produce.js");
include(NB_INCLUDES + "tactics.js");
include(NB_INCLUDES + "lassat.js");
include(NB_INCLUDES + "chat.js");

include(NB_INCLUDES + "timers.js");
include(NB_INCLUDES + "_events.js");
484 changes: 484 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/adapt.js

Large diffs are not rendered by default.

376 changes: 376 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,376 @@

/*
* This file describes building construction procedures.
*
*/

(function(_global) {
////////////////////////////////////////////////////////////////////////////////////////////

function randomLocation() {
var x = baseLocation.x + random(baseScale) - baseScale / 2;
var y = baseLocation.y + random(baseScale) - baseScale / 2;
if (x < 3 || y < 3 || x > mapWidth - 4 || y > mapHeight - 4)
return baseLocation;
return {x: x, y: y};

}

// a function for checking the presence of enemy units at the construction site
function safeSpot(x, y) {
return dangerLevel({x: x, y: y}) <= 0;
}

function truckFree(truck) {
if (truck.droidType !== DROID_CONSTRUCT)
return false;
if (truck.order === DORDER_BUILD)
return false;
if (truck.order === DORDER_HELPBUILD)
return false;
if (truck.order === DORDER_LINEBUILD)
return false;
if (truck.order === DORDER_DEMOLISH)
return false;
return true;
}

// returns one or two free trucks
function getTwoFreeTrucks() {
var trucks = enumTrucks().filter(truckFree);
if (trucks.length > 2) {
var ret = naiveFindClusters(trucks, baseScale / 2);
if (ret.maxCount >= 2)
trucks = ret.clusters[ret.maxIdx];
}
if (trucks.length > 2)
trucks.length = 2;
return trucks;
}

function getFreeTruckAround(x, y) {
var list = enumTrucks().filter(truckFree).filter(function(droid) {
return droidCanReach(droid, x, y);
}).sort(function(one, two) {
return distance(one, x, y) - distance(two, x, y);
});
if (list.length > 0)
return list[0];
}

function buildModule(struct) {
trucks = getTwoFreeTrucks();
if (trucks.length <= 0)
return BUILDRET.FAILURE;
var moduleInfo = modules.filter(function(item) { return isAvailable(item.module) && item.base === struct.stattype; }).last();
if (!defined(moduleInfo))
return BUILDRET.UNAVAILABLE;
if (struct.modules >= moduleInfo.count)
return BUILDRET.UNAVAILABLE;
var success = false;
for (var i = 0; i < trucks.length; ++i)
success = orderDroidBuild(trucks[i], DORDER_BUILD, moduleInfo.module, struct.x, struct.y) || success;
if (success)
return BUILDRET.SUCCESS;
return BUILDRET.FAILURE;
}

function buildBasicStructure(statlist, importance) {
if (throttled(5000, statlist[0])) return BUILDRET.FAILURE;
// by default, don't try building things in dangerous locations
if (!defined(importance))
importance = IMPORTANCE.MANDATORY;
trucks = getTwoFreeTrucks();
if (trucks.length <= 0)
return BUILDRET.FAILURE;
// choose structure type (out of the statlist),
// together with suitable location
var idx, loc, avail = false;
for (var i = 0; i < statlist.length; ++i)
if (isAvailable(statlist[i])) {
avail = true;
if (distanceToBase(trucks[0]) <= baseScale)
loc = pickStructLocation(trucks[0], statlist[i], trucks[0].x, trucks[0].y);
else {
var rndLoc = randomLocation();
loc = pickStructLocation(trucks[0], statlist[i],rndLoc.x, rndLoc.y);
}
idx = i;
break;
}
if (!avail)
return BUILDRET.UNAVAILABLE;
if (!defined(loc))
return BUILDRET.FAILURE;
if (importance === IMPORTANCE.PEACETIME && !safeSpot(loc.x, loc.y))
return BUILDRET.FAILURE;
// now actually build
var success = false;
for (var i = 0; i < trucks.length; ++i)
success = orderDroidBuild(trucks[i], DORDER_BUILD, statlist[idx], loc.x, loc.y) || success;
if (success)
return BUILDRET.SUCCESS;
return BUILDRET.FAILURE;
}

function finishStructures() {
var success = false;
var list = enumStruct(me).filterProperty("status", BEING_BUILT);
for (var i = 0; i < list.length; ++i) {
if (success)
return;
if (throttled(10000, list[i].id))
return;
if (list[i].stattype === RESOURCE_EXTRACTOR)
return;
var truck = getFreeTruckAround(list[i].x, list[i].y);
if (!defined(truck))
return;
if (orderDroidObj(truck, DORDER_HELPBUILD, list[i]))
success = true;
};
return success;
}

function buildStructureAround(statlist, loc, unique) {
if (!defined(statlist))
return BUILDRET.UNAVAILABLE;
var truck = getFreeTruckAround(loc.x, loc.y);
if (!defined(truck))
return BUILDRET.FAILURE;
var stat = statlist.filter(isAvailable).filter(function(s) {
if (unique !== true)
return true;
var list = enumStruct(me, s);
for (var i = 0; i < list.length; ++i)
if (distance(list[i], loc) < baseScale / 2)
return false;
return true;
}).last();
if (!defined(stat))
return BUILDRET.UNAVAILABLE;
var loc2 = pickStructLocation(truck, stat, loc.x, loc.y);
if (!defined(loc2))
return BUILDRET.FAILURE;
// if we're not into turtling, don't build too many towers
if (personality.defensiveness < 100 && distance(loc2, loc) > baseScale / 5)
return BUILDRET.FAILURE;
if (orderDroidBuild(truck, DORDER_BUILD, stat, loc2.x, loc2.y))
return BUILDRET.SUCCESS;
return BUILDRET.FAILURE;
}

function captureOil(oil) {
if (!defined(oil))
return BUILDRET.FAILURE;
var truck = getFreeTruckAround(oil.x, oil.y);
if (!defined(truck))
return BUILDRET.FAILURE;
var stat = structures.derricks.filter(isAvailable).last();
if (!defined(stat))
return BUILDRET.UNAVAILABLE;
if (throttled(90000, oil.y * mapWidth + oil.x))
return BUILDRET.FAILURE;
if (orderDroidBuild(truck, DORDER_BUILD, stat, oil.x, oil.y))
return BUILDRET.SUCCESS;
return BUILDRET.FAILURE;
}

function chooseDefense(defrole) {
return weaponStatsToDefenses(chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseDefendWeaponRole(), 2, defrole), defrole);
}

function buildTowers() {
var oils = enumStructList(structures.derricks);
if (oils.length === 0)
return false;
if (withChance(70))
return buildStructureAround(chooseDefense(DEFROLE.STANDALONE), oils.random()) !== BUILDRET.UNAVAILABLE;
return buildStructureAround(chooseDefense(DEFROLE.FORTRESS).concat(structures.sensors), oils.random(), true) !== BUILDRET.UNAVAILABLE;
}

function buildGateways() {
var oils = countStructList(structures.derricks);
if (oils <= 0)
return BUILDRET.FAILURE;
var gates = enumGateways().filter(function(gate) {
var l = gate.x1 - gate.x2 + gate.y1 - gate.y2;
if (l < 0)
l = -l;
var cnt = enumRange(gate.x1, gate.y1, l, ALLIES).filterProperty("stattype", DEFENSE).length;
cnt += enumRange(gate.x2, gate.y2, l, ALLIES).filterProperty("stattype", DEFENSE).length;
cnt -= enumRange(gate.x1, gate.y1, l, ENEMIES).filterProperty("stattype", DEFENSE).length;
cnt -= enumRange(gate.x2, gate.y2, l, ENEMIES).filterProperty("stattype", DEFENSE).length;
return cnt >= 0 && (cnt < l || (personality.defensiveness === 100 && withChance(70))); // turtle AI needs to keep building towers
}).sort(function(one, two) { return distanceToBase({x: one.x1, y: one.y1}) - distanceToBase({x: two.x1, y: two.y1}); });
if (gates.length === 0)
return;
if (withChance(50))
return buildStructureAround(chooseDefense(DEFROLE.GATEWAY), {x: gates[0].x1, y: gates[0].y1}) !== BUILDRET.UNAVAILABLE;
else
return buildStructureAround(chooseDefense(DEFROLE.GATEWAY), {x: gates[0].x2, y: gates[0].y2}) !== BUILDRET.UNAVAILABLE;
}

function buildArty() {
return buildBasicStructure(chooseDefense(DEFROLE.ARTY), IMPORTANCE.PEACETIME);
}

_global.buildMinimum = function(statlist, count, importance) {
if (countStructList(statlist) < count)
if (buildBasicStructure(statlist, importance) !== BUILDRET.UNAVAILABLE)
return true;
return false;
}

_global.captureSomeOil = function() {
if (throttled(500))
return true;
function getOilList() {
var oils = [];
oilResources.forEach(function(stat) { oils = oils.concat(enumFeature(-1, stat)); });
oils = oils.concat(enumStructList(structures.derricks).filterProperty("status", BEING_BUILT));
oils = oils.sort(function(one, two) {
return distanceToBase(one) - distanceToBase(two);
});
if (oils.length > 10)
oils.length = 10;
return oils;
}
var oils = cached(getOilList, 5000);
if (countFinishedStructList(structures.derricks) >= 4 * structListLimit(structures.gens))
return false;
for (var i = 0; i < oils.length; ++i)
if (captureOil(oils[i]) === BUILDRET.SUCCESS)
return true;
return false;
}

_global.buildMinimumDerricks = function(count) {
if (countFinishedStructList(structures.derricks) < count)
if (captureSomeOil())
return true;
return false;
}

function buildExpand() {
if (myPower() > personality.maxPower) {
switch (chooseObjectType()) {
case 0:
if (needFastestResearch() === PROPULSIONUSAGE.GROUND)
if (buildMinimum(structures.factories, Infinity, IMPORTANCE.PEACETIME))
return true;
// fall-through
case 1:
if (needFastestResearch() === PROPULSIONUSAGE.GROUND)
if (buildMinimum(structures.templateFactories, Infinity, IMPORTANCE.PEACETIME))
return true;
// fall-through
case 3:
if (buildMinimum(structures.vtolFactories, Infinity, IMPORTANCE.PEACETIME))
return true;
}
}
return false;
}

function buildEnergy() {
var oils = countFinishedStructList(structures.derricks);
var gens = countStructList(structures.gens);
if (oils > 4 * gens)
if (buildBasicStructure(structures.gens, IMPORTANCE.PEACETIME) !== BUILDRET.UNAVAILABLE)
return true;
if (withChance(50))
if (captureSomeOil())
return true;
return false;
}

function buildModules() {
var str = [];
for (var i = 0; i < modules.length; ++i) {
if (modules[i].base === FACTORY && needFastestResearch() !== PROPULSIONUSAGE.GROUND)
continue;
str = enumStruct(me, modules[i].base);
for (var j = 0; j < str.length; ++j)
if (buildModule(str[j]) !== BUILDRET.UNAVAILABLE)
return true;
}
return false;
}

_global.buildVtols = function() {
if (buildMinimum(structures.vtolPads, enumDroid(me, DROID_WEAPON).filter(isVTOL).length / 2), IMPORTANCE.PEACETIME)
return true;
return false;
}

function buildExtras() {
if (throttled(180000))
return false;
if (buildBasicStructure(structures.extras, IMPORTANCE.PEACETIME) !== BUILDRET.UNAVAILABLE)
return true;
return false;
}

_global.buildDefenses = function() {
if (chooseObjectType() !== 2)
return false;
if (withChance(33)) {
if (buildTowers()) // includes sensor towers and forts
return true;
} else if (withChance(50)) {
if (buildGateways())
return true;
} else
if (buildArty())
return true;
return false;
}

function listOutdatedDefenses() {
for (var path in weaponStats) {
for (var role in DEFROLE) {
var list = weaponStatsToDefenses(weaponStats[path], DEFROLE[role]);
for (var i = 0; i < list.length - 2; ++i)
if (isAvailable(list[i + 2])) {
if (countStruct(list[i]) > 0)
return enumStruct(me, list[i]);
}
}
};
return [];
}

function recycleDefenses() {
var trucks = enumTrucks().filter(truckFree);
if (trucks.length <= 0)
return false;
var list = listOutdatedDefenses();
for (var i = 0; i < list.length; ++i)
for (var j = 0; j < trucks.length; ++j)
if (droidCanReach(trucks[j], list[i].x, list[i].y)) {
orderDroidObj(trucks[j], DORDER_DEMOLISH, list[i]);
return true;
}
return false;
}

_global.checkConstruction = function() {
if (enumTrucks().filter(truckFree).length === 0)
return;
if (functionSeries("construction", [
finishStructures,
buildOrder,
buildExpand,
buildEnergy,
buildModules,
buildVtols,
buildExtras,
recycleDefenses,
buildDefenses,
]))
queue("checkConstruction");
}

////////////////////////////////////////////////////////////////////////////////////////////
})(this);
151 changes: 151 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/chat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* This file is responsible for chat listening. It contains handler functions for different chat
* messages; each such function takes message and player as a parameter and returns a reply string.
* All chat talking (such as calling for help) is coded in other places, not here.
*
* Messages are marked as translatable when necessary.
*
*/

(function(_global) {
////////////////////////////////////////////////////////////////////////////////////////////

var prefix = '!nb';

// key: name in chat, value: function that will be executed
// function gets two params: sender and argument
var commandMap = {
set: chatSet,
res: chatRes,
truck: chatTruck,
power: chatMoney,
money: chatMoney, // alias for "power"
help: chatHelp,
go: chatHelp, // alias for "help"
tx: chatUnhelp,
}

var beaconInfo = [];

_global.noticeBeacon = function(x, y, from) {
beaconInfo[from] = {
x: x,
y: y,
exists: 1,
};
}

_global.unnoticeBeacon = function(from) {
beaconInfo[from].exists = 0;
}

_global.findBeaconPlayer = function(x, y) {
for (var i = 0; i < beaconInfo.length; ++i)
if (defined(beaconInfo[i]) && beaconInfo[i].x === x && beaconInfo[i].y === y)
return i;
}

_global.handleChatMessage = function(sender, receiver, message) {
// don't reply on any other message coming sender enemies
if (message === "!nb who") {
chat(sender, chatWho(sender));
return;
}
if (!isAlly(sender))
return;
if (message === "help me!!") { // Try to understand Nexus AI's way of calling for help
chatHelp(sender);
return;
}
var result = message.split(/ +/);
if (result[0] !== prefix)
return;
var command = result[1];
var argument = result[2];
if (defined(commandMap[command]))
chat(sender, commandMap[command](sender, argument));
}

function chatWho(sender, argument) {
var str = "NullBot3 (" + scriptName + ") ";
switch(difficulty) {
case EASY: str += _("EASY"); break;
case MEDIUM: str=str + _("MEDIUM"); break;
case HARD: str=str + _("HARD"); break;
case INSANE: str=str + _("INSANE"); break;
}
if (isAlly(sender))
str += (" ~" + personality.chatalias + "~");
return str;
}

function chatSet(sender, argument) {
var str = "";
for (var i in subpersonalities) {
if (subpersonalities[i].chatalias === argument) {
personality = subpersonalities[i];
return _("Personality change successful.");
}
str = str + " " + subpersonalities[i].chatalias;
}
return _("No such personality! Try one of these:") + str;
}

function chatRes(sender, argument) {
if (argument === "cl") {
setForcedResearch(); // clear
return _("Forced research cleared, will research anything I want now.");
}
if (argument === "no") {
setForcedResearch(null); // clear
return _("Research blocked, will research nothing now.");
}
if (argument === "fn") {
setForcedResearch(fundamentalResearch);
return _("Researching fundamental technology.");
}
var str = " cl no fn";
for (var i in weaponStats) {
if (weaponStats[i].chatalias === argument) {
setForcedResearch(weaponStatsToResList(weaponStats[i]));
return _("Researching ") + weaponStats[i].chatalias;
}
if (weaponStats[i].chatalias.indexOf("useless") < 0)
str = str + " " + weaponStats[i].chatalias;
}
return _("No such research path! Try one of these:") + str;
}

function chatTruck(sender, argument) {
var droid = enumTrucks().random();
if (!defined(droid))
return _("Sorry, I have no trucks.");
if (donateObject(droid, sender)) {
addBeacon(droid.x, droid.y, sender);
return _("You can use this one.");
}
return _("Sorry, droid transfer failed.");
}

function chatMoney(sender, argument) {
var power = Math.round(myPower()/3);
donatePower(power, sender);
return _("Power transferred.");
}

function chatHelp(sender, argument) {
if (!defined(beaconInfo[sender]) || !beaconInfo[sender].exists)
return _("Please put a beacon!");
if (setTarget({ x: beaconInfo[sender].x, y: beaconInfo[sender].y, type: POSITION }))
return _("Coming!");
else
return _("Sorry, I don't have any free forces to send for help!");
}

function chatUnhelp(sender, argument) {
unsetTarget(sender);
return _("Any time, big boss!");
}

////////////////////////////////////////////////////////////////////////////////////////////
})(this);
95 changes: 95 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/intensity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@

/*
* This file includes generic functions for improving CPU intensity,
* like caching and throttling mechanisms.
*
*/

(function(_global) {
////////////////////////////////////////////////////////////////////////////////////////////

// to make sure some function is not called more often than every 2 seconds,
// put something like that at start the of its body:
// if (throttled(2000)) return;
// if your function requires several throttling cases, you can use the optional
// notes value:
// if (throttled(60000, player)) return;
// NOTE: it won't work if the function repeatedly dies and gets created again, eg.
// a function defined inside forEach(function(...){...}) can't be throttled
_global.throttled = function(interval, notes) {
if (!defined(arguments.callee.caller.throttleTimes))
arguments.callee.caller.throttleTimes = {};
if (!defined(arguments.callee.caller.throttleTimes[notes])) {
arguments.callee.caller.throttleTimes[notes] = gameTime;
return false;
}
if (gameTime - arguments.callee.caller.throttleTimes[notes] < interval)
return true;
arguments.callee.caller.throttleTimes[notes] = gameTime;
return false;
}

// to cache a function's output value and make sure it's not re-calculated too often,
// use the following trick:
// function calculateValue(params) {
// function uncached(params) {
// // do heavy calculations
// }
// return cached(uncached, 10000);
// }
// add necessary notes to the cached() call if necessary, similar to how you do it
// for throttled().
// NOTE: it won't work if the function repeatedly dies and gets created again, eg.
// a function defined inside forEach(function(...){...}) can't have caching inside
_global.cached = function(whatToCall, interval, notes) {
if (!defined(arguments.callee.caller.cachedTimes)) {
arguments.callee.caller.cachedTimes = {};
arguments.callee.caller.cachedValues = {};
}
var t = arguments.callee.caller.cachedTimes[notes];
if (!defined(t) || gameTime - t >= interval) {
arguments.callee.caller.cachedValues[notes] = whatToCall();
arguments.callee.caller.cachedTimes[notes] = gameTime;
}
return arguments.callee.caller.cachedValues[notes];
}

// if you actually want your script to send debug messages, consider using this function.
// it will only output each message only once, so your debug log will be readable.
_global.niceDebug = function() {
var msg = me + ": " + Array.prototype.join.call(arguments, " ");
if (throttled(Infinity, msg))
return;
debug(msg);
}

// use this if you want to split a certain void function into parts.
// example:
// function first() { return random(2); }
// function second() { return random(2); }
// function third() { return random(2); }
// function doManyThings() {
// if (functionSeries("doStuff", [ first, second, third ]))
// queue("doManyThings");
// }
// NOTE: in this example functions of the list don't need to be global, but
// doManyThings does, otherwise the queue call will fail.
_global.functionSeries = function(id, list) {
if (!defined(functionSeries.last))
functionSeries.last = {};
if (!defined(functionSeries.last[id]))
functionSeries.last[id] = 0;
else
++functionSeries.last[id];
if (functionSeries.last[id] >= list.length) {
functionSeries.last[id] = 0;
return false; // all functions in the list were called, none succeeded
}
if (!list[functionSeries.last[id]]())
return true; // none of the function succeeded yet, telling to call us again next time
functionSeries.last[id] = undefined;
return false; // one of the functions succeeded, no need to call us anymore
}

////////////////////////////////////////////////////////////////////////////////////////////
})(this);
30 changes: 30 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/lassat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

/*
* This file is responsible for incoming laser satellite strikes.
* Relies on lassatSplash variable of the ruleset to figure out
* the radius of lassat.
*
*/

(function(_global) {
////////////////////////////////////////////////////////////////////////////////////////////

// pick a target and fire
_global.fireLassat = function(structure) {
list = [];
enumLivingPlayers().filter(isEnemy).forEach(function(i) {
list = list.concat(enumStruct(i), enumDroid(i));
});
var maxIdx, maxPrice = 0;
list.forEach(function(obj, idx) {
var price = enumRange(obj.x, obj.y, lassatSplash / 2, ENEMIES, false).reduce(function(prev, curr) { return prev + curr.cost; }, 0);
if (price > maxPrice) {
maxPrice = price;
maxIdx = idx;
}
});
activateStructure(structure, list[maxIdx]);
}

////////////////////////////////////////////////////////////////////////////////////////////
})(this);
149 changes: 149 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/math.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@

/*
* This file includes routines and algorithms completely unrelated to Warzone 2100.
*
*/

(function(_global) {
////////////////////////////////////////////////////////////////////////////////////////////

// return random true or false in certain chance
// 0 will return true with chance 0%
// 50 will return true with chance 50%
// 100 will return true with chance 100%
_global.withChance = function distance(chancePercent) {
return 1 + random(100) <= chancePercent;
}

// Get distance between two points
// acceptable arguments:
// distance(obj, obj)
// distance(x,y, obj)
// distance(obj,x,y)
_global.distance = function distance(obj1, obj2, obj3, obj4) {
var x1, x2, y1, y2;
if (defined(obj1.x)) {
x1 = obj1.x;
y1 = obj1.y;
if (defined(obj2.x)) {
x2 = obj2.x;
y2 = obj2.y;
} else {
x2 = obj2;
y2 = obj3;
}
} else {
x1 = obj1;
y1 = obj2;
if (defined(obj3.x)) {
x2 = obj3.x;
y2 = obj3.y;
} else {
x2 = obj3;
y2 = obj4;
}
}
return distBetweenTwoPoints(x1, y1, x2, y2);
}

_global.defined = function(variable) {
return typeof(variable) !== "undefined";
}

// returns a random number between 0 and max-1 inclusively
_global.random = function(max) {
if (max > 0)
return Math.floor(Math.random() * max);
}

// some useful array functions
Array.prototype.random = function() {
if (this.length > 0)
return this[random(this.length)];
}

Array.prototype.last = function() {
if (this.length > 0)
return this[this.length - 1];
}

Array.prototype.filterProperty = function(property, value) {
return this.filter(function(element) {
return element[property] === value;
});
}

Array.prototype.someProperty = function(property, value) {
return this.some(function(element) {
return element[property] === value;
});
}

Array.prototype.shuffle = function() {
return this.sort(function() { return 0.5 - Math.random(); })
}

_global.zeroArray = function(l) {
var ret = [];
for (var i = 0; i < l; ++i)
ret[i] = 0;
return ret;
}

_global.randomUnitArray = function(l) {
var ret = zeroArray(l);
ret[random(l)] = 1;
return ret;
}

Array.prototype.addArray = function(arr) {
for (var i = 0; i < this.length; ++i)
this[i] += arr[i];
}

// returns a random property of an object
_global.randomItem = function(obj) {
var ret;
var count = 0;
for (var i in obj)
if (Math.random() < 1/++count)
ret = i;
return obj[ret];
}

// cluster analysis happens here
_global.naiveFindClusters = function(list, size) {
var ret = { clusters: [], xav: [], yav: [], maxIdx: 0, maxCount: 0 };
for (var i = list.length - 1; i >= 0; --i) {
var x = list[i].x, y = list[i].y;
var found = false;
for (var j = 0; j < ret.clusters.length; ++j) {
if (distance(ret.xav[j], ret.yav[j], x, y) < size) {
var n = ret.clusters[j].length;
ret.clusters[j][n] = list[i];
ret.xav[j] = (n * ret.xav[j] + x) / (n + 1);
ret.yav[j] = (n * ret.yav[j] + y) / (n + 1);
if (ret.clusters[j].length > ret.maxCount) {
ret.maxIdx = j;
ret.maxCount = ret.clusters[j].length;
}
found = true;
break;
}
}
if (!found) {
var n = ret.clusters.length;
ret.clusters[n] = [list[i]];
ret.xav[n] = x;
ret.yav[n] = y;
if (1 > ret.maxCount) {
ret.maxIdx = n;
ret.maxCount = 1
}
}
}
return ret;
}

////////////////////////////////////////////////////////////////////////////////////////////
})(this);
56 changes: 56 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/misc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

/*
* A few globals that didn't have a place for them in any other file
*
*/

var baseLocation = startPositions[me];

function distanceToBase(loc) {
return distance(loc, baseLocation);
}

function canReachFromBase(propulsion, location) {
if (defined(propulsion))
return propulsionCanReach(propulsion, baseLocation.x, baseLocation.y, location.x, location.y);
}

function myPower() {
return playerPower(me) - queuedPower(me);
}

function isAvailable(stat) {
return isStructureAvailable(stat, me);
}

function isAlly(player) {
return allianceExistsBetween(me, player);
}

function isEnemy(player) {
return !isAlly(player);
}

function enumLivingPlayers() {
function uncached() {
var ret = [];
for (var i = 0; i < maxPlayers; ++i) {
if (countStructList(targets, i) > 0) {
ret.push(i);
continue;
}
if (enumDroid(i).length > 0)
ret.push(i);
}
return ret;
};
return cached(uncached, 30000);
}

function enumTrucks() {
return enumDroid(me, DROID_CONSTRUCT);
}

function goEasy() {
personality = randomItem(subpersonalities);
}
239 changes: 239 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/produce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@

/*
* This file is responsible for droid production.
*
*/

(function(_global) {
////////////////////////////////////////////////////////////////////////////////////////////

function ourBuildDroid(factory, name, bodies, propulsions, weapons1, weapons2, weapons3) {
return buildDroid(factory, name, bodies, propulsions, "", "", weapons1, weapons2, weapons3);
}

function produceTruck(factory, turrets) {
var turret = truckTurrets.concat();
if (defined(turrets))
turret = turrets.concat();
turret.reverse();
// TODO: switch to using chooseBodyWeaponPair() here
var bodies = filterBodyStatsByUsage(BODYUSAGE.TRUCK, BODYCLASS.KINETIC).map(function(val) { return val.stat; });
var propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND|PROPULSIONUSAGE.HOVER);
return ourBuildDroid(factory, "Fancy Truck", bodies, propulsions, turret);
}

function chooseWeapon(forVtol) {
if (!defined(forVtol))
forVtol = false;
if (forVtol) {
var ret = chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseAttackWeaponRole(), 3);
if (defined(ret))
return ret.vtols.concat().reverse();
} else {
var ret = chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseAttackWeaponRole(), 0);
if (defined(ret))
return ret.weapons.concat().reverse();
}
}

function chooseBodyWeaponPair(bodies, weapons) {
if (!defined(bodies))
return undefined;
if (!defined(weapons))
return undefined;
for (var i = 0; i < weapons.length; ++i) {
var w = weapons[i].stat, ww = weapons[i].weight;
if (!componentAvailable(w))
continue;
for (var j = 0; j < bodies.length; ++j) {
var b = bodies[j].stat, bw = bodies[j].weight;
if (!componentAvailable(b))
continue;
switch(ww) {
case WEIGHT.ULTRALIGHT:
if (bw <= WEIGHT.LIGHT)
return {b: b, w: w};
break;
case WEIGHT.LIGHT:
if (bw <= WEIGHT.MEDIUM)
return {b: b, w: w};
break;
case WEIGHT.MEDIUM:
return {b: b, w: w};
break;
case WEIGHT.HEAVY:
if (bw >= WEIGHT.MEDIUM)
return {b: b, w: w};
break;
case WEIGHT.ULTRAHEAVY:
if (bw >= WEIGHT.HEAVY)
return {b: b, w: w};
break;
}
}
}
}

function produceTank(factory) {
// TODO: needs refactoring. Make some more clever sorting.
var bodies = []
if (chooseBodyClass() === BODYCLASS.KINETIC) {
bodies = bodies.concat(
filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.KINETIC),
filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.THERMAL)
);
} else {
bodies = bodies.concat(
filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.THERMAL),
filterBodyStatsByUsage(BODYUSAGE.GROUND, BODYCLASS.KINETIC)
);
}
var propulsions;
var ret = scopeRatings();
var rnd = random(ret.land + ret.sea);
if (!defined(rnd)) // we need only vtols?
return false;
propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.GROUND);
if (iHaveHover()) {
if (rnd >= ret.land)
propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.HOVER);
} else {
if (ret.land === 0)
return false;
}
var bwPair = chooseBodyWeaponPair(bodies, chooseWeapon());
if (!defined(bwPair))
return false;
return ourBuildDroid(factory, "Tank", bwPair.b, propulsions, bwPair.w, bwPair.w, bwPair.w);
}

function produceVtol(factory) {
// TODO: consider thermal bodies
var bodies = filterBodyStatsByUsage(BODYUSAGE.AIR, BODYCLASS.KINETIC)
var propulsions = getPropulsionStatsComponents(PROPULSIONUSAGE.VTOL);
var bwPair = chooseBodyWeaponPair(bodies, chooseWeapon(true));
if (!defined(bwPair))
return false;
return ourBuildDroid(factory, "VTOL", bwPair.b, propulsions, bwPair.w, bwPair.w, bwPair.w);
}

function produceTemplateFromList(factory, list) {
var ret = scopeRatings();
for (var i = list.length - 1; i >= 0; --i) {
if (ret.land === 0 && !isHoverPropulsion(list[i].prop) && !isVtolPropulsion(list[i].prop))
continue;
if (ret.land === 0 && ret.sea === 0 && !isVtolPropulsion(list[i].prop))
continue;
if (isVtolPropulsion(list[i].prop) !== (factory.stattype === VTOL_FACTORY))
continue;
if ((!randomTemplates) || withChance(100 / (i + 1)))
if (ourBuildDroid(factory, "Template Droid", list[i].body, list[i].prop, list[i].weapons[0], list[i].weapons[1], list[i].weapons[2]))
return true;
}
return false;
}

function produceTemplate(factory) {
var path = chooseAvailableWeaponPathByRoleRatings(getProductionPaths(), chooseAttackWeaponRole(), 1);
if (defined(path))
return produceTemplateFromList(factory, path.templates);
return false;
}

_global.checkTruckProduction = function() {
var trucks = enumTrucks();
var hoverTrucksCount = trucks.filter(function(droid) { return isHoverPropulsion(droid.propulsion); }).length;
if (iHaveHover() && hoverTrucksCount < personality.minHoverTrucks) {
groundTrucks = trucks.filter(function(droid) { return !isHoverPropulsion(droid.propulsion); });
if (groundTrucks.length > personality.minTrucks) {
groundTrucks.length -= personality.minTrucks;
groundTrucks.forEach(function(droid) { orderDroid(droid, DORDER_RECYCLE); });
return false;
}
}
if (trucks.length >= getDroidLimit(me, DROID_CONSTRUCT))
return false;
if (trucks.length < personality.minTrucks || myPower() > personality.maxPower
|| (iHaveHover() && hoverTrucksCount < personality.minHoverTrucks)
) {
var f;
f = enumFinishedStructList(structures.factories)[0];
if (defined(f))
if (structureIdle(f))
if (produceTruck(f))
return true;
if (defined(f))
return false;
f = enumFinishedStructList(structures.templateFactories)[0];
if (defined(f))
if (structureIdle(f))
if (produceTemplateFromList(f, truckTemplates))
return true;
}
if (!iHaveArty())
return false;
var sensors = enumDroid(me, DROID_SENSOR).length;
if (withChance(100 - 100 * sensors / personality.maxSensors)) {
f = enumFinishedStructList(structures.factories)[0];
if (defined(f))
if (structureIdle(f))
if (produceTruck(f, sensorTurrets))
return true;
}
return false;
}

function checkTankProduction() {
if (!iCanDesign())
return false; // don't cheat by producing tanks before design is available (also saves money for early generators)
var success = false;
enumIdleStructList(structures.factories).forEach(function(factory) {
success = success || produceTank(factory);
});
return success;
}

function checkTemplateProduction() {
var success = false;
enumIdleStructList(structures.templateFactories)
.concat(enumIdleStructList(structures.vtolFactories))
.forEach(function(factory)
{
success = success || produceTemplate(factory);
});
return success;
}

function checkVtolProduction() {
var success = false;
if (!iCanDesign())
return false; // don't cheat by producing vtols before design is available
enumIdleStructList(structures.vtolFactories).forEach(function(factory) {
success = success || produceVtol(factory);
});
return success;
}

_global.checkProduction = function() {
switch(chooseObjectType()) {
case 1:
if (checkTemplateProduction())
return;
case 3:
if (checkVtolProduction())
return;
default:
if (checkTankProduction())
return;
}
// if having too much energy, don't care about what we produce
if (myPower() > personality.maxPower) {
queue("checkConstruction");
checkTemplateProduction();
checkTankProduction();
checkVtolProduction();
}
}

////////////////////////////////////////////////////////////////////////////////////////////
})(this);
87 changes: 87 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/research.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@

/*
* This file controls the AI's research choices.
*
*/

(function(_global) {
////////////////////////////////////////////////////////////////////////////////////////////

var forcedResearch;

_global.setForcedResearch = function(list) {
forcedResearch = list;
}

_global.needFastestResearch = function() {
var ret = scopeRatings();
if (ret.land === 0 && ret.sea === 0 && !iHaveVtol())
return PROPULSIONUSAGE.VTOL;
if (ret.land === 0 && ret.sea !== 0 && !iHaveHover() && !iHaveVtol())
return PROPULSIONUSAGE.HOVER;
return PROPULSIONUSAGE.GROUND;
}

function doResearch(lab) {
if (defined(forcedResearch)) {
if (forcedResearch === null)
return false;
if (pursueResearch(lab, forcedResearch))
return true;
}
// if we need to quickly get a certain propulsion to reach the enemy, prioritize that.
var fastest = needFastestResearch();
if (fastest === PROPULSIONUSAGE.VTOL)
if (pursueResearch(lab, [
propulsionStatsToResList(PROPULSIONUSAGE.VTOL),
fastestResearch,
].random()))
return true;
if (fastest === PROPULSIONUSAGE.HOVER)
if (pursueResearch(lab, [
propulsionStatsToResList(PROPULSIONUSAGE.HOVER),
propulsionStatsToResList(PROPULSIONUSAGE.VTOL),
fastestResearch,
].random()))
return true;
// otherwise, start with completing the fixed research path
if (defined(personality.earlyResearch))
if (pursueResearch(lab, personality.earlyResearch))
return true;
// then, see if we want to research some weapons
var objType = chooseObjectType();
if (withChance(70)) { // TODO: make a more thoughtful decision here
var list = weaponStatsToResList(chooseAvailableWeaponPathByRoleRatings(personality.weaponPaths, chooseWeaponRole()), objType);
if (pursueResearch(lab, list))
return true;
}
if (withChance(65)) { // TODO: make a more thoughtful decision here
if (chooseBodyClass() === BODYCLASS.KINETIC) {
if (withChance(40))
if (pursueResearch(lab, classResearch.kinetic[objType]))
return true;
if (objType === OBJTYPE.TANK || objType === OBJTYPE.VTOL || (objType === OBJTYPE.DEFS && personality.defensiveness < 100))
if (pursueResearch(lab, bodyStatsToResList(BODYCLASS.KINETIC)))
return true;
} else {
if (withChance(40))
if (pursueResearch(lab, classResearch.thermal[objType]))
return true;
if (objType === OBJTYPE.TANK || objType === OBJTYPE.VTOL || (objType === OBJTYPE.DEFS && personality.defensiveness < 100))
if (pursueResearch(lab, bodyStatsToResList(BODYCLASS.THERMAL)))
return true;
}
}
// if nothing of the above holds, do some generic research
if (pursueResearch(lab, fundamentalResearch))
return true;
return false;

}

_global.checkResearch = function() {
enumIdleStructList(structures.labs).forEach(doResearch);
}

////////////////////////////////////////////////////////////////////////////////////////////
})(this);
233 changes: 233 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@

/*
* This file contain functions for manipulating stats defined by rulesets.
*
*/

(function(_global) {
////////////////////////////////////////////////////////////////////////////////////////////

_global.isHoverPropulsion = function(str) {
return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.HOVER && val.stat === str; });
}

_global.isVtolPropulsion = function(str) {
return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.VTOL && val.stat === str; });
}

_global.iHaveHover = function() {
return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.HOVER && componentAvailable(val.stat);} );
}

_global.iHaveVtol = function() {
return propulsionStats.some(function(val) { return val.usage === PROPULSIONUSAGE.VTOL && componentAvailable(val.stat);} );
}

_global.iHaveArty = function() {
for (var stat in weaponStats)
for (var i = 0; i < weaponStats[stat].defenses.length; ++i)
if (weaponStats[stat].defenses[i].defrole === DEFROLE.ARTY)
if (countStruct(weaponStats[stat].defenses[i].stat) > 0)
return true;
return false;
}

// works with stored droid objects too!
_global.safeIsVtol = function(droid) {
return isVtolPropulsion(droid.propulsion);
}

_global.enumStructList = function(list, player) {
if (!defined(player))
player = me;
return list.reduce(function(summ, new_value) { return summ.concat(enumStruct(player, new_value)); }, []);
}

_global.countStructList = function(list, player) {
if (!defined(player))
player = me;
return list.reduce(function(summ, new_value) { return summ + countStruct(new_value, player); }, 0);
}

_global.enumFinishedStructList = function(list, player) {
return enumStructList(list, player).filterProperty("status", BUILT);
}

_global.countFinishedStructList = function(list, player) {
return enumFinishedStructList(list, player).length;
}

_global.enumIdleStructList = function(list, player) {
return enumFinishedStructList(list, player).filter(structureIdle);
}

_global.structListLimit = function(list) {
return list.reduce(function (summ, val) {return summ + getStructureLimit(val)}, 0)
}

_global.guessWeaponRole = function (name) {
for (var stat in weaponStats) {
if (
weaponStats[stat].weapons.someProperty("stat", name) ||
weaponStats[stat].vtols.someProperty("stat", name) ||
weaponStats[stat].templates.some(function (i) {return i.weapons.indexOf(name) > -1;})
)
return weaponStats[stat].roles;
}
niceDebug("Ruleset warning: Couldn't guess weapon role of", name);
}

function guessWeaponMicro(name) {
function uncached() {
for (var stat in weaponStats)
if (weaponStats[stat].weapons.someProperty("stat", name))
return weaponStats[stat].micro;
if (weaponStats[stat].templates.some(function(i) { return i.weapons.indexOf(name) > -1; }))
return weaponStats[stat].micro;
}
return cached(uncached, Infinity, name);
}

_global.guessDroidMicro = function(droid) {
for (var i = 0; i < droid.weapons.length; ++i) {
var ret = guessWeaponMicro(droid.weapons[i].name);
if (ret !== MICRO.RANGED)
return ret;
}
return MICRO.RANGED;
}

_global.guessBodyArmor = function(name) {
var body = bodyStats.filterProperty("stat", name).last()
if (defined(body))
return body.armor;
else
niceDebug("Ruleset warning: Couldn't guess body class of", name);
}

function weaponPathIsAvailable(path, objectType, defrole) {
switch(objectType) {
case 0:
return path.weapons.some(function(val) { return componentAvailable(val.stat); })
case 1:
return path.templates.some(function(val) {
for (var i = 0; i < val.weapons.length; ++i)
if (!componentAvailable(val.weapons[i]))
return false;
return componentAvailable(val.body) && componentAvailable(val.prop);
});
case 2:
return path.defenses.some(function(val) { return val.defrole === defrole && isAvailable(val.stat); });
case 3:
return path.vtols.some(function(val) { return componentAvailable(val.stat); });
default: // research
return true;
}
}

_global.getProductionPaths = function() {
if (!defined(fallbackWeapon) || gameTime > 600000)
return personality.weaponPaths;
return [weaponStats[fallbackWeapon]].concat(personality.weaponPaths);
}

_global.chooseAvailableWeaponPathByRoleRatings = function(paths, rating, objectType, defrole) {
var minDist = Infinity, minPath;
paths.forEach(function(path) {
if (!weaponPathIsAvailable(path, objectType, defrole))
return;
var dist = 0;
for (var i = 0; i < ROLE.LENGTH; ++i) {
var newDist = Math.abs(rating[i] - path.roles[i])
if (newDist > dist)
dist = newDist;
}
if (dist < minDist) {
minDist = dist;
minPath = path;
}
});
return minPath;
}

//
// here be functions for querying out research paths
//

function statsToResList(path) {
return path.map(function (val) { return val.res; });
}

// todo make one StatsToResList and do filtering for path outside
_global.bodyStatsToResList = function(armor) {
return statsToResList(filterBodyStatsByUsage(armor)).reverse();
}

_global.propulsionStatsToResList = function(usage) {
return statsToResList(filterDataByFlag(propulsionStats, 'usage', usage));
}

_global.weaponStatsToResList = function(path, objType) {
if (!defined(path))
return [];
var ret = [];
switch(objType) {
case 0:
ret = statsToResList(path.weapons); break;
case 1:
ret = statsToResList(path.templates); break;
case 2:
ret = statsToResList(path.defenses); break;
case 3:
ret = statsToResList(path.vtols); break;
}
if (ret.length === 0)
ret = ret.concat(
statsToResList(path.weapons),
statsToResList(path.templates),
statsToResList(path.defenses),
statsToResList(path.vtols)
);
return ret;
}

//
// here be functions for querying out component lists
//

// TODO: move this to math. If we have same structure of data <list of objects> we can use it all around.
_global.filterDataByFlag = function(data, attr_name, flag) {
return data.filter(function(obj) { return obj[attr_name] & flag; });
}

_global.filterBodyStatsByUsage = function(usage, armor) {
var data;
if (defined(armor))
data = filterDataByFlag(bodyStats, 'armor', armor)
else
data = bodyStats;
return filterDataByFlag(data, 'usage', usage).reverse();
}

_global.getPropulsionStatsComponents = function(usage) {
var data = filterDataByFlag(propulsionStats, 'usage', usage)
return data.map(function(val) { return val.stat; }).reverse()
}

//
// here be functions for querying out defensive structures
//

_global.weaponStatsToDefenses = function(stats, defrole) {
if (!defined(stats))
return [];
var ret = [];
for (var i = 0; i < stats.defenses.length; ++i)
if (!defined(defrole) || stats.defenses[i].defrole === defrole)
ret.push(stats.defenses[i].stat);
// reverse not needed here
return ret;
}

////////////////////////////////////////////////////////////////////////////////////////////
})(this);
462 changes: 462 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/tactics.js

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions data/mp/multiplay/skirmish/nb_includes/timers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

/*
* This file lists all timers used by the AI.
*
*/

(function(_global) {
////////////////////////////////////////////////////////////////////////////////////////////

// slightly shift all timers so that not to get too many of them on the same game frame,
// especially when multiple AI instances are running
function rnd() {
return random(201) - 100;
}

_global.setTimers = function() {
setTimer("spendMoney", 2000 + 3 * rnd());
setTimer("checkConstruction", 3000 + 8 * rnd());
setTimer("checkAttack", 100);
setTimer("adaptCycle", 100);
setTimer("rebalanceGroups", 10000 + 20 * rnd());
if (difficulty === EASY)
setTimer("goEasy", 30000);
}

////////////////////////////////////////////////////////////////////////////////////////////
})(this);
564 changes: 564 additions & 0 deletions data/mp/multiplay/skirmish/nb_rulesets/standard.js

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions data/mp/multiplay/skirmish/nb_turtle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@

/*
* This file defines a standard AI personality for the base game.
*
* It relies on ruleset definition in /rulesets/ to provide
* standard strategy descriptions and necessary game stat information.
*
* Then it passes control to the main code.
*
*/

// You can redefine these paths when you make a customized AI
// for a map or a challenge.
NB_PATH = "/multiplay/skirmish/";
NB_INCLUDES = NB_PATH + "nb_includes/";
NB_RULESETS = NB_PATH + "nb_rulesets/";
NB_COMMON = NB_PATH + "nb_common/";

// please don't touch this line
include(NB_INCLUDES + "_head.js");

////////////////////////////////////////////////////////////////////////////////////////////
// Start the actual personality definition

// the rules in which this personality plays
include(NB_RULESETS + "standard.js");
include(NB_COMMON + "standard_build_order.js");

// variables defining the personality
var subpersonalities = {
MR: {
chatalias: "mr",
weaponPaths: [
weaponStats.rockets_AT,
weaponStats.machineguns,
weaponStats.rockets_AS,
weaponStats.rockets_AA,
weaponStats.rockets_Arty,
],
earlyResearch: [
"R-Defense-Tower01",
"R-Defense-Pillbox01",
"R-Defense-Tower06",
"R-Defense-MRL",
],
minTanks: 3, becomeHarder: 3, maxTanks: 21,
minTrucks: 5, minHoverTrucks: 4, maxSensors: 6,
minMiscTanks: 1, maxMiscTanks: 6,
vtolness: 100,
defensiveness: 100, // this enables turtle AI specific code
maxPower: 300,
repairAt: 50,
},
MC: {
chatalias: "mc",
weaponPaths: [
weaponStats.cannons,
weaponStats.machineguns,
weaponStats.mortars,
weaponStats.fireMortars,
weaponStats.cannons_AA,
],
earlyResearch: [
"R-Defense-Tower01",
"R-Defense-Pillbox04",
"R-Defense-WallTower02",
"R-Defense-MortarPit",
],
minTanks: 3, becomeHarder: 3, maxTanks: 21,
minTrucks: 5, minHoverTrucks: 4, maxSensors: 6,
minMiscTanks: 1, maxMiscTanks: 6,
vtolness: 100, defensiveness: 100,
maxPower: 300,
repairAt: 50,
},
};

// this function describes the early build order
// you can rely on personality.chatalias for choosing different build orders for
// different subpersonalities
function buildOrder() {
// Only use this build order in early game, on standard difficulty, in T1 no bases.
// Otherwise, fall back to the safe build order.
if (gameTime > 300000 || difficulty === INSANE
|| isStructureAvailable("A0ComDroidControl") || baseType !== CAMP_CLEAN)
return buildOrder_StandardFallback();
if (buildMinimum(structures.factories, 1)) return true;
if (buildMinimum(structures.labs, 1)) return true;
if (buildMinimum(structures.hqs, 1)) return true;
if (buildMinimum(structures.labs, 3)) return true;
if (buildMinimum(structures.gens, 1)) return true;
if (buildMinimumDerricks(3)) return true;
return withChance(25) ? captureSomeOil() : buildDefenses();
}

////////////////////////////////////////////////////////////////////////////////////////////
// Proceed with the main code

include(NB_INCLUDES + "_main.js");
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"AI": {
"js": "nullbot-turtle.js",
"js": "nb_turtle.js",
"name": "Turtle AI",
"tip": "Tower wars AI, based on NullBot"
}
Expand Down
492 changes: 0 additions & 492 deletions data/mp/multiplay/skirmish/nullbot-generic.js.inc

This file was deleted.

1,036 changes: 0 additions & 1,036 deletions data/mp/multiplay/skirmish/nullbot-header.js.inc

This file was deleted.

8 changes: 0 additions & 8 deletions data/mp/multiplay/skirmish/nullbot-hover.js

This file was deleted.

250 changes: 0 additions & 250 deletions data/mp/multiplay/skirmish/nullbot-hover.js.inc

This file was deleted.

2,781 changes: 0 additions & 2,781 deletions data/mp/multiplay/skirmish/nullbot-main.js.inc

This file was deleted.

8 changes: 0 additions & 8 deletions data/mp/multiplay/skirmish/nullbot-turtle.js

This file was deleted.

285 changes: 0 additions & 285 deletions data/mp/multiplay/skirmish/nullbot-turtle.js.inc

This file was deleted.

8 changes: 0 additions & 8 deletions data/mp/multiplay/skirmish/nullbot.js

This file was deleted.