Files
2018-07-17 14:15:37 +02:00

1489 lines
44 KiB
ActionScript

import elements.GuiContextMenu;
import tabs.Tab;
from tabs.tabbar import ActiveTab;
from obj_selection import selectedObject, selectedObjects, isSelected;
from overlays.AnomalyOverlay import AnomalyOverlay;
import resources;
import pickups;
import orbitals;
import abilities;
import util.icon_view;
import systems;
import constructions;
import dialogs.MessageDialog;
import dialogs.QuestionDialog;
import dialogs.InputDialog;
import void openSupportOverlay(Object@ obj, Object@ to) from "tabs.GalaxyTab";
import void openSupportOverlay(Object@ obj) from "tabs.GalaxyTab";
import bool switchToTab(TabCategory cat) from "tabs.tabbar";
const Sprite EXPORT_ARROW(spritesheet::ContextIcons, 0);
const Sprite COLONIZE_ICON(spritesheet::ContextIcons, 1);
const Sprite IMPORT_ARROW(spritesheet::ContextIcons, 2);
TradePath pathCheck;
#include "include/resource_constants.as"
bool playedSound = false;
void playOptionSound(const SoundSource@ sound) {
if(sound is null || playedSound)
return;
playedSound = true;
sound.play(priority=true);
}
const string tag_Weapon = "Weapon";
bool hasWeapons(Object@ obj) {
Ship@ ship = cast<Ship>(obj);
if(ship is null)
return false;
return ship.blueprint.design.hasTag(tag_Weapon);
}
class AutoColonize : MultiOption {
void call(Object@ selected) {
if(selected.isPlanet) {
Empire@ owner = selected.visibleOwner;
if(owner is null || !owner.valid) {
playerEmpire.autoColonize(selected);
playOptionSound(sound::generic_click);
}
}
}
};
class AutoColonizeLevel : MultiOption {
void call(Object@ selected) {
if(selected.isPlanet && !selected.owner.valid) {
playerEmpire.autoColonize(selected);
auto@ resType = getResource(selected.primaryResourceType);
if(resType !is null && resType.level > 0)
playerEmpire.autoImportToLevel(selected, resType.level);
playOptionSound(sound::generic_click);
}
}
};
class AutoColonizeSystem : MultiOption {
bool level = false;
AutoColonizeSystem(bool importLevel = false) {
level = importLevel;
}
void call(Object@ selected) {
Region@ region = cast<Region>(selected);
if(selected.isStar)
@region = selected.region;
if(region !is null) {
uint plCnt = region.planetCount;
for(uint i = 0; i < plCnt; ++i) {
Planet@ pl = region.planets[i];
if(pl is null)
continue;
if(pl.owner is null || pl.owner.valid)
continue;
if(!pl.known)
continue;
playerEmpire.autoColonize(pl);
if(level) {
auto@ resType = getResource(pl.primaryResourceType);
if(resType.level != 0)
playerEmpire.autoImportToLevel(pl, resType.level);
}
}
playOptionSound(sound::generic_click);
}
}
};
class CancelColonizeSystem : MultiOption {
void call(Object@ selected) {
Region@ region = cast<Region>(selected);
if(selected.isStar)
@region = selected.region;
if(region !is null) {
uint plCnt = region.planetCount;
for(uint i = 0; i < plCnt; ++i) {
Planet@ pl = region.planets[i];
if(pl is null)
continue;
if(pl.owner is null || pl.owner.valid)
continue;
if(!pl.known)
continue;
if(!pl.isBeingColonized)
continue;
playerEmpire.cancelColonization(pl);
playerEmpire.cancelAutoImportTo(pl);
pl.exportResource(0, null);
}
playOptionSound(sound::generic_click);
}
}
};
class CancelAutoColonize : MultiOption {
void call(Object@ selected) {
if(selected.hasSurfaceComponent && selected.isBeingColonized) {
playerEmpire.cancelColonization(selected);
playerEmpire.cancelAutoImportTo(selected);
selected.exportResource(0, null);
playOptionSound(sound::generic_click);
}
}
}
class Colonize : MultiOption, QuestionDialogCallback {
void call(GuiContextMenu@ menu) {
if(selected.canSafelyColonize)
MultiOption::call(menu);
else
question(locale::COLONIZE, locale::COLONIZE_UNSAFE_PROMPT,
locale::COLONIZE, locale::CANCEL, this);
}
void questionCallback(QuestionDialog@ dialog, int answer) {
if(answer == QA_Yes) {
GuiContextMenu@ m;
MultiOption::call(m);
}
}
void call(Object@ obj) {
if(selected is obj)
return;
if(selected !is null && selected.isPlanet && selected.owner.controlled) {
selected.colonize(obj, 1.0);
playOptionSound(sound::order_goto);
}
}
void draw(GuiListbox@ ele, uint flags, const recti& absPos) override {
if(!selected.canSafelyColonize)
drawRectangle(absPos, Color(0xff900040));
MultiOption::draw(ele, flags, absPos);
}
};
class RepackOrbital : Option {
void call() {
/*cast<Orbital>(selected).repack();*/
playOptionSound(sound::generic_click);
}
};
class CancelColonize : MultiOption {
void call(Object@ obj) {
if(obj is selected)
return;
if(selected.isPlanet && selected.owner.controlled) {
selected.stopColonizing(obj);
playOptionSound(sound::generic_click);
}
}
};
class CancelColonizeTarget : Option {
Object@ target;
CancelColonizeTarget(Object@ target) {
@this.target = target;
}
void call() {
if(selected.isPlanet && selected.owner.controlled) {
selected.stopColonizing(target);
playOptionSound(sound::generic_click);
}
}
};
class MoveTo : SelectionOption {
void call(Object@ selected) {
if(selected.hasLeaderAI) {
Region@ region = clicked.region;
if(clicked.isStar && region !is null)
selected.addGotoOrder(region, shiftKey);
else
selected.addGotoOrder(clicked, shiftKey);
playOptionSound(sound::order_move);
}
}
};
class OddityGate : SelectionOption {
void call(Object@ selected) {
if(selected.hasLeaderAI) {
selected.addOddityGateOrder(cast<Oddity>(clicked), shiftKey);
playOptionSound(sound::order_gate);
}
}
};
class Rally : SelectionOption {
void call(Object@ selected) {
if(selected.hasConstruction) {
selected.rallyTo(clicked);
playOptionSound(sound::order_move);
}
}
};
class ClearRally : SelectionOption {
void call(Object@ selected) {
if(selected.hasConstruction) {
selected.clearRally();
playOptionSound(sound::order_move);
}
}
};
class Attack : SelectionOption {
void call(Object@ selected) {
selected.addAttackOrder(clicked, shiftKey);
playOptionSound(sound::order_attack);
}
};
class CaptureOption : SelectionOption {
void call(Object@ selected) {
if(selected.hasLeaderAI && clicked.isPlanet) {
selected.addCaptureOrder(cast<Planet>(clicked), shiftKey);
playOptionSound(sound::order_attack);
}
}
};
class ProtectedOption : MultiOption {
void draw(GuiListbox@ ele, uint flags, const recti& absPos) override {
if(clicked.isProtected(playerEmpire))
drawRectangle(absPos, Color(0x00c0ff40));
MultiOption::draw(ele, flags, absPos);
}
};
class PickUp : SingleSelectionOption {
void call(Object@ selected) {
if(selected.hasLeaderAI && clicked.isPickup) {
selected.addPickupOrder(cast<Pickup>(clicked), shiftKey);
playOptionSound(sound::order_pickup);
}
}
};
class ScanAnomaly : SelectionOption {
void call(Object@ selected) {
if(selected.hasLeaderAI && clicked.isAnomaly) {
selected.addScanOrder(cast<Anomaly>(clicked), shiftKey);
playOptionSound(sound::order_scan);
}
}
};
class InvestigateAnomaly : Option {
void call() {
if(clicked.isAnomaly) {
AnomalyOverlay(ActiveTab, cast<Anomaly>(clicked));
playOptionSound(sound::generic_click);
}
}
};
class TransferSupport : Option {
void call() {
if(clicked !is null && clicked !is selected)
openSupportOverlay(selected, clicked);
else
openSupportOverlay(selected);
}
};
class ClaimPlanet : Option {
void call() {
clicked.annex();
playOptionSound(sound::order_attack);
}
};
class RefreshOption : SelectionOption {
void call(Object@ selected) {
if(selected.hasLeaderAI) {
selected.addRefreshOrder(clicked, shiftKey);
playOptionSound(sound::order_refresh);
}
}
};
class ScuttleOrbital : Option, QuestionDialogCallback {
void questionCallback(QuestionDialog@ dialog, int answer) {
if(answer == QA_Yes)
cast<Orbital>(clicked).scuttle();
}
void call() {
question(
locale::SCUTTLE,
format(locale::PROMPT_SCUTTLE, clicked.name),
locale::SCUTTLE, locale::CANCEL,
this).titleBox.color = Color(0xe00000ff);
}
};
class ScuttleFlagship : Option, QuestionDialogCallback {
void questionCallback(QuestionDialog@ dialog, int answer) {
if(answer == QA_Yes)
cast<Ship>(clicked).scuttle();
}
void call() {
question(
locale::SCUTTLE,
format(locale::PROMPT_SCUTTLE, clicked.name),
locale::SCUTTLE, locale::CANCEL,
this).titleBox.color = Color(0xe00000ff);
}
};
class RetrofitOption : Option {
Object@ constructAt;
Object@ constructFrom;
RetrofitOption(Object@ constructAt, Object@ constructFrom) {
@this.constructAt = constructAt;
@this.constructFrom = constructFrom;
}
void call() {
selected.retrofitFleetAt(constructAt, constructFrom=constructFrom);
playOptionSound(sound::order_refresh);
}
};
class TerraformOption : Option, IInputDialogCallback {
array<const ResourceType@> types;
double laborFactor;
double costFactor;
void call() {
@pathCheck.forEmpire = playerEmpire;
pathCheck.generate(getSystem(selected.region), getSystem(clicked.region));
if(!pathCheck.isUsablePath)
return;
laborFactor = 1.0 + config::TERRAFORM_COST_STEP * double(pathCheck.pathSize - 1);
costFactor = selected.terraformCostMod * laborFactor;
InputDialog@ dialog = InputDialog(this, null);
dialog.addTitle(locale::TERRAFORM+" "+clicked.name);
dialog.accept.text = locale::TERRAFORM;
dialog.addSelection(locale::RESOURCE);
for(uint i = 0, cnt = getResourceCount(); i < cnt; ++i) {
const ResourceType@ res = getResource(i);
if(res.terraformLabor > 0 && res.canTerraform(selected, clicked)) {
dialog.addItem(format(locale::TERRAFORM_ITEM, res.name, formatMoney(
res.terraformCost * costFactor), standardize(res.terraformLabor * laborFactor)));
types.insertLast(res);
}
}
addDialog(dialog);
changeCallback(dialog);
}
void changeCallback(InputDialog@ dialog) {
uint index = uint(dialog.getSelection(0));
if(index < types.length)
dialog.accept.disabled = !playerEmpire.canPay(types[index].terraformCost * costFactor);
else
dialog.accept.disabled = true;
}
void inputCallback(InputDialog@ dialog, bool accepted) {
if(accepted) {
uint index = uint(dialog.getSelection(0));
if(index < types.length)
selected.startTerraform(cast<Planet>(clicked), types[index].id);
}
}
};
class FinanceDryDock : Option, IInputDialogCallback {
Object@ buildAt;
const Design@ design;
double curPct = 0.0;
bool isFree = false;
FinanceDryDock() {}
FinanceDryDock(Object@ obj) {
@selected = obj;
call();
}
FinanceDryDock(const Design@ dsg, Object@ at) {
@design = dsg;
@buildAt = at;
call();
}
void call() {
Orbital@ orb = cast<Orbital>(selected);
if(orb !is null) {
@design = orb.getDesign(OV_DRY_Design);
curPct = orb.getValue(OV_DRY_Financed);
isFree = orb.getValue(OV_DRY_Free) != 0.0;
}
int price = getBuildCost(design);
price = double(price) * config::DRYDOCK_BUILDCOST_FACTOR * playerEmpire.DrydockCostFactor;
double have = playerEmpire.RemainingBudget;
if(buildAt !is null)
have -= 100.0;
double remain = ceil((1.0 - curPct) * double(price));
double maxPct = curPct + clamp(have / remain, 0.0, 1.0) * (1.0 - curPct);
InputDialog@ dialog = InputDialog(this, null);
dialog.addTitle(format(locale::FINANCE_DRY_DOCK, design.name));
dialog.accept.text = locale::FINANCE;
dialog.addSpinboxInput(locale::PERCENTAGE, defaultValue=max(maxPct*100.0,1.0),
minValue=max(curPct*100.0,1.0), maxValue=100.0, decimals=0, step=5.0);
dialog.addLabel("", FT_Medium, 0.5, true);
addDialog(dialog);
changeCallback(dialog);
}
void changeCallback(InputDialog@ dialog) {
double nextPct = dialog.getSpinboxInput(0) / 100.0;
int price = ceil(double(getBuildCost(design) * config::DRYDOCK_BUILDCOST_FACTOR) * (nextPct - curPct) * playerEmpire.DrydockCostFactor);
if(buildAt !is null)
price += 100;
int maintain = ceil(double(getMaintenanceCost(design)) * (nextPct - curPct));
if(isFree)
maintain = 0;
dialog.accept.disabled = !playerEmpire.canPay(price);
dialog.setLabel(1, formatMoney(price, maintain));
}
void inputCallback(InputDialog@ dialog, bool accepted) {
if(accepted) {
double pct = dialog.getSpinboxInput(0) / 100.0;
Orbital@ orb = cast<Orbital>(selected);
if(orb is null)
buildAt.buildDryDock(design, pct);
else
orb.sendValue(OV_DRY_Financed, pct);
}
}
};
class WorkDryDock : Option {
void call() {
selected.workDryDock(cast<Orbital>(clicked));
playOptionSound(sound::order_goto);
}
};
class ExportLabor : SelectionOption {
void call(Object@ selected) {
selected.exportLaborTo(clicked);
playOptionSound(sound::order_goto);
}
};
class SetDefending : Option {
Object@ obj;
bool val;
SetDefending(Object@ obj, bool value) {
@this.obj = obj;
this.val = value;
}
void call() {
playerEmpire.setDefending(obj, val);
playOptionSound(sound::order_refresh);
}
};
class ExportResource : Option {
Object@ from;
Object@ to;
const ResourceType@ type;
uint index;
ExportResource(Object@ From, Object@ To, const ResourceType@ Type, uint Index) {
@from = From;
@to = To;
@type = Type;
index = Index;
}
void call() {
from.exportResource(index, to);
playOptionSound(sound::order_goto);
}
int getWidth(GuiListbox@ ele) override {
return Option::getWidth(ele) + 84;
}
void draw(GuiListbox@ ele, uint flags, const recti& absPos) override {
const Font@ font = ele.skin.getFont(ele.TextFont);
int baseLine = font.getBaseline();
type.smallIcon.draw(recti_area(absPos.topLeft + vec2i(ele.horizPadding+3, (ele.lineHeight - 20) / 2), vec2i(20, 20)));
EXPORT_ARROW.draw(recti_area(absPos.topLeft + vec2i(ele.horizPadding+26, (ele.lineHeight - 24) / 2), vec2i(24, 24)));
drawObjectIcon(to, recti_area(absPos.topLeft + vec2i(ele.horizPadding+50, (ele.lineHeight - 26) / 2), vec2i(26, 26)));
vec2i textOffset(ele.horizPadding+84, (ele.lineHeight - baseLine) / 2);
font.draw(absPos.topLeft + textOffset, text);
}
};
class ExportAnyResource : SelectionOption {
void call(Object@ selected) {
if(selected.isPlanet) {
if(selected !is clicked) {
selected.exportResource(0, clicked);
playOptionSound(sound::order_goto);
}
}
else if(selected.isAsteroid) {
if(selected.nativeResourceCount == 1) {
selected.exportResource(0, clicked);
playOptionSound(sound::order_goto);
}
}
}
};
class BuildMiningBase : Option {
uint resource;
Object@ obj;
Object@ slaved;
BuildMiningBase(Object@ obj, uint resId, Object@ slaved = null) {
@this.obj = obj;
resource = resId;
@this.slaved = slaved;
}
void call() {
obj.buildAsteroid(cast<Asteroid>(clicked), resource, constructFrom=slaved);
playOptionSound(sound::order_goto);
}
};
class ImportResource : Option {
Object@ from;
Object@ to;
const ResourceType@ type;
uint index;
ImportResource(Object@ From, Object@ To, const ResourceType@ Type, uint Index) {
@from = From;
@to = To;
@type = Type;
index = Index;
}
void call() {
from.exportResource(index, to);
playOptionSound(sound::generic_click);
}
int getWidth(GuiListbox@ ele) override {
return Option::getWidth(ele) + 84;
}
void draw(GuiListbox@ ele, uint flags, const recti& absPos) override {
const Font@ font = ele.skin.getFont(ele.TextFont);
int baseLine = font.getBaseline();
drawObjectIcon(to, recti_area(absPos.topLeft + vec2i(ele.horizPadding, (ele.lineHeight - 26) / 2), vec2i(26, 26)));
IMPORT_ARROW.draw(recti_area(absPos.topLeft + vec2i(ele.horizPadding+26, (ele.lineHeight - 24) / 2), vec2i(24, 24)));
type.smallIcon.draw(recti_area(absPos.topLeft + vec2i(ele.horizPadding+53, (ele.lineHeight - 20) / 2), vec2i(20, 20)));
vec2i textOffset(ele.horizPadding+84, (ele.lineHeight - baseLine) / 2);
font.draw(absPos.topLeft + textOffset, text);
}
};
class AutoAssign : Option {
const ResourceClass@ resCls;
AutoAssign(const ResourceClass@ cls) {
@resCls = cls;
}
void call() {
playerEmpire.autoImportResourceOfClass(clicked, resCls.id);
playOptionSound(sound::generic_click);
}
};
class AutoAssignBoth : Option {
const ResourceClass@ clsOne;
const ResourceClass@ clsTwo;
AutoAssignBoth(const ResourceClass@ one, const ResourceClass@ two) {
@clsOne = one;
@clsTwo = two;
}
void call() {
playerEmpire.autoImportResourceOfClass(clicked, clsOne.id);
playerEmpire.autoImportResourceOfClass(clicked, clsTwo.id);
playOptionSound(sound::generic_click);
}
};
class AutoAssignLevel : Option {
uint level;
AutoAssignLevel(uint level) {
this.level = level;
}
void call() {
playerEmpire.autoImportToLevel(clicked, level);
playOptionSound(sound::generic_click);
}
};
class CancelAutoAssign : Option {
void call() {
playerEmpire.cancelAutoImportTo(clicked);
playOptionSound(sound::generic_click);
}
}
class StopExportResource : SingleSelectionOption {
uint index;
StopExportResource(uint Index) {
index = Index;
}
void call(Object@ selected) {
selected.exportResource(index, null);
playOptionSound(sound::generic_click);
}
};
class StopImportResource : Option {
uint index;
StopImportResource(uint Index) {
index = Index;
}
void call() {
clicked.exportResource(index, null);
playOptionSound(sound::generic_click);
}
};
class Abandon : SingleSelectionOption, QuestionDialogCallback {
Object@ planet;
Abandon() {
}
void questionCallback(QuestionDialog@ dialog, int answer) {
if(answer == QA_Yes)
planet.abandon();
}
void call(Object@ selected) {
@planet = selected;
question(
locale::ABANDON,
format(locale::PROMPT_ABANDON, selected.name),
locale::ABANDON_ACCEPT, locale::CANCEL,
this).titleBox.color = Color(0xe00000ff);
}
};
class TriggerAbility : SingleSelectionOption {
int id;
TriggerAbility(int ID) {
id = ID;
}
void call(Object@ selected) {
selected.activateAbility(id);
playOptionSound(sound::order_attack);
}
};
class TargetAbility : MultiOption {
int id;
Object@ target;
double range;
Color bg = colors::Invisible;
const AbilityType@ type;
TargetAbility(int ID, Object@ targ, double range, const AbilityType@ type) {
id = ID;
@target = targ;
this.range = range;
@this.type = type;
}
void call(Object@ obj) {
if(obj is null || (!obj.isArtifact && !obj.owner.controlled) || !obj.hasAbilities)
return;
int ablId = -1;
if(obj is this.selected) {
ablId = id;
playOptionSound(type.activateSound);
}
else {
ablId = obj.findAbilityOfType(type.id);
}
if(ablId != -1) {
if(!obj.hasLeaderAI || range == INFINITY)
obj.activateAbility(id, target);
else
obj.addAbilityOrder(id, target, shiftKey);
}
}
void draw(GuiListbox@ ele, uint flags, const recti& absPos) override {
drawRectangle(absPos, bg);
MultiOption::draw(ele, flags, absPos);
}
};
class ConstructionOption : Option {
const ConstructionType@ type;
Object@ from;
Targets targets;
Color bg(0x00000000);
ConstructionOption(const ConstructionType@ type, Object@ from, Targets@ targets) {
@this.type = type;
@this.from = from;
this.targets = targets;
}
void call() {
from.buildConstruction(type.id, objTarg=targets[0].obj);
}
void draw(GuiListbox@ ele, uint flags, const recti& absPos) override {
drawRectangle(absPos, bg);
Option::draw(ele, flags, absPos);
}
};
bool openContextMenu(Object& clicked, Object@ selected = null) {
//Play only one sound per context menu
playedSound = false;
if(selected is null)
@selected = selectedObject;
GuiContextMenu menu(mousePos);
//Find the right visible owners
Empire@ selOwner = null, clickedOwner = null;
Region@ clickedRegion;
if(selected !is null) {
if(selected.hasSurfaceComponent)
@selOwner = selected.visibleOwner;
else
@selOwner = selected.owner;
}
if(clicked !is null) {
if(clicked.hasSurfaceComponent)
@clickedOwner = clicked.visibleOwner;
else
@clickedOwner = clicked.owner;
@clickedRegion = cast<Region>(clicked);
if(clicked.isStar)
@clickedRegion = clicked.region;
}
Orbital@ orbitalMaster;
if(selected !is null && selected.isOrbital) {
Orbital@ selOrb = cast<Orbital>(selected);
if(selOrb.hasMaster())
@orbitalMaster = selOrb.getMaster();
}
Object@ constructObj;
Object@ constructSlave;
if(orbitalMaster !is null && orbitalMaster.hasConstruction) {
@constructObj = orbitalMaster;
@constructSlave = selected;
}
else if(selected !is null && selected.hasConstruction)
@constructObj = selected;
//GuiContextMenu@ sub = menu.addSubMenu("Test");
//sub.addOption("A");
//sub.addOption("B");
//sub.addOption("C");
if(selected !is null && selOwner is playerEmpire && selected.hasLeaderAI && selected.isShip) {
if(clicked.isAnomaly && cast<Anomaly>(clicked).progress < 1.f)
addOption(menu, selected, clicked, locale::SCAN_ANOMALY, ScanAnomaly(), icons::Anomaly);
}
else if(clicked.isAnomaly) {
addOption(menu, selected, clicked, locale::INVESTIGATE_ANOMALY, InvestigateAnomaly(), icons::Anomaly);
}
//Resource exporting
if(selected !is null && selected.hasResources && clicked.hasResources) {
if(selectedObjects.length > 1)
addOption(menu, selected, clicked, format(locale::EXPORT_RESOURCES, clicked.name), ExportAnyResource());
//Exports
uint cnt = selected.nativeResourceCount;
if(clicked is selected) {
if(selected.owner is playerEmpire) {
for(uint i = 0; i < cnt; ++i) {
Object@ dest = selected.nativeResourceDestination[i];
if(dest is null)
continue;
const ResourceType@ type = getResource(selected.nativeResourceType[i]);
if(!type.exportable)
continue;
if(selected.nativeResourceLocked[i])
continue;
string text;
if(selected.nativeResourceUsable[i] && dest.owner is playerEmpire)
text = format(locale::STOP_EXPORT_RESOURCE, type.name, dest.name);
else
text = format(locale::STOP_EXPORT_RES_QUEUE, type.name, dest.name);
addOption(menu, selected, clicked, text, StopExportResource(i), type.smallIcon);
}
}
}
else if(selected.exportEnabled && clicked.importEnabled
&& (selected.region !is null && clicked.region !is null)) {
for(uint i = 0; i < cnt; ++i) {
const ResourceType@ type = getResource(selected.nativeResourceType[i]);
if(!type.exportable)
continue;
if(selected.nativeResourceLocked[i])
continue;
bool usable = false;
Object@ dest = selected.nativeResourceDestination[i];
string text;
if(selected.owner is playerEmpire)
usable = selected.nativeResourceUsable[i];
if(dest is clicked) {
if(usable && clicked.owner is playerEmpire)
text = format(locale::STOP_EXPORT_RESOURCE, type.name, dest.name);
else
text = format(locale::STOP_EXPORT_RES_QUEUE, type.name, dest.name);
addOption(menu, selected, clicked, text, StopExportResource(i), type.smallIcon);
}
else {
if(usable && clicked.owner is playerEmpire)
text = format(locale::EXPORT_RESOURCE, type.name, clicked.name);
else
text = format(locale::QUEUE_EXPORT_RESOURCE, type.name, clicked.name);
addOption(menu, selected, clicked, text, ExportResource(selected, clicked, type, i));
}
}
}
//Imports
if(clicked !is selected) {
if(selected.importEnabled && clicked.exportEnabled
&& (selected.region !is null && clicked.region !is null)) {
cnt = clicked.nativeResourceCount;
for(uint i = 0; i < cnt; ++i) {
const ResourceType@ type = getResource(clicked.nativeResourceType[i]);
if(!type.exportable)
continue;
bool usable = false;
Object@ dest = clicked.nativeResourceDestination[i];
string text;
if(clicked.owner is playerEmpire)
usable = clicked.nativeResourceUsable[i];
if(dest is selected) {
if(usable && selected.owner is playerEmpire)
text = format(locale::STOP_IMPORT_RESOURCE, type.name, clicked.name);
else
text = format(locale::STOP_IMPORT_RES_QUEUE, type.name, clicked.name);
addOption(menu, selected, clicked, text, StopImportResource(i), type.smallIcon);
}
else {
if(usable && selected.owner is playerEmpire)
text = format(locale::IMPORT_RESOURCE, type.name, clicked.name);
else
text = format(locale::QUEUE_IMPORT_RESOURCE, type.name, clicked.name);
addOption(menu, clicked, selected, text, ImportResource(clicked, selected, type, i));
}
}
}
}
}
//Auto-assign resources
if(clicked.isPlanet && (clickedOwner is playerEmpire || clicked.isBeingColonized)) {
if(clicked.hasAutoImports) {
addOption(menu, selected, clicked, locale::CANCEL_AUTO_IMPORT, CancelAutoAssign());
}
else {
uint clickedLevel = clicked.level;
uint assignLevel = 0;
auto@ resType = getResource(clicked.primaryResourceType);
if(resType !is null) {
if(clickedLevel > resType.level) {
assignLevel = min(5, clickedLevel+1);
}
else if(clickedLevel == resType.level) {
if(clicked.nativeResourceDestination[0] is null)
assignLevel = clickedLevel+1;
else
assignLevel = 0;
}
else {
assignLevel = resType.level;
}
}
if(assignLevel != 0 && assignLevel > clickedLevel && assignLevel < uint(clicked.maxLevel)) {
Sprite icon;
if(assignLevel <= 3)
icon = Sprite(spritesheet::ResourceClassIcons, clamp(assignLevel-1, 0, 2));
addOption(menu, selected, clicked, format(locale::AUTO_IMPORT_LEVEL, toString(assignLevel)),
AutoAssignLevel(assignLevel), icon);
}
}
}
//Colonization
if(clicked.isPlanet && playerEmpire.NoAutoColonize == 0) {
bool quarantined = clicked.quarantined;
bool addedColonyOptions = false;
if(selected !is null && selected.owner is playerEmpire && selected.isPlanet
&& selected !is clicked && selected.maxPopulation > 1) {
//Colonization from selected planet
if(!selected.hasColonyTarget(clicked)) {
if(clickedOwner is null || !clickedOwner.valid) {
if(!quarantined) {
addedColonyOptions = true;
//TODO: Take slipstream & gate into account
double eta = 1.0;
eta += newtonArrivalTime(selected.colonyShipAccel, clicked.position - selected.position, vec3d()) / 60.0;
if(selected.isColonizing)
eta += double(selected.colonyOrderCount);
if(selected.owner.HasFlux != 0)
eta = 0;
if(playerEmpire.ForbidColonization == 0) {
if(eta <= 0)
addOption(menu, selected, clicked, format(locale::COLONIZE_WITH_BASIC, selected.name), Colonize(), COLONIZE_ICON);
else
addOption(menu, selected, clicked, format(locale::COLONIZE_WITH, selected.name, toString(eta, 1)), Colonize(), COLONIZE_ICON);
}
if(clicked.isBeingColonized)
addOption(menu, selected, clicked, locale::CANCEL_AUTO_COLONIZE, CancelAutoColonize());
}
}
else if(clicked.owner is playerEmpire && clicked.population < 1) {
if(playerEmpire.ForbidColonization == 0)
addOption(menu, selected, clicked, locale::COLONIZE_CONTINUE, Colonize(), COLONIZE_ICON);
addedColonyOptions = true;
}
}
else {
string text = format(locale::COLONIZE_CANCEL, clicked.name);
addOption(menu, selected, clicked, text, CancelColonize());
addedColonyOptions = true;
}
}
if(!addedColonyOptions) {
//Auto-colonization
if(clicked.isBeingColonized) {
addOption(menu, selected, clicked, locale::CANCEL_AUTO_COLONIZE, CancelAutoColonize());
}
else {
if(clickedOwner is null || !clickedOwner.valid) {
addOption(menu, selected, clicked, quarantined ? locale::AUTO_COLONIZE_BLOCKED : locale::AUTO_COLONIZE, AutoColonize(), COLONIZE_ICON);
auto@ resType = getResource(clicked.primaryResourceType);
if(resType !is null && resType.level > 0) {
addOption(menu, selected, clicked,
format(locale::AUTO_COLONIZE_LEVEL, resType.level),
AutoColonizeLevel(),
Sprite(spritesheet::ResourceClassIcons, clamp(resType.level-1, 0, 2)));
}
}
}
}
}
if(selected !is null && selected.owner.controlled) {
if(selected.isPlanet && clicked.isPlanet) {
//Colonization order management
if(selected is clicked) {
uint colonyOrders = selected.colonyOrderCount;
for(uint i = 0; i < colonyOrders; ++i) {
Object@ target = selected.colonyTarget[i];
if(target is null)
break;
string text = format(locale::COLONIZE_CANCEL, target.name);
addOption(menu, selected, clicked, text, CancelColonizeTarget(target));
}
}
}
//Transfering support ships
if(clicked !is null && clicked.owner is selected.owner) {
if(selected.hasLeaderAI && selected.SupplyCapacity > 0) {
if(selected is clicked) {
addOption(menu, selected, clicked, locale::MANAGE_SUPPORTS, TransferSupport(), icons::ManageSupports);
}
else if(clicked.hasLeaderAI && clicked.SupplyCapacity > 0) {
addOption(menu, selected, clicked, locale::TRANSFER_SUPPORT_SHIPS, TransferSupport(), icons::ManageSupports);
}
}
}
//Pickup goodies
if(selected.hasLeaderAI && clicked.isPickup && selected.hasMover && selected.maxAcceleration > 0) {
Pickup@ pickup = cast<Pickup>(clicked);
const PickupType@ type = getPickupType(pickup.PickupType);
Object@ prot = pickup.getProtector();
if(prot !is null)
addOption(menu, selected, prot, locale::ATTACK, Attack());
if(shiftKey || (!pickup.isPickupProtected && type.canPickup(pickup, selected)))
addOption(menu, selected, clicked, format(type.verb, type.name), PickUp());
}
//Combat options
if(clickedOwner !is selected.owner && clickedOwner !is null && clickedOwner.valid) {
Ship@ ship = cast<Ship>(selected);
if(ship !is null && ship.hasLeaderAI) {
const Design@ dsg = ship.blueprint.design;
if(dsg !is null) {
bool hasWeapons = dsg.hasTag(ST_Weapon);
bool hasSupportAttack = ship.supportCount > 0 && ship.getFleetDPS() > 0;
if((hasWeapons && ship.blueprint.canTarget(ship, clicked)) || (hasSupportAttack && (clicked.isShip || clicked.isOrbital)))
addOption(menu, selected, clicked, locale::ATTACK, Attack(), icons::Strength);
}
}
}
if(selected.isShip && selected.hasLeaderAI && clicked.isPlanet
&& clickedOwner !is null && clickedOwner.valid && clickedOwner !is playerEmpire) {
if(playerEmpire.isHostile(clickedOwner) && !selected.hasOrbit) {
if(clicked.isProtected(playerEmpire)) {
addOption(menu, selected, clicked, locale::PROTECTED_OPTION, ProtectedOption(), icons::Strength * Color(0xff0000ff));
}
else {
double base = clicked.baseLoyalty;
double loy = clicked.currentLoyalty;
double timer = config::SIEGE_LOYALTY_TIME * ceil(base / 10.0) * (loy / max(base, 1.0));
timer *= selected.owner.CaptureTimeFactor;
timer *= clicked.owner.CaptureTimeDifficulty;
double cost = config::SIEGE_LOYALTY_SUPPLY_COST * loy;
cost *= selected.owner.CaptureSupplyFactor;
cost *= clicked.owner.CaptureSupplyDifficulty;
addOption(menu, selected, clicked, format(locale::CAPTURE_OPTION, standardize(cost), formatTime(timer)), CaptureOption(), icons::Strength * Color(0xff8000ff));
}
}
}
//Movement options
if(selected.hasMover && selected.hasLeaderAI && (!selected.hasOrbit || selected.maxAcceleration > 0)) {
if(clicked !is null && clicked.isOddity && cast<Oddity>(clicked).isGate())
addOption(menu, selected, clicked, locale::ODDITY_GATE, OddityGate());
Object@ nameObj = clickedRegion is null ? @clicked : @clickedRegion;
addOption(menu, selected, clicked, format(locale::MOVE_TO_OBJ, formatObjectName(nameObj)), MoveTo(),
Sprite(spritesheet::ContextIcons, 0, Color(0xffcd00ff)));
}
}
//System actions
if(selected !is null && selected.owner is playerEmpire) {
//Support resupply
if(clicked !is null && selected.isShip && selected.hasLeaderAI) {
if(clicked.isStar && clicked.region.AvailSupportMask & playerEmpire.mask != 0) {
addOption(menu, selected, clicked.region, format(locale::REFRESH_SUPPORTS, clicked.region.name), RefreshOption(),
icons::ManageSupports);
}
else if((clicked.isPlanet || (clicked.isOrbital && clicked.hasLeaderAI)) && clickedOwner is playerEmpire && clicked.supportCount > 0) {
addOption(menu, selected, clicked, format(locale::REFRESH_SUPPORTS, clicked.name), RefreshOption(),
icons::ManageSupports);
}
}
//Asteroid base construction
if(clicked !is null && constructObj !is null && constructObj.hasConstruction
&& constructObj.canBuildAsteroids && clicked.isAsteroid &&
cast<Asteroid>(clicked).canDevelop(playerEmpire)
&& clicked.region !is null && constructObj.region !is null) {
Object@ pathFrom = constructObj;
if(constructSlave !is null && constructSlave.region !is null)
@pathFrom = constructSlave;
@pathCheck.forEmpire = playerEmpire;
pathCheck.generate(getSystem(pathFrom.region), getSystem(clicked.region));
if(pathCheck.isUsablePath) {
double costFactor = 1.0 + config::ASTEROID_COST_STEP * double(pathCheck.pathSize - 1);
Asteroid@ asteroid = cast<Asteroid>(clicked);
for(uint i = 0, cnt = asteroid.getAvailableCount(); i < cnt; ++i) {
uint resId = asteroid.getAvailable(i);
double cost = asteroid.getAvailableCost(i);
const ResourceType@ type = getResource(resId);
if(type !is null && cost > 0.0)
addOption(menu, selected, clicked,
format(locale::BUILD_ASTEROID_OPTION,
type.name, standardize(cost*costFactor, true)),
BuildMiningBase(constructObj, resId, constructSlave),
type.smallIcon);
}
}
}
//Terraforming
if(clicked !is null && selected.hasConstruction
&& selected.canTerraform && clicked.isPlanet
&& clickedOwner is playerEmpire && !clicked.isTerraforming()
&& clicked.region !is null && selected.region !is null
&& clicked !is selected
&& config::ENABLE_TERRAFORMING != 0
&& playerEmpire.ForbidTerraform == 0) {
@pathCheck.forEmpire = playerEmpire;
pathCheck.generate(getSystem(selected.region), getSystem(clicked.region));
if(pathCheck.isUsablePath)
addOption(menu, selected, clicked, locale::TERRAFORM_OPTION, TerraformOption());
}
if(selected is clicked && selected.isPlanet) {
//Abandon planet
if(!selected.isContested)
addOption(menu, selected, clicked, locale::ABANDON, Abandon(), icons::UnderSiege * colors::Red);
}
//Retrofit options
if(selected.hasLeaderAI && clicked !is null && clicked.owner is playerEmpire
&& selected.region !is null && selected.region is clicked.region) {
//Figure out if we should redirect this
Object@ constructFrom;
Object@ constructAt;
if(clicked.isOrbital) {
Orbital@ orb = cast<Orbital>(clicked);
if(orb.hasMaster()) {
@constructAt = orb.getMaster();
@constructFrom = clicked;
}
else {
@constructAt = clicked;
}
}
else {
@constructAt = clicked;
}
if(constructAt.hasConstruction && constructAt.canBuildShips) {
int cost = selected.getRetrofitCost();
if(cost >= 0) {
double labor = selected.getRetrofitLabor();
addOption(menu, selected, clicked, format(locale::RETROFIT_OPTION, formatMoney(cost), standardize(labor, true)), RetrofitOption(constructAt, constructFrom));
}
}
}
}
//System colonization
if(clicked.isStar && playerEmpire.NoAutoColonize == 0) {
Region@ region = clicked.region;
if(region !is null) {
//Check if there are any uncolonized planets
uint plCnt = region.planetCount;
bool hasUncolonized = false, hasUnderleveled = false, hasColonizing = false;
for(uint i = 0; i < plCnt; ++i) {
Planet@ pl = region.planets[i];
if(pl is null)
continue;
if(pl.owner is null || pl.owner.valid)
continue;
if(!pl.known)
continue;
if(pl.isBeingColonized) {
hasColonizing = true;
continue;
}
hasUncolonized = true;
auto@ resType = getResource(pl.primaryResourceType);
if(resType !is null && resType.level != 0)
hasUnderleveled = true;
}
if(hasUncolonized) {
addOption(menu, selected, clicked, format(locale::AUTO_COLONIZE_SYSTEM, region.name), AutoColonizeSystem(), COLONIZE_ICON);
if(hasUnderleveled)
addOption(menu, selected, clicked, format(locale::AUTO_COLONIZE_SYSTEM_LEVEL, region.name), AutoColonizeSystem(true), Sprite(spritesheet::ResourceClassIcons, 7));
}
if(hasColonizing)
addOption(menu, selected, clicked, format(locale::STOP_COLONIZE_SYSTEM, region.name), CancelColonizeSystem());
}
}
//Defense projection
if((selected is clicked && selected.owner is playerEmpire && selected.isPlanet)
|| (clickedRegion !is null && clickedRegion.PlanetsMask & playerEmpire.mask != 0)
|| (clickedOwner is playerEmpire && clicked.isOrbital && clicked.hasLeaderAI && clicked.region !is null && clicked.region.TradeMask & playerEmpire.TradeMask.value != 0 && clicked.SupplyCapacity > 0)
|| playerEmpire.isDefending(clicked)) {
Object@ obj = clickedRegion is null ? @clicked : @clickedRegion;
bool isDefending = playerEmpire.isDefending(obj);
if(isDefending) {
Sprite sprt = icons::Defense;
sprt.color = colors::Red;
addOption(menu, selected, obj, format(locale::DEFENSE_OFF_OPTION, obj.name),
SetDefending(obj, false), sprt);
}
else {
addOption(menu, selected, clicked, format(locale::DEFENSE_ON_OPTION, obj.name),
SetDefending(obj, true), icons::Defense);
}
}
//Finance options
if(clicked is selected && clicked.owner is playerEmpire) {
Orbital@ orb = cast<Orbital>(clicked);
if(orb !is null && orb.getDesign(OV_DRY_Design) !is null && orb.getValue(OV_DRY_Financed) < 1.f)
addOption(menu, selected, clicked, locale::FINANCE_OPTION, FinanceDryDock());
}
else if(selected !is null && clicked !is null && clicked !is selected && selected.owner is playerEmpire && clicked.owner is playerEmpire) {
Orbital@ orb = cast<Orbital>(clicked);
if(orb !is null && orb.getDesign(OV_DRY_Design) !is null && selected.hasConstruction && selected.canBuildShips)
addOption(menu, selected, clicked, locale::WORK_DRY_DOCK, WorkDryDock());
}
//Labor exporting
if(selected !is null && selected.owner is playerEmpire && clickedOwner is playerEmpire
&& selected.hasConstruction && selected.canExportLabor && clicked.hasConstruction && clicked.canImportLabor
&& selected.laborIncome > 0) {
addOption(menu, selected, clicked, format(locale::EXPORT_LABOR, clicked.name), ExportLabor(), icons::Labor);
}
//Rallying
if(selected !is null && selected.owner is playerEmpire && selected.hasConstruction)
if(selected.isRallying && (selected is clicked || selected.rallyObject is clicked))
addOption(menu, selected, clicked, locale::STOP_RALLY, ClearRally(), COLONIZE_ICON);
else if(selected.canBuildShips)
addOption(menu, selected, clicked, format(locale::RALLY_TO, clicked.name), Rally(), COLONIZE_ICON);
//Scuttle options
if(clicked is selected && clicked.owner.controlled) {
if(clicked.isOrbital && !cast<Orbital>(clicked).isContested)
addOption(menu, selected, clicked, locale::SCUTTLE, ScuttleOrbital());
else if(clicked.isShip && clicked.hasLeaderAI)
addOption(menu, selected, clicked, locale::SCUTTLE, ScuttleFlagship());
}
//Abilities
if(selected !is null && selected.hasAbilities
&& (selected.owner.controlled || selected.isArtifact)) {
array<Ability> abilities;
abilities.syncFrom(selected.getAbilities());
Targets targs;
@targs.add(TT_Object, fill=true).obj = clicked;
for(uint i = 0, cnt = abilities.length; i < cnt; ++i) {
Ability@ abl = abilities[i];
if(abl.disabled)
continue;
if(selected.isArtifact)
@abl.emp = playerEmpire;
else
@abl.emp = selected.owner;
string option = format(locale::ABILITY_TRIGGER, abl.type.name);
string costs = abl.formatCosts(targs);
if(costs.length != 0)
option += " ("+costs+")";
if(abl.cooldown > 0)
option += format(locale::ABILITY_CD, formatTime(abl.cooldown));
else if(abl.type.cooldown > 0)
option += format(locale::ABILITY_COOLDOWN, formatTime(abl.type.cooldown));
if(abl.type.targets.length == 0) {
if(selected !is clicked)
continue;
addOption(menu, selected, clicked, option,
TriggerAbility(abl.id), abl.type.icon);
}
else if(abl.type.targets[0].type == TT_Object) {
if(selected is clicked)
continue;
if(!abl.canActivate(targs, ignoreCost=true))
continue;
TargetAbility opt(abl.id, clicked, abl.getRange(targs), abl.type);
if(!abl.canActivate(targs))
opt.bg = Color(0xff900040);
addOption(menu, selected, clicked, option, opt, abl.type.icon);
}
}
}
//Constructions
if(constructObj !is null && clicked !is null) {
Targets@ targs;
for(uint i = 0, cnt = getConstructionTypeCount(); i < cnt; ++i) {
auto@ cons = getConstructionType(i);
if(!cons.inContext)
continue;
if(cons.targets.length == 0)
continue;
if(cons.targets[0].type != TT_Object)
continue;
if(targs is null) {
@targs = Targets();
@targs.add(TT_Object, true).obj = clicked;
}
if(!cons.canBuild(constructObj, targs, ignoreCost=true)) {
continue;
}
bool haveCost = cons.canBuild(constructObj, targs, ignoreCost=false);
ConstructionOption opt(cons, constructObj, targs);
if(!haveCost)
opt.bg = Color(0xff000030);
string costs = cons.formatCosts(constructObj, targs);
string optText;
if(costs.length != 0)
optText = format("$1: $2 ($3)", constructObj.name, cons.name, costs);
else
optText = format("$1: $2", constructObj.name, cons.name);
addOption(menu, selected, clicked, optText, opt);
}
}
//Only show the menu if there are options
if(menu.list.itemCount == 0) {
menu.remove();
return false;
}
else {
menu.updateAbsolutePosition();
return true;
}
}
// {{{ Shortcut helpers for context menu
class Option : GuiContextOption {
Object@ clicked;
Object@ selected;
void call() {
}
void call(GuiContextMenu@ menu) {
call();
}
};
class SelectionOption : Option {
void call(Object@ selected) {
}
void call(GuiContextMenu@ menu) {
if(selectedObject is null) {
call(selected);
return;
}
array<Object@>@ selection = selectedObjects;
for(uint i = 0, cnt = selection.length; i < cnt; ++i)
call(selection[i]);
}
};
class MultiOption : Option {
void call(Object@ selected) {
}
void call(GuiContextMenu@ menu) {
if(selectedObject is null) {
if(selected !is null)
call(selected);
if(clicked !is null && selected !is clicked)
call(clicked);
return;
}
array<Object@>@ selection = selectedObjects;
bool haveClicked = false;
for(uint i = 0, cnt = selection.length; i < cnt; ++i) {
if(selection[i] is clicked)
haveClicked = true;
call(selection[i]);
}
if(!haveClicked && clicked !is null)
call(clicked);
}
};
class SingleSelectionOption : Option {
void call(Object@ selected) {
}
void call(GuiContextMenu@ menu) {
if(selectedObject is null) {
call(selected);
return;
}
call(selectedObject);
}
};
void addOption(GuiContextMenu@ menu, Object@ selected, Object@ clicked, const string& text, Option@ opt, int value = 0) {
@opt.clicked = clicked;
@opt.selected = selected;
opt.value = value;
opt.text = text;
menu.addOption(opt);
}
void addOption(GuiContextMenu@ menu, Object@ selected, Object@ clicked, const string& text, Option@ opt, const Sprite& sprt, int value = 0) {
@opt.clicked = clicked;
@opt.selected = selected;
opt.value = value;
opt.text = text;
opt.icon = sprt;
menu.addOption(opt);
}
// }}}