| Scripts/miningcontracts.js | "use strict";
this.name        = "miningcontracts";
this.author      = "Diagoras";
this.copyright   = "2015 Maxim Savchenko";
this.licence     = "Ms-PL";
this.description = "Excavate asteroids for reward with deadline conditions.";
this.startUp = function() {
	this.MC_contracts = [];
	if (missionVariables.MC_initialized)
	{
		this.MC_system = missionVariables.MC_system;
		this.MC_clock = missionVariables.MC_clock;
		this.MC_debt = missionVariables.MC_debt;
		this.MC_subscr = missionVariables.MC_subscr;
		var s = missionVariables.MC_contracts;
		if (s && s.length > 0) this.MC_contracts = JSON.parse(s);
	}
	else
	{
		this.MC_system = "";
		this.MC_clock = 0;
		this.MC_debt = 0;
		this.MC_subscr = 0;
	}
}
this.startUpComplete = function() {
	this.MC_countAsteroids();
	this.MC_setInstructions();
	if(player.ship.docked) this.shipDockedWithStation(player.ship.dockedStation);
}
this.shipDockedWithStation = function(station)
{
	if (!this.MC_stationPrimRoles[station.primaryRole]) return;
	if (this.MC_system != system.name)
	{
		this.MC_system = system.name;
		this.MC_clock = 0;
		this.MC_contracts = [];
		this.MC_debt = 0;
	}
	station.setInterface("MiningContracts-bm", {
		title: "Mining Contracts",
		category: "Contracts",
		summary: "Sign up for local mining operations.",
		callback: this.MC_interface.bind(this)
	});
}
this.playerWillSaveGame = function(message)
{
	missionVariables.MC_initialized = 1;
	missionVariables.MC_system = this.MC_system;
	missionVariables.MC_clock = this.MC_clock;
	missionVariables.MC_debt = this.MC_debt;
	missionVariables.MC_subscr = this.MC_subscr;
	missionVariables.MC_contracts = JSON.stringify(this.MC_contracts);
}
this.shipExitedWitchspace = function()
{
	// this.MC_markHermits();
	this.MC_countAsteroids();
	var number = this.MC_contracts.length;
	var total = this.MC_debt;
	for (var i = 0; i < number; i++)
	{
		var contract = this.MC_contracts[i];
		if (contract.signed) total += contract.amount * contract.penalty - contract.reward;
	}
	this.MC_contracts = [];
	this.MC_debt = 0;
	this.MC_clock = 0;
	if (total > 0)
	{
		player.ship.setBounty(player.bounty + 50, "contract obligations");
        var targets = system.shipsWithPrimaryRole("buoy-witchpoint");
        if (targets.length > 0) {
			targets[0].commsMessage(
				"The Mining Association of " +
				this.MC_system +
				" has put a bounty of " +
				formatCredits(50, true, true) +
				" on your head.", player.ship);
		}
	}
	this.MC_setInstructions();
}
this.MC_stationPrimRoles = {
	"rockhermit": true,
	"rockhermit-chaotic": true,
	"rockhermit-pirate": false
}
this.MC_countAsteroids = function()
{
	this.MC_icequota = system.countShipsWithRole("staer9_asteroid") / system.countShipsWithRole("asteroid");
}
this.MC_setInstructions = function()
{
	var instructions = [];
	var number = this.MC_contracts.length;
	for (var i = 0; i < number; i++)
	{
		var contract = this.MC_contracts[i];
		if (contract.signed == 1)
		{
			var msg =
				contract.amount +
				"t of " +
				contract.cargo +
				" before " +
				this.MC_clockStringToMinutes(contract.deadline);
			instructions.push(msg);
		}
	}
	this.MC_active = instructions.length > 0;
	if (this.MC_active) instructions.unshift("Mining Contracts:");
	else instructions = null;
	mission.setInstructions(instructions);
}
this.MC_clockStringToMinutes = function(time)
{
	return clock.clockStringForTime(time).slice(4,13);
}
this.MC_waitPrice = 5;
this.MC_interfaceNoGen = function()
{
	var debtMsg =
		this.MC_debt > 0 ? "\n\nYou are in debt to the Mining Association of " + this.MC_system +
		" (" + formatCredits(this.MC_debt, true, true) + ")" : "";
	var number = this.MC_contracts.length;
	var parameters = {
		title: "Mining Contracts",
		message:
			"The Rock Hermit main lounge is as dusty as everything in this place. " +
			"A couple of grumpy miners are playing cards at the only table in the corner. " +
			"A balding clerk is sleeping at the front desk of the local Mining Association, " +
			"and large blackboard with a list of current contracts is hanging on the wall behind him. " +
			"Suddenly an ancient fax machine standing on the desk squeals. " +
			"Dragged out of the hay, the clerk swears, wipes one line from a blackboard and falls back to sleep. " +
			"On the opposite wall you notice numerous square hatches layered in two rows under a flaking advert:\n\n" +
			"RESTING PODS! NO TIME LIMIT! ONLY " + formatCredits(this.MC_waitPrice, true, true) + " PER USE!" +
			debtMsg,
		choices: {
			"WAIT": "Wait for new contracts (" + formatCredits(this.MC_waitPrice, true, true) + ")",
			"XXIT": "Exit Mining Contracts"
		},
		allowInterrupt: true
	}
	for (var i = 0; i < number; i++)
	{
		var contract = this.MC_contracts[i];
		var status = ["OFFER", "TAKEN", "BLOWN"][contract.signed];
		parameters.choices["C" + i]
			= status + ": "
			+ contract.amount + "t of "
			+ contract.cargo + " before "
			+ this.MC_clockStringToMinutes(contract.deadline) + " for "
			+ formatCredits(contract.reward, true, true);
	}
	if (this.MC_debt > 0) parameters.choices["DEBT"] = "Repay debt";
	mission.runScreen(parameters, this.MC_contractDetails.bind(this));
}
this.MC_interface = function()
{
	this.MC_generateContracts();
	this.MC_interfaceNoGen();
}
this.MC_contractDetails = function(choice)
{
	if (choice == null) return;
	if (choice == "DEBT")
	{
		if (this.MC_debt > player.credits) player.consoleMessage("NOT ENOUGH CREDITS");
		else
		{
			player.commsMessage(formatCredits(this.MC_debt, true, true) + " deducted");
			player.credits -= this.MC_debt;
			this.MC_debt = 0;
		}
		this.MC_interface();
		return;
	}
	if (choice == "WAIT")
	{
		if (player.credits < this.MC_waitPrice)
		{
			player.consoleMessage("NOT ENOUGH CREDITS");
			this.MC_interface();
			return;
		}
		player.commsMessage(formatCredits(this.MC_waitPrice, true, true) + " deducted");
		player.credits -= this.MC_waitPrice;
		this.MC_waitForContract();
		this.MC_interfaceNoGen();
		return;
	}
	if (choice == "XXIT") return;
	var cid = choice.replace("C", "").split(":");
	var contract = this.MC_contracts[cid[0]];
	var debt = this.MC_debt;
	var header, descr, deliv, ps, id, option;
	var mediator = "Mining Association of " + this.MC_system;
	var warning = "\n\nn.b. The " + mediator + " reserves the right to set a bounty on the heads of defaulting contractors.";
	var terms =
		"Terms and conditions:\n" +
		"Payment is calculated and carried out after one of three events - " +
		"full delivery of required cargo before due time, " +
		"premature contract termination initiated by contractor or " +
		"coming of due time. " +
		"If forfeit penalty exceeds fee, the difference becomes contractor's debt liability to the " + mediator + ". " +
		"Contract fee meant for indebted contractor will be detained for repayment first. " +
		"Contractors leaving system before completion of obligations or debt retirement will be recognised as defaulters." +
		warning;
	function debtMsg(reward)
	{
		return debt > 0 ?
			"\n\nDue to an unpaid debt to the " + mediator + ", we are withholding " +
			formatCredits(Math.min(reward, debt), true, true) +
			" from your reward." : "";
	}
	switch (contract.signed)
	{
		case 0:
			header = "Contractual Offer";
			descr = "Subject of delivery";
			deliv = contract.amount;
			ps = terms;
			id = "SIGN";
			option = "Sign contract";
			break;
		case 1:
			descr = "Delivered";
			deliv = (contract.gross - contract.amount) + "/" + contract.gross + " ";
			if (contract.amount > 0)
			{
				header = "Operating Contract";
				ps = terms;
				id = "CARGO";
				option = "Dispatch cargo (" + Math.min(contract.amount, manifest[contract.cargo]) + "t)";
			}
			else
			{
				header = "Contract Completion";
				ps =
					"You have successfully completed a contract. " +
					"According to the terms of the contract, your reward is " + formatCredits(contract.reward, true, true) + ". " +
					"We are looking forward to working with you in the future." + debtMsg(contract.reward);
				id = "REWARD";
				option = "Collect reward";
			}
			break;
		case 2:
			header = "Terminated Contract";
			descr = "Delivered";
			deliv = (contract.gross - contract.amount) + "/" + contract.gross + " ";
			var balance = contract.reward - contract.amount * contract.penalty;
			if (balance < 0)
			{
				ps =
					"You have failed to perform a contract and the forfeit penalty exceeds your fee. " +
					"According to the terms of the contract, your debt to the " + mediator + " is increased by " + formatCredits(-balance, true, true) + ". " +
					"This debt may be liquidated with a cash deposit. " +
					"Until full debt liquidation all your future contract fees will be detained for repayment first. " +
					"If you leave system before full debt liquidation, you will be recognised as defaulter. " +
					"Please, do not delay your payments." +
					warning;
				id = "DEBT";
				option = "Take debt notification";
			}
			else
			{
				ps =
					"You have failed to perform a contract fully and the forfeit penalty has been deducted from your fee. " +
					"According to the terms of the contract, your reward is " + formatCredits(balance, true, true) + ". " +
					"Please, be more diligent next time." + debtMsg(balance);
				id = "REWARD";
				option = "Collect reward";
			}
			break;
	}
	var parameters = {
		title: header,
		message:
			"Ordering party: " + contract.company + "\n" +
			"Mediator: Mining Association of " + this.MC_system + "\n" +
			descr + ": " + deliv + "t of " + contract.cargo + "\n" +
			"Due time: " + clock.clockStringForTime(contract.deadline) + "\n" +
			"Fee: " + formatCredits(contract.reward, true, true) + "\n" +
			"Forfeit penalty: " + formatCredits(contract.penalty, true, true) + " per ton of shortage\n" +
			"\n" +
			ps,
		choices: {},
		allowInterrupt: id != "REWARD" && id != "DEBT"
	}
	parameters.choices["0_" + id + ":" + cid[0]] = option;
	var next = "2_NEXT:" + cid[0];
	var prev = "3_PREV:" + cid[0];
	if (parameters.allowInterrupt)
	{
		if (id == "CARGO") parameters.choices["1_TERMINATE:" + cid[0]] = "Terminate contract";
		if (contract.signed == 0)
		{
			parameters.choices[next] = "Next unsigned contract";
			parameters.choices[prev] = "Previous unsigned contract";
		}
		else if (contract.signed == 1)
		{
			parameters.choices[next] = "Next unfinished contract";
			parameters.choices[prev] = "Previous unfinished contract";
		}
		parameters.choices["4_BACK"] = "Back to the list";
		if (id == "SIGN") parameters.initialChoicesKey = "4_BACK";
	}
	switch (cid[1])
	{
		case "N":
			parameters.initialChoicesKey = next;
			break;
		case "P":
			parameters.initialChoicesKey = prev;
			break;
	}
	mission.runScreen(parameters, this.MC_contractAction.bind(this));
}
this.MC_contractAction = function(choice)
{
	if (choice == null) return;
	var ch = choice.split(":");
	switch (ch[0])
	{
		case "0_SIGN":
			var idx = ch[1];
			this.MC_contracts[idx].signed = 1;
			this.MC_contracts[idx].gross = this.MC_contracts[idx].amount;
			player.consoleMessage("CONTRACT SIGNED");
			this.MC_setInstructions();
			this.MC_contractDetails("C" + idx);
			break;
		case "0_DEBT":
			var idx = ch[1];
			var contract = this.MC_contracts[idx];
			var debt = contract.amount * contract.penalty - contract.reward;
			this.MC_debt += debt;
			this.MC_contracts.splice(idx, 1);
			this.MC_interface();
			break;
		case "0_REWARD":
			var idx = ch[1];
			var contract = this.MC_contracts[idx];
			var reward = contract.reward - contract.amount * contract.penalty;
			var repayment = Math.min(reward, this.MC_debt);
			reward -= repayment;
			this.MC_debt -= repayment;
			player.commsMessage(formatCredits(reward, true, true) + " received");
			player.credits += reward;
			this.MC_contracts.splice(idx, 1);
			this.MC_setInstructions();
			this.MC_interface();
			break;
		case "0_CARGO":
			var idx = ch[1];
			var contract = this.MC_contracts[idx];
			var amount = Math.min(contract.amount, manifest[contract.cargo]);
			if (amount > 0)
			{
				player.commsMessage(amount + "t of " + contract.cargo + " dispatched");
				this.MC_contracts[idx].amount -= amount;
				manifest[contract.cargo] -= amount;
				this.MC_subscr = Math.max(this.MC_subscr, this.MC_floorScale(clock.seconds, 900)) + amount * 900;
				this.MC_setInstructions();
			}
			else player.consoleMessage("no " + contract.cargo + " on board");
			this.MC_contractDetails("C" + idx);
			break;
		case "1_TERMINATE":
			var idx = ch[1];
			var contract = this.MC_contracts[idx];
			var parameters = {
				title: "Mining Contracts",
				message: "Are you sure you want to prematurely terminate the contract with " + contract.company + "?",
				choices: {},
				allowInterrupt: true
			}
			parameters.choices["0_TERMYES:" + idx] = "Yes";
			parameters.choices["1_TERMNO:" + idx] = "No";
			parameters.initialChoicesKey = "1_TERMNO:" + idx;
			mission.runScreen(parameters, this.MC_contractAction.bind(this));
			break;
		case "0_TERMYES":
			var idx = ch[1];
			this.MC_contracts[idx].signed = 2;
			player.consoleMessage("CONTRACT TERMINATED");
			this.MC_setInstructions();
			this.MC_contractDetails("C" + idx);
			break;
		case "1_TERMNO":
			var idx = ch[1];
			this.MC_contractDetails("C" + idx);
			break;
		case "2_NEXT":
			var idx = ch[1];
			var signed = this.MC_contracts[idx].signed;
			while (true)
			{
				idx++;
				if (idx >= this.MC_contracts.length) idx = 0;
				if (this.MC_contracts[idx].signed == signed) break;
			}
			this.MC_contractDetails("C" + idx + ":N");
			break;
		case "3_PREV":
			var idx = ch[1];
			var signed = this.MC_contracts[idx].signed;
			while (true)
			{
				if (idx <= 0) idx = this.MC_contracts.length;
				idx--;
				if (this.MC_contracts[idx].signed == signed) break;
			}
			this.MC_contractDetails("C" + idx + ":P");
			break;
		case "4_BACK":
			this.MC_interface();
			break;
	}
}
this.MC_randomElement = function(array)
{
	return array[Math.floor(Math.random() * array.length)];
}
this.MC_randomExponential = function ()
{
	return -Math.log(1 - Math.random());
}
this.MC_randomNormals = function()
{
	var u = Math.sqrt(this.MC_randomExponential());
	var v = 2 * Math.PI * Math.random();
	return {
		x: u * Math.cos(v),
		y: u * Math.sin(v)
	}
}
this.MC_ceilScale = function(value, scale)
{
	return scale * Math.ceil(value / scale);
}
this.MC_floorScale = function(value, scale)
{
	return scale * Math.floor(value / scale);
}
this.MC_companyPatterns = [
	"? Company Limited",
	"? plc",
	"? S.A.",
	"? Group",
	"? Gold Corporation",
	"? Inc.",
	"Potash Corporation of ? Inc.",
	"? Copper Corporation",
	"MMC ? Nickel",
	"? Platinum Limited",
	"? Mining Corporation",
	"Aluminum Corporation of ? Limited",
	"? Platinum Holdings Limited",
	"? Mining Limited",
	"?Gold",
	"? Goldfields Corporation",
	"National Copper Corporation of ?",
	"? Industries Ltd."
];
this.MC_companyName = function()
{
	return this.MC_randomElement(this.MC_companyPatterns).replace("?", randomName());
}
this.MC_randomCargo = function()
{
	return Math.random() < this.MC_icequota ? "alloys" : "minerals";
}
this.MC_logistic = function(x)
{
	return 1 / (1 + Math.exp(x));
}
this.MC_uniform = function(x, y)
{
	return x + (y - x) * Math.random();
}
this.MC_amountMu = 3;
this.MC_timeMu = 6;
this.MC_rateLambda = 4;
this.MC_generationRate = ((Math.exp(this.MC_amountMu) + 0.5) * Math.exp(this.MC_timeMu) + 450) / this.MC_rateLambda;
this.MC_createContract = function(time)
{
	var rn = this.MC_randomNormals();
	rn.y += (system.government - 3.5) / 8;
	var amount = Math.ceil(Math.exp(rn.x + this.MC_amountMu));
	var deadline = this.MC_ceilScale(time + amount * Math.exp(rn.y / 2 + this.MC_timeMu), 900);
	var expires = this.MC_uniform(time, deadline);
	var cargo = this.MC_randomCargo();
	var price = player.ship.dockedStation.market[cargo].price / 10;
	var reward = this.MC_ceilScale(this.MC_floorScale(amount * price, 10) * this.MC_logistic(rn.y), 10);
	var contract = {
		company: this.MC_companyName(),
		expires: expires,
		deadline: deadline,
		cargo: cargo,
		amount: amount,
		reward: reward,
		penalty: price,
		signed: 0
	}
	this.MC_contracts.push(contract);
}
this.MC_generateContracts = function()
{
	var prevClock = this.MC_clock;
	var nextClock = clock.seconds;
	var pregen = 96 * 3600;
	if (nextClock - prevClock > pregen) prevClock = nextClock - pregen;
	while (true)
	{
		prevClock += this.MC_randomExponential() * this.MC_generationRate;
		if (prevClock > nextClock) break;
		this.MC_createContract(prevClock);
	}
	this.MC_clock = nextClock;
	this.MC_expireContracts();
}
this.MC_waitForContract = function()
{
	this.MC_clock += this.MC_randomExponential() * this.MC_generationRate;
	this.MC_createContract(this.MC_clock);
	var time = this.MC_clock - clock.seconds;
	if (time > 0) clock.addSeconds(time);
	this.MC_expireContracts();
}
this.MC_expireContracts = function()
{
	var number = this.MC_contracts.length;
	var updated = [];
	for (var i = 0; i < number; i++)
	{
		var contract = this.MC_contracts[i];
		if (contract.signed == 1 && contract.deadline < clock.seconds) contract.signed = 2;
		if (contract.signed || contract.expires > this.MC_clock) updated.push(contract);
	}
	this.MC_contracts = updated;
	this.MC_setInstructions();
}
 |