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(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(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(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(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(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(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(clicked), shiftKey); playOptionSound(sound::order_pickup); } } }; class ScanAnomaly : SelectionOption { void call(Object@ selected) { if(selected.hasLeaderAI && clicked.isAnomaly) { selected.addScanOrder(cast(clicked), shiftKey); playOptionSound(sound::order_scan); } } }; class InvestigateAnomaly : Option { void call() { if(clicked.isAnomaly) { AnomalyOverlay(ActiveTab, cast(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(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(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 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(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(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(selected); if(orb is null) buildAt.buildDryDock(design, pct); else orb.sendValue(OV_DRY_Financed, pct); } } }; class WorkDryDock : Option { void call() { selected.workDryDock(cast(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(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(clicked); if(clicked.isStar) @clickedRegion = clicked.region; } Orbital@ orbitalMaster; if(selected !is null && selected.isOrbital) { Orbital@ selOrb = cast(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(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(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(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(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(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(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(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(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(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(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 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@ 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@ 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); } // }}}