diff --git a/OpenSR/data/components/leader_ai.txt b/OpenSR/data/components/leader_ai.txt index bbe143c..4168935 100644 --- a/OpenSR/data/components/leader_ai.txt +++ b/OpenSR/data/components/leader_ai.txt @@ -44,6 +44,7 @@ restricted local: safe bool get_autoBuySupports() const; safe bool get_autoFillSupports() const; safe bool get_allowFillFrom() const; + safe bool get_autoBuildSupports() const; restricted remote: void addGotoOrder(Object& target, bool append = false); @@ -94,6 +95,7 @@ restricted remote: void set_autoBuySupports(bool value); void set_autoFillSupports(bool value); void set_allowFillFrom(bool value); + void set_autoBuildSupports(bool value); void addExperience(double amount); void resetLevelExperience(bool resetLevels = false); diff --git a/OpenSR/locales/english/CP_gui.txt b/OpenSR/locales/english/CP_gui.txt index cdb4a14..d1041c2 100644 --- a/OpenSR/locales/english/CP_gui.txt +++ b/OpenSR/locales/english/CP_gui.txt @@ -3,4 +3,8 @@ STATUS_SHIPLEVEL_DESC: << The crew of this flagship consists of seasoned veterans, increasing its health and combat effectiveness by 20%, its support command by 10%, and its shield capacity by 30% for every veterancy level. Destroy enemy flagships or use a simulator to train your crew to increase a ship's veterancy level. ->> \ No newline at end of file +>> +AUTO_LOCAL_DEFENSE_GEN: Auto Build +TT_AUTO_LOCAL_DEFENSE_GEN: << + When checked, the fleet will automatically build new support ships if it has local defense generation until the fleet is full. +>> diff --git a/OpenSR/scripts/definitions/generic_effects.as b/OpenSR/scripts/definitions/generic_effects.as index caa9a63..ca01be3 100644 --- a/OpenSR/scripts/definitions/generic_effects.as +++ b/OpenSR/scripts/definitions/generic_effects.as @@ -3629,6 +3629,11 @@ class AddLocalDefense : GenericEffect { if(disable_in_combat.boolean && obj.inCombat) tickDefense = 0; + if (obj.hasLeaderAI && !obj.autoBuildSupports) { + // Don't build if user said not to + tickDefense = 0; + } + // Loop while have defense for this tick to build with // Fixes vanilla issue blowing all labor on the first support // even when labor would be leftover @@ -3772,6 +3777,11 @@ class AddLocalDefenseAdjacentFlags : GenericEffect { if(disable_in_combat.boolean && obj.inCombat) tickDefense = 0; + if (obj.hasLeaderAI && !obj.autoBuildSupports) { + // Don't build if user said not to + tickDefense = 0; + } + if(dat.design is null) { @dat.design = getDefenseDesign(obj.owner, secondDefense); if(dat.design !is null) diff --git a/OpenSR/scripts/gui/overlays/Supports.as b/OpenSR/scripts/gui/overlays/Supports.as new file mode 100644 index 0000000..4a37a5e --- /dev/null +++ b/OpenSR/scripts/gui/overlays/Supports.as @@ -0,0 +1,1016 @@ +import dialogs.InputDialog; +import elements.BaseGuiElement; +import elements.GuiPanel; +import elements.GuiText; +import elements.GuiButton; +import elements.GuiSprite; +import elements.GuiCheckbox; +import elements.GuiSkinElement; +import elements.GuiContextMenu; +import elements.MarkupTooltip; +import resources, ship_groups; +import elements.Gui3DObject; +#include "dialogs/include/UniqueDialogs.as" +import obj_selection; +import icons; +from gui import animate_time; +from targeting.targeting import cancelTargeting; +from targeting.MoveTarget import isExtendedMoveTarget; + +const int GROUP_WIDTH = 256; +const int GROUP_SPACING = 32; + +const int SUPPORT_PADDING = 12; +const int SUPPORT_HEIGHT = 56; +const int SUPPORT_WIDTH = GROUP_WIDTH - SUPPORT_PADDING * 2; +const int SUPPORT_SPACING = 8; +const int BAR_WIDTH = SUPPORT_WIDTH + SUPPORT_PADDING * 2 + 20; +const vec2i addButtonSize = vec2i(180,32); + +const double ANIM_TIME = 0.15; + +from tabs.GalaxyTab import GalaxyOverlay; +GalaxyOverlay@ createSupportOverlay(IGuiElement@ tab, Object@ obj, Object@ to, bool animate = true) { + return SupportOverlay(tab, obj, to, animate); +} + +class SupportOverlay : GalaxyOverlay, BaseGuiElement { + Object@ leader; + + GroupDisplay@ main; + GroupDisplay@ secondary; + + SupportClass@[] selected; + vec2i dragStart; + bool leftDown = false; + bool rightDown = false; + bool dragging = false; + bool closing = false; + + SupportClass@ clsHover; + GroupDisplay@ grpHover; + + SupportOverlay(IGuiElement@ parent, Object@ Leader, Object@ To, bool animate) { + @leader = Leader; + super(parent, Alignment(Left, Top, Right, Bottom)); + + if(To !is null) + makeSecondary(To); + + @main = GroupDisplay(leader, this, 255); + Alignment align(Left+4, Top+34, Left+4+BAR_WIDTH, Bottom); + if(animate) + main.animate(align); + else + @main.alignment = align; + + updateAbsolutePosition(); + bringToFront(); + setGuiFocus(this); + } + + void makeSecondary(Object@ obj) { + if(main !is null && obj is main.leader) + @obj = null; + if(secondary !is null) { + if(obj is null) { + secondary.remove(); + @secondary = null; + } + else { + secondary.set(obj); + } + } + else if(obj !is null) { + @secondary = GroupDisplay(obj, this); + @secondary.alignment = Alignment(Left+2+BAR_WIDTH, Top+94, Left+2+BAR_WIDTH*2, Bottom-220); + secondary.sendToBack(); + } + } + + GroupDisplay@ getHoveredGroup(vec2i absPos) { + if(main.absolutePosition.isWithin(absPos)) + return main; + if(secondary !is null && secondary.absolutePosition.isWithin(absPos)) + return secondary; + return null; + } + + SupportClass@ getHoveredClass(vec2i absPos) { + GroupDisplay@ grp = getHoveredGroup(absPos); + if(grp !is null) + return grp.getHoveredClass(absPos); + return null; + } + + bool isOpen() { + return parent !is null; + } + + bool objectInteraction(Object& object, uint mouseButton, bool doubleClicked) { + return false; + } + + void close() { + if(parent is null || closing) + return; + close(main); + } + + void close(GroupDisplay@ disp) { + if(disp is main) { + main.animateClose(); + closing = true; + } + else if(disp is secondary) { + secondary.remove(); + @secondary = null; + deselect(); + } + } + + IGuiElement@ elementFromPosition(const vec2i& pos) { + if(dragging) { + //Cannot access inner elements while dragging, + //we handle all the drag and drop stuff ourselves + if(AbsoluteClipRect.isWithin(pos)) + return this; + return null; + } + else { + return BaseGuiElement::elementFromPosition(pos); + } + } + + void updateHover() { + @grpHover = getHoveredGroup(mousePos); + if(grpHover !is null) + @clsHover = grpHover.getHoveredClass(mousePos); + else + @clsHover = null; + } + + void dropGroups() { + updateTimer = 0.1; + + Object@ transferTo; + if(grpHover !is null) + @transferTo = grpHover.leader; + else + @transferTo = hoveredObject; + if(transferTo !is null && transferTo.owner.controlled && transferTo.hasLeaderAI) { + for(uint i = 0, cnt = selected.length; i < cnt; ++i) { + SupportClass@ cls = selected[i]; + if(cls.disp.leader is transferTo || cls.disp is null) + continue; + + uint amt = cls.dat.totalSize - cls.leaveAmount; + cls.disp.leader.transferSupports(cls.dat.dsg, amt, transferTo); + } + } + + main.update(); + if(secondary !is null) + secondary.update(); + deselect(); + } + + void deselect() { + for(uint i = 0, cnt = selected.length; i < cnt; ++i) { + selected[i].selected = false; + selected[i].leaveAmount = 0; + } + selected.length = 0; + } + + void select(SupportClass@ cls) { + cls.selected = true; + cls.leaveAmount = 0; + if(selected.find(cls) == -1) + selected.insertLast(cls); + } + + void deselect(SupportClass@ cls) { + cls.selected = false; + cls.leaveAmount = 0; + selected.remove(cls); + } + + void pingUpdate() { + main.update(); + if(secondary !is null) + secondary.update(); + updateTimer = 0.15; + } + + double updateTimer = 0.0; + bool update(double time) { + if(closing) + return true; + if(selectedObject !is main.leader) { + if(selectedObject is null || !selectedObject.hasLeaderAI) + return false; + main.set(selectedObject); + if(secondary !is null) { + secondary.remove(); + @secondary = null; + } + } + + updateTimer -= time; + if(updateTimer <= 0) { + main.update(); + if(secondary !is null) { + secondary.update(); + secondary.visible = !main.animating; + } + updateTimer += 0.5; + } + + return true; + } + + bool onMouseEvent(const MouseEvent& event, IGuiElement@ source) { + if(event.type == MET_Button_Down) { + if(event.button == 0) { + dragging = false; + leftDown = true; + dragStart = mousePos; + + if(clsHover !is null) { + if(ctrlKey) { + if(clsHover.selected) + deselect(clsHover); + else + select(clsHover); + leftDown = false; + } + else { + if(!shiftKey) + deselect(); + select(clsHover); + } + } + else { + deselect(); + } + } + else if(event.button == 1) { + if(!leftDown) + dragStart = mousePos; + rightDown = true; + return false; + } + } + else if(event.type == MET_Moved) { + updateHover(); + if(!dragging && leftDown && mousePos.distanceTo(dragStart) > 3 && selected.length != 0) { + bool selSats = false; + for(uint i = 0, cnt = selected.length; i < cnt; ++i) { + if(selected[i].dat.dsg.hasTag(ST_Satellite)) { + selSats = true; + break; + } + } + if(!selSats) { + dragging = true; + for(uint i = 0, cnt = selected.length; i < cnt; ++i) { + if(shiftKey) + selected[i].leaveAmount = selected[i].dat.totalSize - selected[i].dat.amount; + else + selected[i].leaveAmount = 0; + } + } + } + if(dragging) { + } + } + else if(event.type == MET_Scrolled) { + for(uint i = 0, cnt = selected.length; i < cnt; ++i) { + SupportClass@ cls = selected[i]; + + int y = event.y; + if(shiftKey) + y *= 10; + cls.leaveAmount = clamp(cls.leaveAmount - y, 0, cls.dat.totalSize); + } + return true; + } + else if(event.type == MET_Button_Up) { + if(event.button == 0) { + leftDown = false; + if(dragging) { + dropGroups(); + dragging = false; + return true; + } + } + else if(event.button == 1) { + rightDown = false; + if(mousePos.distanceTo(dragStart) < 5 && !isExtendedMoveTarget() && hoveredObject is null) { + close(main); + cancelTargeting(); + return true; + } + } + } + return BaseGuiElement::onMouseEvent(event, source); + } + + bool onKeyEvent(const KeyboardEvent& event, IGuiElement@ source) override { + switch(event.type) { + case KET_Key_Down: + if(event.key == KEY_ESC) + return true; + break; + case KET_Key_Up: + if(event.key == KEY_ESC) { + close(main); + return true; + } + break; + } + return BaseGuiElement::onKeyEvent(event, source); + } + + bool get_isRoot() const override { + return getHoveredGroup(mousePos) is null; + } + + void draw() override { + BaseGuiElement::draw(); + + if(dragging) { + vec2i dragDiff = mousePos - dragStart; + for(uint i = 0, cnt = selected.length; i < cnt; ++i) + selected[i].drawAt(selected[i].absolutePosition.topLeft + dragDiff, true); + } + } +}; + +class GroupDisplay : BaseGuiElement { + SupportOverlay@ overlay; + Object@ leader; + Alignment@ targPos; + bool animating = false; + bool closing = false; + + //Group leader display + GuiSkinElement@ capBG; + GuiText@ capText; + GuiText@ groupSize; + + //List of support groups + GuiPanel@ listPanel; + SupportClass@[] classes; + GroupData[] groupData; + + //Action buttons + GuiButton@ addButton; + GuiButton@ rebuyButton; + GuiButton@ clearButton; + GuiCheckbox@ autoBuy; + GuiCheckbox@ autoFill; + GuiCheckbox@ allowFillFrom; + GuiCheckbox@ autoBuild; + + GroupDisplay(Object@ obj, SupportOverlay@ Overlay, int offset = 0) { + @leader = obj; + @overlay = Overlay; + + super(overlay, recti()); + + @capBG = GuiSkinElement(this, Alignment(Left, Top, Right-22, Top+50), SS_FullTitle); + + @capText = GuiText(this, Alignment(Left+12, Top+8, Right-12, Top+24), locale::SUPPORT_CAPACITY); + capText.font = FT_Bold; + + @groupSize = GuiText(this, Alignment(Left+4, Top+20, Right-32, Top+48)); + groupSize.horizAlign = 1.0; + + @listPanel = GuiPanel(this, Alignment(Left+SUPPORT_PADDING, Top+55, Right-SUPPORT_PADDING, Bottom-offset-108)); + + @addButton = GuiButton(this, recti(0, 0, 180, 32), locale::ADD_SUPPORTS); + addButton.tooltip = locale::CREATE_SUPPORT_SHIPS; + addButton.buttonIcon = icons::Add; + + @rebuyButton = GuiButton(this, Alignment(Left+4, Bottom-offset-60, Left+0.5f-15, Bottom-offset-30), locale::REBUY_GHOSTS); + setMarkupTooltip(rebuyButton, locale::TT_REBUY_GHOSTS); + rebuyButton.color = colors::Money; + rebuyButton.setIcon(icons::Money); + @clearButton = GuiButton(this, Alignment(Left+0.5f-6, Bottom-offset-60, Right-25, Bottom-offset-30), locale::CLEAR_GHOSTS); + clearButton.color = colors::Red; + clearButton.setIcon(icons::Remove); + setMarkupTooltip(clearButton, locale::TT_CLEAR_GHOSTS); + + @autoFill = GuiCheckbox(this, Alignment(Left+8, Bottom-offset-29, Left+0.5f-15, Bottom-offset-3), locale::AUTO_FILL_SUPPORTS); + setMarkupTooltip(autoFill, locale::TT_AUTO_FILL_SUPPORTS); + @autoBuy = GuiCheckbox(this, Alignment(Left+0.5f-5+3, Bottom-offset-29, Right-25, Bottom-offset-3), locale::AUTO_BUY_SUPPORTS); + setMarkupTooltip(autoBuy, locale::TT_AUTO_BUY_SUPPORTS); + @allowFillFrom = GuiCheckbox(this, Alignment(Left+8, Bottom-offset-29, Left+0.5f-15, Bottom-offset-3), locale::ALLOW_FILL_FROM_SUPPORTS); + setMarkupTooltip(allowFillFrom, locale::TT_ALLOW_FILL_FROM_SUPPORTS); + + @autoBuild = GuiCheckbox(this, Alignment(Left+0.5f-5+3, Bottom-offset-2, Right-25, Bottom-offset+24), locale::AUTO_LOCAL_DEFENSE_GEN); + setMarkupTooltip(autoBuild, locale::TT_AUTO_LOCAL_DEFENSE_GEN); + + update(); + } + + void remove() { + @overlay = null; + BaseGuiElement::remove(); + } + + void set(Object@ obj) { + @leader = obj; + update(); + } + + SupportClass@ getHoveredClass(vec2i absPos) { + for(uint i = 0, cnt = classes.length; i < cnt; ++i) { + if(classes[i].absolutePosition.isWithin(absPos)) + return classes[i]; + } + return null; + } + + void update() { + groupData.syncFrom(leader.getSupportGroups()); + + //Remove old buttons + uint newCnt = groupData.length; + uint oldCnt = classes.length; + + for(uint i = newCnt; i < oldCnt; ++i) { + classes[i].remove(); + @classes[i] = null; + } + + //Update current buttons + classes.length = newCnt; + int y = SUPPORT_SPACING; + for(uint i = 0; i < newCnt; ++i) { + if(classes[i] is null) + @classes[i] = SupportClass(this); + + SupportClass@ cls = classes[i]; + cls.set(groupData[i]); + cls.position = vec2i(0, y); + y += SUPPORT_HEIGHT + SUPPORT_SPACING; + } + + //Update action button position + if(y + 36 > listPanel.size.height) + y = listPanel.rect.botRight.y + 14; + else + y += 55; + addButton.position = vec2i((GROUP_WIDTH - addButton.size.width) / 2, y); + + //Update group size + uint size = 0; + Ship@ leaderShip = cast(leader); + if(leaderShip !is null) + size = leaderShip.blueprint.design.size; + + int supUsed = leader.SupplyUsed; + int supCap = leader.SupplyCapacity; + groupSize.text = toString(supUsed) + " / " + + toString(supCap); + if(supUsed >= supCap) + capBG.color = colors::Red; + else if(float(supUsed) >= float(supCap) * 0.9f) + capBG.color = Color(0xff8000ff); + else + capBG.color = colors::White; + addButton.disabled = supUsed >= supCap; + + //Update controls + autoFill.visible = !leader.isPlanet && !leader.isOrbital; + allowFillFrom.visible = !autoFill.visible; + autoBuy.visible = true; + rebuyButton.visible = !leader.isPlanet; + clearButton.visible = !leader.isPlanet; + autoBuy.checked = leader.autoBuySupports; + autoBuild.visible = true; + autoBuild.checked = leader.autoBuildSupports; + + if(autoFill.visible) { + autoFill.checked = leader.autoFillSupports; + + int cost = 0; + for(uint i = 0; i < newCnt; ++i) { + auto@ dat = groupData[i]; + if(dat.ghost > 0) + cost += getBuildCost(dat.dsg) * dat.ghost; + } + + rebuyButton.disabled = cost == 0 || !playerEmpire.canPay(cost); + clearButton.disabled = cost == 0; + rebuyButton.text = format(locale::REBUY_GHOSTS, formatMoney(cost)); + } + else { + allowFillFrom.checked = leader.allowFillFrom; + } + } + + void draw() { + uint flags = SF_Normal; + recti pos = AbsolutePosition; + if(!listPanel.vert.visible) + pos.botRight.x -= 20; + skin.draw(SS_GroupPanel, flags, pos); + BaseGuiElement::draw(); + } + + void animate(Alignment@ pos) { + @targPos = pos; + @alignment = null; + recti endPos = targPos.resolve(parent.size); + rect = endPos + vec2i(0, endPos.height); + animate_time(this, endPos, ANIM_TIME); + animating = true; + if(overlay.secondary !is null) + overlay.secondary.visible = false; + } + + void animateClose() { + if(parent is null || overlay is null) + return; + @alignment = null; + recti endPos = rect + vec2i(0, rect.height); + animate_time(this, endPos, ANIM_TIME); + animating = true; + closing = true; + if(overlay.secondary !is null) + overlay.secondary.visible = false; + } + + bool onMouseEvent(const MouseEvent& event, IGuiElement@ source) override { + if(event.type == MET_Button_Down) { + if(event.button == 1) { + return true; + } + } + else if(event.type == MET_Button_Up) { + if(event.button == 1) { + overlay.close(this); + return true; + } + } + return BaseGuiElement::onMouseEvent(event, source); + } + + bool onGuiEvent(const GuiEvent& evt) { + if(evt.type == GUI_Clicked) { + if(evt.caller is addButton) { + CreateSupportDialog dlg(overlay, leader, null, 10); + addDialog(D_SupportAmount, dlg); + return true; + } + if(evt.caller is clearButton) { + leader.clearAllGhosts(); + overlay.pingUpdate(); + return true; + } + if(evt.caller is rebuyButton) { + leader.rebuildAllGhosts(); + overlay.pingUpdate(); + return true; + } + } + else if(evt.type == GUI_Changed) { + if(evt.caller is autoFill) { + leader.autoFillSupports = autoFill.checked; + return true; + } + if(evt.caller is autoBuy) { + leader.autoBuySupports = autoBuy.checked; + return true; + } + if(evt.caller is allowFillFrom) { + leader.allowFillFrom = allowFillFrom.checked; + return true; + } + if(evt.caller is autoBuild) { + leader.autoBuildSupports = autoBuild.checked; + return true; + } + } + else if(evt.type == GUI_Animation_Complete) { + animating = false; + if(closing) { + if(overlay !is null) + overlay.remove(); + } + else { + @alignment = targPos; + if(overlay.secondary !is null) + overlay.secondary.visible = true; + } + return true; + } + return BaseGuiElement::onGuiEvent(evt); + } +}; + +class SupportClass : BaseGuiElement { + GroupDisplay@ disp; + GroupData@ dat; + bool selected = false; + uint leaveAmount = 0; + + GuiButton@ addButton; + GuiButton@ removeButton; + + SupportClass(GroupDisplay@ d) { + @disp = d; + super(disp.listPanel, recti(0, 0, SUPPORT_WIDTH, SUPPORT_HEIGHT)); + + @addButton = GuiButton(this, Alignment(Right-40, Top, Right, Top+0.5f)); + addButton.tooltip = locale::CREATE_SUPPORT_SHIPS; + addButton.color = colors::Green; + addButton.visible = false; + addButton.setIcon(icons::Add); + + @removeButton = GuiButton(this, Alignment(Right-40, Top+0.5f, Right, Bottom)); + removeButton.tooltip = locale::SCUTTLE_SUPPORT_SHIPS; + removeButton.color = colors::Red; + removeButton.setIcon(icons::Minus); + removeButton.visible = false; + } + + void remove() { + @disp = null; + BaseGuiElement::remove(); + } + + void set(GroupData@ data) { + @dat = data; + } + + void drawAt(const vec2i& absPos, bool dragging) { + recti pos = recti_area(absPos, size); + + uint flags = SF_Normal; + if(disp !is null && disp.overlay.clsHover is this) + flags |= SF_Hovered; + if(selected) + flags |= SF_Active; + + uint tot = dat.totalSize; + uint amount = dat.amount; + uint ghost = dat.ghost; + uint ordered = dat.ordered; + + if(dragging) { + tot = dat.totalSize - leaveAmount; + + //Calculate proportions to take + uint lv = leaveAmount; + uint take = min(ordered, lv); + lv -= take; + ordered -= take; + + take = min(ghost, lv); + lv -= take; + ghost -= take; + + take = min(amount, lv); + lv -= take; + amount -= take; + } + else if(disp.overlay.dragging && selected) { + tot = leaveAmount; + + //Calculate proportions to leave + uint lv = leaveAmount; + uint take = min(ghost, lv); + lv -= take; + ghost = take; + + take = min(ordered, lv); + lv -= take; + ordered = take; + + take = min(amount, lv); + lv -= take; + amount= take; + } + + skin.draw(SS_PatternBox, flags, pos, dat.dsg.color); + if(disp !is null && disp.overlay.clsHover is this && !dragging) + skin.draw(SS_SubtleGlow, SF_Normal, pos, dat.dsg.color); + + Color col; + col = dat.dsg.color; + col.a = 0x80; + dat.dsg.icon.draw(recti_area(pos.topLeft+vec2i(4,0), vec2i(pos.height, pos.height)), col); + + const Font@ normal = skin.getFont(FT_Normal); + normal.draw(pos=recti_area(pos.topLeft + vec2i(pos.height+6, 6), vec2i(pos.width-pos.height-12, 22)), text=formatShipName(dat.dsg), stroke=colors::Black); + + const Font@ bold = skin.getFont(FT_Bold); + bold.draw(pos=recti_area(pos.topLeft + vec2i(pos.height+12, 28), vec2i(60, 22)), text=toString(amount)+"x", stroke=colors::Black); + + if(ordered > 0) + normal.draw(pos=recti_area(pos.topLeft + vec2i(pos.height+82, 28), vec2i(60, 22)), text="(+"+toString(ordered)+"x)", stroke=colors::Black, color=Color(0x80ff80ff)); + if(ghost > 0) + normal.draw(pos=recti_area(pos.topLeft + vec2i(pos.height+132, 28), vec2i(60, 22)), text="(-"+toString(ghost)+"x)", stroke=colors::Black, color=Color(0xff8080ff)); + } + + bool onGuiEvent(const GuiEvent& evt) { + if(evt.type == GUI_Clicked) { + if(evt.caller is addButton) { + + GuiContextMenu menu(mousePos); + + uint canBuild = uint(double(disp.leader.SupplyAvailable) / dat.dsg.size); + uint max = canBuild + dat.ghost; + if(max > 0) + menu.addOption(OrderSupports(disp.leader, dat.dsg, max)); + if(dat.ghost > 0 && dat.ghost < max) + menu.addOption(OrderSupports(disp.leader, dat.dsg, dat.ghost)); + uint maxMoney = floor(double(playerEmpire.RemainingBudget) / double(getBuildCost(dat.dsg))); + if(maxMoney < max && maxMoney != dat.ghost) + menu.addOption(OrderSupports(disp.leader, dat.dsg, maxMoney)); + if(max > 10 && maxMoney > 10) + menu.addOption(OrderSupports(disp.leader, dat.dsg, 10)); + if(max > 1 && maxMoney > 1) + menu.addOption(OrderSupports(disp.leader, dat.dsg, 1)); + + uint orderAmt = 10; + if(dat.ghost != 0) + orderAmt = dat.ghost; + if(orderAmt > max) + orderAmt = max; + menu.addOption(CustomSupportOrder(disp.overlay, disp.leader, dat.dsg, orderAmt)); + menu.finalize(); + + return true; + } + else if(evt.caller is removeButton) { + InputDialog@ dialog = InputDialog(ScuttleSupports(this), disp.overlay); + dialog.addTitle(locale::SCUTTLE_SUPPORT_SHIPS); + dialog.accept.text = locale::REMOVE; + + uint defAmount = 0; + if(dat.ghost > 0) + defAmount = dat.ghost; + if(dat.ordered > 0) + defAmount = dat.ordered; + dialog.addSpinboxInput(locale::AMOUNT, defAmount, 10.0, 1.0, dat.totalSize, 0); + + addDialog(D_SupportAmount, dialog); + dialog.focusInput(); + } + } + return BaseGuiElement::onGuiEvent(evt); + } + + void draw() { + drawAt(AbsolutePosition.topLeft, false); + + bool hover = disp !is null && !disp.overlay.dragging && disp.overlay.clsHover is this; + addButton.visible = hover && (disp.leader.SupplyAvailable >= uint(dat.dsg.size) || dat.ghost > 0); + removeButton.visible = hover; + + BaseGuiElement::draw(); + } +}; + +class OrderSupports : GuiContextOption { + Object@ forObject; + const Design@ dsg; + uint amount; + int build = 0; + int maintain = 0; + + OrderSupports(Object@ forObject, const Design@ dsg, uint amount) { + double labor = 0; + getBuildCost(dsg, this.build, maintain, labor, amount); + + this.amount = amount; + @this.forObject = forObject; + @this.dsg = dsg; + + string text = format(locale::ORDER_SUPPORT_COUNT, toString(amount), dsg.name); + text += " ("+formatMoney(this.build, maintain)+")"; + super(text); + } + + void call(GuiContextMenu@ menu) { + if(dsg !is null && amount > 0) + forObject.orderSupports(dsg, amount); + } + + void draw(GuiListbox@ ele, uint flags, const recti& absPos) override { + if(!playerEmpire.canPay(this.build)) + drawRectangle(absPos, Color(0xff900040)); + GuiContextOption::draw(ele, flags, absPos); + } +}; + +class CustomSupportOrder : GuiContextOption { + SupportOverlay@ overlay; + Object@ forObject; + const Design@ dsg; + uint amount; + + CustomSupportOrder(SupportOverlay@ overlay, Object@ forObject, const Design@ dsg, uint amount) { + this.amount = amount; + @this.forObject = forObject; + @this.dsg = dsg; + @this.overlay = overlay; + + super(locale::ORDER_SUPPORT_CUSTOM); + } + + void call(GuiContextMenu@ menu) { + CreateSupportDialog dlg(overlay, forObject, dsg, amount); + addDialog(D_SupportAmount, dlg); + } +}; + +enum Dialogs { + D_SupportAmount, +}; + +class ScuttleSupports : InputDialogCallback { + Object@ at; + const Design@ dsg; + + ScuttleSupports(SupportClass@ cls) { + @at = cls.disp.leader; + @dsg = cls.dat.dsg; + } + + void inputCallback(InputDialog@ dialog, bool accepted) { + if(accepted) { + double amt = dialog.getSpinboxInput(0); + at.scuttleSupports(dsg, round(amt)); + } + } +}; + +class CreateSupportDialog : Dialog { + array designs; + const Design@ dsg; + Object@ forObject; + + GuiText@ designLabel; + GuiListbox@ designList; + + GuiText@ amountLabel; + GuiSpinbox@ amountBox; + + GuiText@ costLabel; + GuiText@ costText; + + GuiButton@ accept; + GuiButton@ cancel; + + CreateSupportDialog(SupportOverlay@ overlay, Object@ obj, const Design@ design = null, int defaultNum = 10) { + @dsg = design; + @forObject = obj; + super(overlay); + + addTitle(locale::ADD_SUPPORTS); + width = 700; + + int y = 32; + + //Show list of designs + if(dsg is null) { + @designLabel = GuiText(bg, recti(12, y, width / 3 - 6, y+22), locale::ORDER_SUPPORT_DESIGN); + designLabel.font = FT_Bold; + @designList = GuiListbox(bg, recti(width / 3 + 6, y, width - 12, y+222)); + designList.required = true; + designList.itemHeight = 40; + designList.tabIndex = 0; + designList.style = SS_PlainBox; + + { + ReadLock lck(playerEmpire.designMutex); + uint cnt = playerEmpire.designCount; + designs.reserve(cnt); + designs.length = 0; + for(uint i = 0; i < cnt; ++i) { + const Design@ other = playerEmpire.designs[i]; + if(other.obsolete || other.newest() !is other) + continue; + if(other.hasTag(ST_Support) || (obj.canHaveSatellites && other.hasTag(ST_Satellite))) { + designList.addItem(GuiListText(formatShipName(other), other.icon * other.color)); + designs.insertLast(other); + } + } + } + + y += 232; + height += 232; + } + + //Show spinbox + @amountLabel = GuiText(bg, recti(12, y, width / 3 - 6, y+22), locale::ORDER_SUPPORT_AMOUNT); + amountLabel.font = FT_Bold; + @amountBox = GuiSpinbox(bg, recti(width / 3 + 6, y, width - 12, y+22), double(defaultNum)); + amountBox.tabIndex = 1; + amountBox.min = 0; + amountBox.decimals = 0; + amountBox.step = 10.0; + + y += 32; + height += 28; + + //Show cost + @costLabel = GuiText(bg, recti(12, y, width / 3 - 6, y+22), locale::ORDER_SUPPORT_COST); + costLabel.font = FT_Bold; + @costText = GuiText(bg, recti(width / 3 + 6, y, width - 12, y+22)); + costText.font = FT_Medium; + + height += 32; + + @accept = GuiButton(bg, recti()); + accept.text = locale::BUILD; + accept.tabIndex = 2; + @accept.callback = this; + + @cancel = GuiButton(bg, recti()); + cancel.text = locale::CANCEL; + cancel.tabIndex = 3; + @cancel.callback = this; + + alignAcceptButtons(accept, cancel); + updateCost(); + } + + const Design@ get_currentDesign() { + if(dsg is null && designList.selected != -1) + return designs[designList.selected]; + return dsg; + } + + void updateCost() { + const Design@ cur = currentDesign; + if(cur !is null) { + double canBuild = double(forObject.SupplyAvailable) / cur.size; + canBuild += forObject.getGhostCount(cur); + amountBox.maximum = floor(canBuild); + + int build = 0, maintain = 0; + double time = 0.0; + getBuildCost(cur, build, maintain, time, amountBox.value); + + costText.text = formatMoney(build)+" / "+formatMoney(maintain); + if(playerEmpire.RemainingBudget >= build) { + costText.color = Color(0xffffffff); + accept.disabled = false; + } + else if(playerEmpire.canPay(build)) { + costText.color = Color(0xfdff00ff); + accept.disabled = false; + } + else { + costText.color = Color(0xff0000ff); + accept.disabled = true; + } + } + else + costText.text = ""; + } + + void close() { + close(false); + } + + void close(bool accepted) { + if(accepted) { + @dsg = currentDesign; + int amount = ceil(amountBox.value); + if(dsg !is null && amount > 0) + forObject.orderSupports(dsg, amount); + } + Dialog::close(); + } + + void confirmDialog() { + close(true); + } + + bool onGuiEvent(const GuiEvent& event) { + if(Closed) + return false; + if(event.type == GUI_Clicked && (event.caller is accept || event.caller is cancel)) { + close(event.caller is accept); + return true; + } + else if(event.type == GUI_Changed && (event.caller is amountBox || event.caller is designList)) { + updateCost(); + return true; + } + else if(event.type == GUI_Confirmed) { + close(true); + return true; + } + return Dialog::onGuiEvent(event); + } +}; diff --git a/OpenSR/scripts/server/orders/LeaderAI.as b/OpenSR/scripts/server/orders/LeaderAI.as index ade04d6..04d160e 100644 --- a/OpenSR/scripts/server/orders/LeaderAI.as +++ b/OpenSR/scripts/server/orders/LeaderAI.as @@ -96,6 +96,10 @@ tidy class LeaderAI : Component_LeaderAI, Savable { bool rememberGhosts = true; //Whether fleets can autofill from this bool AllowFillFrom = false; + // Whether to automatically build new supports with local defense generation + // until full + // TODO: Default to false for sandbox + bool autoBuild = true; AutoState autoState = AS_None; vec3d initialPosition; @@ -277,6 +281,8 @@ tidy class LeaderAI : Component_LeaderAI, Savable { msg >> FreeRaiding; msg >> RaidRange; } + + msg >> autoBuild; } double get_GhostHP() const { @@ -343,6 +349,8 @@ tidy class LeaderAI : Component_LeaderAI, Savable { msg << FreeRaiding; msg << RaidRange; + + msg << autoBuild; } float getFleetEffectiveness() const { @@ -1022,6 +1030,15 @@ tidy class LeaderAI : Component_LeaderAI, Savable { orderDelta = true; } + bool get_autoBuildSupports() { + return autoBuild; + } + + void set_autoBuildSupports(bool value) { + autoBuild = value; + orderDelta = true; + } + bool get_autoFillSupports() { return autoFill; } @@ -2572,6 +2589,7 @@ tidy class LeaderAI : Component_LeaderAI, Savable { msg.writeSmall(uint(engageType)); msg.writeSmall(uint(engageBehave)); msg << autoFill << autoBuy << AllowFillFrom; + msg << autoBuild; } void writeLeaderData(Message& msg) { diff --git a/OpenSR/scripts/shadow/orders/LeaderAI.as b/OpenSR/scripts/shadow/orders/LeaderAI.as new file mode 100644 index 0000000..9dbe02b --- /dev/null +++ b/OpenSR/scripts/shadow/orders/LeaderAI.as @@ -0,0 +1,622 @@ +import ship_groups; +import orders; +import resources; + +//Factor of new design cost as minimum for retrofit +const double RETROFIT_MIN_PCT = 0.3; + +tidy class LeaderAI : Component_LeaderAI { + OrderDesc[] orders; + Object@[] supports; + GroupData@[] groupData; + + uint supplyCapacity = 0; + uint supplyUsed = 0; + double ghostHP = 0.0; + double ghostDPS = 0.0; + double orderedHP = 0.0; + double orderedDPS = 0.0; + double fleetHP = 0.0; + double fleetDPS = 0.0; + double fleetMaxHP = 0.0; + double fleetMaxDPS = 0.0; + double bonusDPS = 0.0; + float fleetEffectiveness = 1.f; + float permanentEffectiveness = 0.f; + float needExperience = 0.f; + bool autoFill = false; + bool autoBuy = false; + bool AllowFillFrom = false; + bool allowSatellites = false; + bool autoBuild = true; + + AutoMode autoMode = AM_AreaBound; + EngagementBehaviour engageBehave = EB_CloseIn; + EngagementRange engageType = ER_SupportMin; + + FleetPlaneNode@ node; + + float getFleetEffectiveness() const { + return fleetEffectiveness * getBaseFleetEffectiveness(); + } + + float getBaseFleetEffectiveness() const { + if(permanentEffectiveness < 0) + return pow(0.5, -2.0 * double(permanentEffectiveness)); + return 1.0 + permanentEffectiveness; + } + + void setFleetEffectiveness(float value) { + fleetEffectiveness = value; + } + + uint getAutoMode() { + return uint(autoMode); + } + + uint getEngageBehave() { + return uint(engageBehave); + } + + uint getEngageType() { + return uint(engageType); + } + + void leaderInit(Object& obj) { + if(obj.isShip) { + double formationRad = getFormationRadius(obj); + @node = FleetPlaneNode(); + node.establish(obj, formationRad); + } + leaderChangeOwner(obj, null, obj.owner); + } + + void leaderDestroy(Object& obj) { + if(obj.owner !is null && obj.owner.valid) + obj.owner.unregisterFleet(obj); + if(node !is null) { + cast(node).markForDeletion(); + @node = null; + } + } + + void leaderTick(Object& obj, double time) { + //Set plane visibility + if(node !is null) { + node.visible = obj.isVisibleTo(playerEmpire); + if(obj.region !is null) + node.hintParentObject(obj.region); + } + } + + void leaderChangeOwner(Object& obj, Empire@ oldOwner, Empire@ newOwner) { + if(oldOwner !is null && oldOwner.valid) + oldOwner.unregisterFleet(obj); + if(newOwner !is null && newOwner.valid) + newOwner.registerFleet(obj); + } + + int getRetrofitCost(const Object& obj) const { + int cost = 0; + bool have = false; + const Ship@ ship = cast(obj); + if(ship !is null) { + const Design@ from = ship.blueprint.design; + if(from !is null) { + @from = from.mostUpdated(); + const Design@ to = from.newest().mostUpdated(); + if(from !is to && from.hasTag(ST_Support) == to.hasTag(ST_Support) && from.hasTag(ST_Satellite) == to.hasTag(ST_Satellite)) { + int fromCost = getBuildCost(from); + int toCost = getBuildCost(to); + cost += max(toCost - fromCost, int(ceil(toCost * RETROFIT_MIN_PCT))); + have = true; + } + } + } + + for(uint i = 0, cnt = groupData.length; i < cnt; ++i) { + GroupData@ dat = groupData[i]; + const Design@ from = dat.dsg; + if(from is null) + continue; + @from = from.mostUpdated(); + const Design@ to = from.newest().mostUpdated(); + if(from !is to && from.hasTag(ST_Support) == to.hasTag(ST_Support) && from.hasTag(ST_Satellite) == to.hasTag(ST_Satellite)) { + int fromCost = getBuildCost(from); + int toCost = getBuildCost(to); + cost += max(toCost - fromCost, int(ceil(toCost * RETROFIT_MIN_PCT))) * dat.amount; + have = true; + } + } + + if(!have) + return -1; + else + return cost; + } + + double getRetrofitLabor(const Object& obj) const { + double cost = 0; + bool have = false; + const Ship@ ship = cast(obj); + if(ship !is null) { + const Design@ from = ship.blueprint.design; + if(from !is null) { + @from = from.mostUpdated(); + const Design@ to = from.newest().mostUpdated(); + if(from !is to && from.hasTag(ST_Support) == to.hasTag(ST_Support) && from.hasTag(ST_Satellite) == to.hasTag(ST_Satellite)) { + double fromCost = getLaborCost(from); + double toCost = getLaborCost(to); + cost += max(toCost - fromCost, toCost * RETROFIT_MIN_PCT); + have = true; + } + } + } + + for(uint i = 0, cnt = groupData.length; i < cnt; ++i) { + GroupData@ dat = groupData[i]; + const Design@ from = dat.dsg; + if(from is null) + continue; + @from = from.mostUpdated(); + const Design@ to = from.newest().mostUpdated(); + if(from !is to && from.hasTag(ST_Support) == to.hasTag(ST_Support) && from.hasTag(ST_Satellite) == to.hasTag(ST_Satellite)) { + double fromCost = getLaborCost(from); + double toCost = getLaborCost(to); + cost += max(toCost - fromCost, toCost * RETROFIT_MIN_PCT) * dat.amount; + have = true; + } + } + + if(!have) + return -1; + else + return cost; + } + + + double get_GhostHP() const { + return ghostHP; + } + + double get_GhostDPS() const { + return ghostDPS; + } + + bool get_hasOrders() { + return orders.length != 0; + } + + bool hasOrder(uint type, bool checkQueued = false) { + if(orders.length == 0) + return false; + if(!checkQueued) + return orders[0].type == type; + for(int i = orders.length - 1; i >= 0; --i) { + if(orders[i].type == type) + return true; + } + return false; + } + + uint get_orderCount() { + return orders.length; + } + + string get_orderName(uint num) { + return "(null)"; //TODO + } + + uint get_orderType(uint num) { + if(num >= orders.length) + return 0; + return orders[num].type; + } + + bool get_orderHasMovement(uint num) { + if(num >= orders.length) + return false; + return orders[num].hasMovement; + } + + vec3d get_orderMoveDestination(uint num) { + if(num >= orders.length) + return vec3d(); + return orders[num].moveDestination; + } + + vec3d get_finalMoveDestination(const Object& obj) { + for(int i = orders.length - 1; i >= 0; --i) { + if(orders[i].hasMovement) + return orders[i].moveDestination; + } + return obj.position; + } + + void getSupportGroups() const { + for(uint i = 0, cnt = groupData.length; i < cnt; ++i) + yield(groupData[i]); + } + + double getFormationRadius(Object& obj) { + Planet@ pl = cast(obj); + if(pl !is null) + return pl.OrbitSize; + return obj.radius * 10.0 + 20.0; + } + + uint get_supportCount() { + return supports.length; + } + + Object@ get_supportShip(uint index) { + if(index >= supports.length) + return null; + auto@ supp = supports[index]; + if(!supp.valid || !supp.initialized) + return null; + return supp; + } + + uint get_SupplyUsed() const { + return supplyUsed; + } + + uint get_SupplyCapacity() const { + return supplyCapacity; + } + + uint get_SupplyAvailable() const { + return supplyCapacity - supplyUsed; + } + + void updateFleetStrength(Object& obj) { + double hp = 0.0, dps = 0.0, maxHP = 0.0, maxDPS = 0.0; + + if(obj.isShip) { + Ship@ ship = cast(obj); + auto@ bp = ship.blueprint; + + hp = bp.currentHP * bp.hpFactor + ship.Shield; + dps = ship.DPS * bp.shipEffectiveness; + + maxHP = bp.design.totalHP + ship.MaxShield; + maxDPS = ship.MaxDPS; + } + if(obj.isOrbital) { + Orbital@ orb = cast(obj); + hp = orb.health + orb.armor; + maxHP = orb.maxHealth + orb.maxArmor; + maxDPS = orb.dps; + dps = maxDPS * orb.efficiency; + } + + for(uint i = 0, cnt = supports.length; i < cnt; ++i) { + Ship@ ship = cast(supports[i]); + if(ship !is null) { + auto@ bp = ship.blueprint; + const Design@ dsg = bp.design; + if(dsg is null) + continue; + hp += bp.currentHP * bp.hpFactor + ship.Shield; + dps += ship.DPS * bp.shipEffectiveness; + maxHP += dsg.totalHP + ship.MaxShield; + maxDPS += ship.MaxDPS; + } + } + + fleetHP = hp; + fleetDPS = dps; + fleetMaxHP = maxHP; + fleetMaxDPS = maxDPS; + } + + void transferSupports(Object& obj, const Design@ ofDesign, uint amount, Object@ transferTo) { + if(!transferTo.hasLeaderAI || ofDesign is null || amount == 0) + return; + + int ind = getGroupDataIndex(ofDesign, false); + if(ind == -1) + return; + + //Don't try to transfer over our supply cap + amount = min(amount, transferTo.SupplyAvailable / uint(ofDesign.size)); + if(amount == 0) + return; + + ind = getGroupDataIndex(ofDesign, false); + if(ind == -1) + return; + GroupData@ dat = groupData[ind]; + + //Transfer real ships over first + uint take = min(dat.amount, amount); + if(take != 0) { + amount -= take; + dat.amount -= take; + supplyUsed -= take * ofDesign.size; + transferTo.addFakeSupports(ofDesign, take); + } + + if(dat.totalSize <= 0) + groupData.removeAt(ind); + + //Transfer ordered + if(amount == 0) + return; + + take = min(dat.ordered, amount); + if(take != 0) { + amount -= take; + dat.ordered -= take; + supplyUsed -= take * ofDesign.size; + transferTo.addSupportOrdered(ofDesign, take); + } + + if(dat.totalSize <= 0) + groupData.removeAt(ind); + + //Transfer ghosts + if(amount == 0) + return; + + take = min(dat.ghost, amount); + if(take != 0) { + dat.ghost -= take; + amount -= take; + supplyUsed -= take * ofDesign.size; + transferTo.addSupportGhosts(ofDesign, take); + } + + if(dat.totalSize <= 0) + groupData.removeAt(ind); + } + + void orderSupports(Object& obj, const Design@ ofDesign, uint amount) { + if(!obj.owner.canPay(getBuildCost(ofDesign, amount))) + return; + + int index = getGroupDataIndex(ofDesign, true); + groupData[index].ordered += amount; + } + + void addSupportGhosts(Object& obj, const Design@ ofDesign, uint amount) { + int ind = getGroupDataIndex(ofDesign, true); + + GroupData@ dat = groupData[ind]; + dat.ghost += amount; + supplyUsed += amount * ofDesign.size; + } + + void addSupportOrdered(Object& obj, const Design@ ofDesign, uint amount) { + int ind = getGroupDataIndex(ofDesign, true); + GroupData@ dat = groupData[ind]; + dat.ordered += amount; + supplyUsed += amount * ofDesign.size; + } + + void addFakeSupports(Object& obj, const Design@ ofDesign, uint amount) { + int ind = getGroupDataIndex(ofDesign, true); + GroupData@ dat = groupData[ind]; + dat.amount += amount; + supplyUsed += amount * ofDesign.size; + } + + double getRemainingExp() const { + return needExperience; + } + + double getFleetHP() const { + return fleetHP; + } + + double getFleetDPS() const { + return fleetDPS + bonusDPS; + } + + double getFleetStrength(const Object& obj) const { + return fleetHP * (fleetDPS + bonusDPS); + } + + double getFleetMaxStrength(const Object& obj) const { + return (fleetMaxHP + ghostHP + orderedHP) * (fleetMaxDPS + bonusDPS + ghostDPS + orderedDPS) * getBaseFleetEffectiveness(); + } + + bool get_canHaveSatellites() const { + return allowSatellites; + } + + int getGroupDataIndex(const Design@ dsg, bool create = false) { + @dsg = dsg.mostUpdated(); + for(uint i = 0, cnt = groupData.length; i < cnt; ++i) { + GroupData@ dat = groupData[i]; + const Design@ oldDesign = dat.dsg; + const Design@ newDesign = dat.dsg.mostUpdated(); + if(newDesign is dsg.mostUpdated()) { + if(oldDesign !is newDesign) + @dat.dsg = newDesign; + return i; + } + } + if(create) { + GroupData dat; + @dat.dsg = dsg.mostUpdated(); + + groupData.insertLast(dat); + return groupData.length - 1; + } + return -1; + } + + uint getGhostCount(const Design@ dsg) const { + int ind = getGroupDataIndex(dsg); + if(ind == -1) + return 0; + return groupData[ind].ghost; + } + + void readOrders(Message& msg) { + msg.readAlign(); + uint cnt = msg.read_uint(); + orders.length = cnt; + for(uint i = 0; i < cnt; ++i) + orders[i].read(msg); + + autoMode = AutoMode(msg.readSmall()); + engageType = EngagementRange(msg.readSmall()); + engageBehave = EngagementBehaviour(msg.readSmall()); + msg >> autoFill >> autoBuy >> AllowFillFrom; + msg >> autoBuild; + } + + bool get_autoBuySupports() { + return autoBuy; + } + + bool get_autoFillSupports() { + return autoFill; + } + + void set_autoBuySupports(bool value) { + autoBuy = value; + } + + void set_autoFillSupports(bool value) { + autoFill = value; + } + + bool get_autoBuildSupports() { + return autoBuild; + } + + bool get_allowFillFrom() { + return AllowFillFrom; + } + + void set_allowFillFrom(bool value) { + AllowFillFrom = value; + } + + void readLeaderData(Message& msg) { + uint cnt = msg.readSmall(); + groupData.length = cnt; + + double gHP = 0, gDPS = 0; + double oHP = 0, oDPS = 0; + for(uint i = 0; i < cnt; ++i) { + GroupData@ dat = groupData[i]; + if(dat is null) { + @dat = GroupData(); + @groupData[i] = dat; + } + msg >> dat; + + if(dat.dsg !is null) { + double dps = dat.dsg.total(SV_DPS); + + gHP += double(dat.ghost) * dat.dsg.totalHP; + gDPS += double(dat.ghost) * dps; + + oHP += double(dat.ordered) * dat.dsg.totalHP; + oDPS += double(dat.ordered) * dps; + } + } + + ghostHP = gHP; + ghostDPS = gDPS; + + orderedHP = oHP; + orderedDPS = oDPS; + + bool hadSupply = supplyCapacity > 0; + + supplyCapacity = msg.readSmall(); + supplyUsed = msg.readSmall(); + + if(msg.readBit()) + fleetEffectiveness = msg.readFixed(0.f, 50.f, 16); + else + fleetEffectiveness = 1.f; + + if(msg.readBit()) + permanentEffectiveness = msg.readFixed(0.f, 50.f, 16); + else + permanentEffectiveness = 0.f; + + if(msg.readBit()) + bonusDPS = msg.read_float(); + else + bonusDPS = 0.0; + + if(msg.readBit()) + needExperience = msg.read_float(); + else + needExperience = 0.0; + + if(node !is null) { + if(hadSupply != (supplyCapacity > 0)) + node.hasSupply = supplyCapacity > 0; + } + } + + void readGroup(Message& msg) { + bool hadSupports = supports.length > 0; + uint cnt = msg.readSmall(); + supports.length = cnt; + for(uint i = 0; i < cnt; ++i) + msg >> supports[i]; + + readLeaderData(msg); + + if(node !is null) { + if(hadSupports != (supports.length > 0)) + node.hasFleet = supports.length > 0; + } + } + + void readGroupDelta(Message& msg) { + bool hadSupports = supports.length > 0; + + //Added + if(msg.readBit()) { + uint cnt = msg.readSmall(); + for(uint i = 0; i < cnt; ++i) { + Object@ ship; + msg >> ship; + + supports.insertLast(ship); + } + } + + //Removed + if(msg.readBit()) { + uint cnt = msg.readSmall(); + for(uint i = 0; i < cnt; ++i) { + Object@ ship; + msg >> ship; + + supports.remove(ship); + } + } + + readLeaderData(msg); + + if(node !is null) { + if(hadSupports != (supports.length > 0)) + node.hasFleet = supports.length > 0; + } + } + + void readLeaderAI(Object& obj, Message& msg) { + readGroup(msg); + readOrders(msg); + msg >> allowSatellites; + } + + void readLeaderAIDelta(Message& msg) { + if(msg.readBit()) + readGroupDelta(msg); + if(msg.readBit()) + readOrders(msg); + } +};