package mindustry.ui.dialogs; import arc.*; import arc.assets.loaders.TextureLoader.*; import arc.func.*; import arc.graphics.*; import arc.graphics.Texture.*; import arc.graphics.g2d.*; import arc.graphics.gl.*; import arc.input.*; import arc.math.*; import arc.math.geom.*; import arc.scene.*; import arc.scene.event.*; import arc.scene.style.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; import mindustry.*; import mindustry.content.*; import mindustry.content.TechTree.*; import mindustry.core.*; import mindustry.ctype.*; import mindustry.game.EventType.*; import mindustry.game.Objectives.*; import mindustry.game.SectorInfo.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.graphics.g3d.PlanetGrid.*; import mindustry.graphics.g3d.*; import mindustry.input.*; import mindustry.io.*; import mindustry.maps.*; import mindustry.type.*; import mindustry.type.Planet.*; import mindustry.ui.*; import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.CoreBlock.*; import static arc.Core.*; import static mindustry.Vars.*; import static mindustry.graphics.g3d.PlanetRenderer.*; import static mindustry.ui.dialogs.PlanetDialog.Mode.*; public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ //if true, enables launching anywhere for testing public static boolean debugSelect = false, debugSectorAttackEdit; public static float sectorShowDuration = 60f * 2.4f; public final FrameBuffer buffer = new FrameBuffer(2, 2, true); public final LaunchLoadoutDialog loadouts = new LaunchLoadoutDialog(); public final PlanetRenderer planets = renderer.planets; public PlanetParams state = new PlanetParams(); public float zoom = 1f; public @Nullable Sector selected, hovered, launchSector; /** Must not be null in planet launch mode. */ public @Nullable Seq launchCandidates; public Mode mode = look; public boolean launching; public Cons listener = s -> {}; public Seq newPresets = new Seq<>(); public float presetShow = 0f; public boolean showed = false, sectorsShown; public String searchText = ""; public Table sectorTop = new Table(), notifs = new Table(), expandTable = new Table(); public Label hoverLabel = new Label(""); private Texture[] planetTextures; private Element mainView; private CampaignRulesDialog campaignRules = new CampaignRulesDialog(); private SectorSelectDialog selectDialog = new SectorSelectDialog(); public PlanetDialog(){ super("", Styles.fullDialog); state.renderer = this; state.drawUi = true; shouldPause = true; state.planet = content.getByName(ContentType.planet, Core.settings.getString("lastplanet", "serpulo")); if(state.planet == null) state.planet = Planets.serpulo; clampZoom(); addListener(new InputListener(){ @Override public boolean keyDown(InputEvent event, KeyCode key){ if(event.targetActor == PlanetDialog.this && (key == KeyCode.escape || key == KeyCode.back || key == Binding.planetMap.value.key)){ if(showing() && newPresets.size > 1){ //clear all except first, which is the last sector. newPresets.truncate(1); }else if(selected != null){ selectSector(null); }else{ Core.app.post(() -> hide()); } return true; } return false; } }); hoverLabel.setStyle(Styles.outlineLabel); hoverLabel.setAlignment(Align.center); rebuildButtons(); onResize(this::rebuildButtons); dragged((cx, cy) -> { //no multitouch drag if(Core.input.getTouches() > 1) return; if(showing() && newPresets.peek() != state.planet.getLastSector()) return; newPresets.clear(); Vec3 pos = state.camPos; float upV = pos.angle(Vec3.Y); float xscale = 9f, yscale = 10f; float margin = 1; //scale X speed depending on polar coordinate float speed = 1f - Math.abs(upV - 90) / 90f; pos.rotate(state.camUp, cx / xscale * speed); //prevent user from scrolling all the way up and glitching it out float amount = cy / yscale; amount = Mathf.clamp(upV + amount, margin, 180f - margin) - upV; pos.rotate(Tmp.v31.set(state.camUp).rotate(state.camDir, 90), amount); }); addListener(new InputListener(){ @Override public boolean scrolled(InputEvent event, float x, float y, float amountX, float amountY){ if(event.targetActor == PlanetDialog.this){ zoom = Mathf.clamp(zoom + amountY / 10f, state.planet.minZoom, state.planet.maxZoom); } return true; } }); addCaptureListener(new ElementGestureListener(){ float lastZoom = -1f; @Override public void zoom(InputEvent event, float initialDistance, float distance){ if(lastZoom < 0){ lastZoom = zoom; } zoom = (Mathf.clamp(initialDistance / distance * lastZoom, state.planet.minZoom, state.planet.maxZoom)); } @Override public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){ lastZoom = zoom; } @Override public void tap(InputEvent event, float x, float y, int count, KeyCode button){ var hovered = getHoverPlanet(x, y); if(hovered != null){ var hit = scene.hit(x, y, true); if(hit == mainView){ viewPlanet(hovered, false); } } } }); shown(this::setup); //show selection of Erekir/Serpulo campaign if the user has no bases, and hasn't selected yet (essentially a "have they played campaign before" check) shown(() -> { if(!settings.getBool("campaignselect") && !content.planets().contains(p -> p.sectors.contains(Sector::hasBase))){ var diag = new BaseDialog("@campaign.select"); Planet[] selected = {null}; var group = new ButtonGroup<>(); group.setMinCheckCount(0); state.planet = Planets.sun; Planet[] choices = {Planets.serpulo, Planets.erekir}; int i = 0; for(var planet : choices){ TextureRegion tex = new TextureRegion(planetTextures[i]); diag.cont.button(b -> { b.top(); b.add(planet.localizedName).color(Pal.accent).style(Styles.outlineLabel); b.row(); b.image(new TextureRegionDrawable(tex)).grow().scaling(Scaling.fit); }, Styles.togglet, () -> selected[0] = planet).size(mobile ? 220f : 320f).group(group); i ++; } diag.cont.row(); diag.cont.label(() -> selected[0] == null ? "@campaign.none" : "@campaign." + selected[0].name).labelAlign(Align.center).style(Styles.outlineLabel).width(440f).wrap().colspan(2); diag.buttons.button("@ok", Icon.ok, () -> { state.planet = selected[0]; lookAt(state.planet.getStartSector()); selectSector(state.planet.getStartSector()); settings.put("campaignselect", true); diag.hide(); }).size(300f, 64f).disabled(b -> selected[0] == null); app.post(diag::show); } }); planetTextures = new Texture[2]; String[] names = {"sprites/planets/serpulo.png", "sprites/planets/erekir.png"}; for(int i = 0; i < names.length; i++){ int fi = i; assets.load(names[i], Texture.class, new TextureParameter(){{ minFilter = magFilter = TextureFilter.linear; }}).loaded = t -> planetTextures[fi] = t; assets.finishLoadingAsset(names[i]); } //unlock defaults for older campaign saves (TODO move? where to?) if(content.planets().contains(p -> p.sectors.contains(Sector::hasBase)) || Blocks.scatter.unlocked() || Blocks.router.unlocked()){ Seq.with(Blocks.junction, Blocks.mechanicalDrill, Blocks.conveyor, Blocks.duo, Items.copper, Items.lead).each(UnlockableContent::quietUnlock); } } /** show with no limitations, just as a map. */ @Override public Dialog show(){ if(net.client()){ ui.showInfo("@map.multiplayer"); return this; } //view current planet by default if(Vars.state.rules.sector != null){ state.planet = Vars.state.rules.sector.planet; settings.put("lastplanet", state.planet.name); } if(!selectable(state.planet)){ state.planet = Planets.serpulo; } rebuildButtons(); mode = look; state.otherCamPos = null; selected = hovered = launchSector = null; launching = false; zoom = 1f; state.zoom = 1f; state.uiAlpha = 0f; launchSector = Vars.state.getSector(); presetShow = 0f; showed = false; listener = s -> {}; newPresets.clear(); //announce new presets for(SectorPreset preset : content.sectors()){ if(preset.unlocked() && !preset.alwaysUnlocked && !preset.sector.info.shown && preset.requireUnlock && !preset.sector.hasBase() && preset.planet == state.planet){ newPresets.add(preset.sector); preset.sector.info.shown = true; preset.sector.saveInfo(); } } if(newPresets.any()){ newPresets.add(state.planet.getLastSector()); } newPresets.reverse(); updateSelected(); if(state.planet.getLastSector() != null){ lookAt(state.planet.getLastSector()); } return super.show(); } void rebuildButtons(){ buttons.clearChildren(); buttons.bottom(); if(Core.graphics.isPortrait()){ buttons.add(sectorTop).colspan(2).fillX().row(); addBack(); addTech(); }else{ addBack(); buttons.add().growX(); buttons.add(sectorTop).minWidth(230f); buttons.add().growX(); addTech(); } } void addBack(){ buttons.button("@back", Icon.left, this::hide).size(200f, 54f).pad(2).bottom(); } void addTech(){ buttons.button("@techtree", Icon.tree, () -> ui.research.show()).size(200f, 54f).visible(() -> mode == look).pad(2).bottom(); } public void showOverview(){ //TODO implement later if necessary /* sectors.captured = Captured Sectors sectors.explored = Explored Sectors sectors.production.total = Total Production sectors.resources.total = Total Resources */ var dialog = new BaseDialog("@overview"); dialog.addCloseButton(); dialog.add("@sectors.captured"); } //TODO not fully implemented, cutscene needed public void showPlanetLaunch(Sector sector, Seq launchCandidates, Cons listener){ selected = null; hovered = null; launching = false; this.listener = listener; this.launchCandidates = (launchCandidates == null ? sector.planet.launchCandidates : launchCandidates); launchSector = sector; //automatically select next planets; if(this.launchCandidates.size == 1){ state.planet = this.launchCandidates.first(); state.otherCamPos = sector.planet.position; state.otherCamAlpha = 0f; //unlock and highlight sector var destSec = state.planet.sectors.get(state.planet.startSector); var preset = destSec.preset; if(preset != null){ preset.unlock(); } selected = destSec; } //TODO pan over to correct planet //update view to sector zoom = 1f; state.zoom = 1f; state.uiAlpha = 0f; mode = planetLaunch; updateSelected(); rebuildExpand(); if(sectorTop != null){ sectorTop.color.a = 0f; } super.show(); } public void showSelect(Sector sector, Cons listener){ selected = null; hovered = null; launching = false; this.listener = listener; //update view to sector lookAt(sector); zoom = 1f; state.zoom = 1f; state.uiAlpha = 0f; state.otherCamPos = null; launchSector = sector; mode = select; super.show(); } public void lookAt(Sector sector){ if(sector.tile == Ptile.empty) return; //TODO should this even set `state.planet`? the other lookAt() doesn't, so... state.planet = sector.planet; sector.planet.lookAt(sector, state.camPos); clampZoom(); } public void lookAt(Sector sector, float alpha){ float len = state.camPos.len(); state.camPos.slerp(sector.planet.lookAt(sector, Tmp.v33).setLength(len), alpha); } void clampZoom(){ zoom = Mathf.clamp(zoom, state.planet.minZoom, state.planet.maxZoom); } boolean canSelect(Sector sector){ if(mode == select) return sector.hasBase() && launchSector != null && sector.planet == launchSector.planet; if(mode == planetLaunch && sector.hasBase()){ return false; } if(sector.planet.generator == null) return false; if(sector.hasBase() || sector.id == sector.planet.startSector) return true; //preset sectors can only be selected once unlocked if(sector.preset != null && sector.preset.requireUnlock){ TechNode node = sector.preset.techNode; return sector.preset.unlocked() || node == null || node.parent == null || (node.parent.content.unlocked() && (!(node.parent.content instanceof SectorPreset preset) || preset.sector.hasBase())); } return mode == planetLaunch ? sector.planet.generator.allowAcceleratorLanding(sector) : sector.planet.generator.allowLanding(sector); } @Nullable Sector findLauncher(Sector to){ if(mode == planetLaunch || to.planet.generator == null) return launchSector; Sector candidate = to.planet.generator.findLaunchCandidate(to, launchSector); return candidate == null ? launchSector : candidate; } boolean showing(){ return newPresets.any(); } @Override public void renderSectors(Planet planet){ //draw all sector stuff if(state.uiAlpha > 0.01f){ for(Sector sec : planet.sectors){ if(canSelect(sec) || sec.unlocked() || debugSelect){ Color color = sec.hasBase() ? Tmp.c2.set(Team.sharded.color).lerp(Team.crux.color, sec.hasEnemyBase() ? 0.5f : 0f) : sec.preset != null && sec.preset.requireUnlock ? sec.preset.unlocked() ? Tmp.c2.set(Team.derelict.color).lerp(Color.white, Mathf.absin(Time.time, 10f, 1f)) : Color.gray : sec.hasEnemyBase() ? Team.crux.color : null; if(color != null){ planets.drawSelection(sec, Tmp.c1.set(color).mul(0.8f).a(state.uiAlpha), 0.026f, -0.001f); } }else{ planets.fill(sec, Tmp.c1.set(shadowColor).mul(1, 1, 1, state.uiAlpha), -0.001f); } } } Sector current = Vars.state.getSector() != null && Vars.state.getSector().isBeingPlayed() && Vars.state.getSector().planet == state.planet ? Vars.state.getSector() : null; if(current != null){ planets.fill(current, hoverColor.write(Tmp.c1).mulA(state.uiAlpha), -0.001f); } //draw hover border if(hovered != null){ planets.fill(hovered, hoverColor.write(Tmp.c1).mulA(state.uiAlpha), -0.001f); planets.drawBorders(hovered, borderColor, state.uiAlpha); } //draw selected borders if(selected != null){ planets.drawSelection(selected, state.uiAlpha); planets.drawBorders(selected, borderColor, state.uiAlpha); } planets.batch.flush(Gl.triangles); } @Override public void renderOverProjections(Planet planet){ Sector hoverOrSelect = hovered != null ? hovered : selected; Sector launchFrom = hoverOrSelect != null && !hoverOrSelect.hasBase() ? findLauncher(hoverOrSelect) : null; if(hoverOrSelect != null && !hoverOrSelect.hasBase() && launchFrom != null && hoverOrSelect != launchFrom && canSelect(hoverOrSelect)){ planets.drawArcLine(planet, launchFrom.tile.v, hoverOrSelect.tile.v); } if(mode == planetLaunch && launchSector != null && selected != null && hovered == null){ planets.drawArcLine(planet, launchSector.tile.v, selected.tile.v); } if(state.uiAlpha > 0.001f){ for(Sector sec : planet.sectors){ if(sec.hasBase()){ //draw vulnerable sector attack arc if(planet.campaignRules.sectorInvasion){ for(Sector enemy : sec.near()){ if(enemy.hasEnemyBase() && (enemy.preset == null || !enemy.preset.requireUnlock)){ planets.drawArcLine(planet, enemy.tile.v, sec.tile.v, Team.crux.color.write(Tmp.c2).a(state.uiAlpha), Color.clear, 0.24f, 110f, 25, 0.005f); } } } if(selected != null && selected != sec && selected.hasBase()){ //imports if(sec.info.destination == selected && sec.info.anyExports()){ planets.drawArc(planet, sec.tile.v, selected.tile.v, Color.gray.write(Tmp.c2).a(state.uiAlpha), Pal.accent.write(Tmp.c3).a(state.uiAlpha), 0.4f, 90f, 25); } //exports if(selected.info.destination == sec && selected.info.anyExports()){ planets.drawArc(planet, selected.tile.v, sec.tile.v, Pal.place.write(Tmp.c2).a(state.uiAlpha), Pal.accent.write(Tmp.c3).a(state.uiAlpha), 0.4f, 90f, 25); } } } } } } @Override public void renderProjections(Planet planet){ float iw = 64f/4f; for(Sector sec : planet.sectors){ if(sec != hovered){ var preficon = sec.icon(); var icon = sec.isAttacked() ? Fonts.getLargeIcon("warning") : !sec.hasBase() && sec.preset != null && sec.preset.requireUnlock && sec.preset.unlocked() && preficon == null ? (sec.preset != null && sec.preset.requireUnlock && sec.preset.unlocked() ? sec.preset.uiIcon : Fonts.getLargeIcon("terrain")) : sec.preset != null && sec.preset.requireUnlock && sec.preset.locked() && sec.preset.techNode != null && (sec.preset.techNode.parent == null || !sec.preset.techNode.parent.content.locked()) ? Fonts.getLargeIcon("lock") : preficon; var color = sec.isAttacked() ? Team.sharded.color : Color.white; if(icon != null){ planets.drawPlane(sec, () -> { //use white for content icons Draw.color(preficon == icon && sec.info.contentIcon != null ? Color.white : color, state.uiAlpha); Draw.rect(icon, 0, 0, iw, iw * icon.height / icon.width); }); } } } Draw.reset(); if(hovered != null && state.uiAlpha > 0.01f){ planets.drawPlane(hovered, () -> { Draw.color(hovered.isAttacked() ? Pal.remove : Color.white, Pal.accent, Mathf.absin(5f, 1f)); Draw.alpha(state.uiAlpha); var icon = hovered.locked() && !canSelect(hovered) && hovered.planet.generator != null ? hovered.planet.generator.getLockedIcon(hovered) : hovered.isAttacked() ? Fonts.getLargeIcon("warning") : hovered.icon(); if(icon != null){ Draw.rect(icon, 0, 0, iw, iw * icon.height / icon.width); } Draw.reset(); }); } Draw.reset(); } boolean selectable(Planet planet){ //TODO what if any sector is selectable? //TODO launch criteria - which planets can be launched to? Where should this be defined? Should planets even be selectable? if(mode == select) return planet == state.planet; if(mode == planetLaunch) return launchSector != null && (launchCandidates.contains(planet) || (planet == launchSector.planet && planet.allowSelfSectorLaunch)); return (planet.alwaysUnlocked && planet.isLandable()) || planet.sectors.contains(Sector::hasBase) || debugSelect; } void setup(){ searchText = ""; zoom = state.zoom = 1f; state.uiAlpha = 1f; ui.minimapfrag.hide(); clearChildren(); margin(0f); stack( mainView = new Element(){ { //add listener to the background rect, so it doesn't get unnecessary touch input addListener(new ElementGestureListener(){ @Override public void tap(InputEvent event, float x, float y, int count, KeyCode button){ if(showing() || button != KeyCode.mouseLeft) return; if(hovered != null && selected == hovered && count == 2){ playSelected(); } if(hovered != null && (canSelect(hovered) || debugSelect)){ selected = hovered; } if(selected != null){ updateSelected(); } } @Override public void touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){ super.touchDown(event, x, y, pointer, button); var hovered = PlanetDialog.this.hovered; if(debugSectorAttackEdit && hovered != null){ if(button == KeyCode.mouseRight){ if(input.shift()){ hovered.generateEnemyBase = !hovered.generateEnemyBase; }else{ selectDialog.show(state.planet, result -> { for(var other : state.planet.sectors){ if(other.preset == result){ other.preset = null; } } hovered.preset = result; }); } } } } }); } @Override public void act(float delta){ if(scene.getDialog() == PlanetDialog.this && (scene.getHoverElement() == null || !scene.getHoverElement().isDescendantOf(e -> e instanceof ScrollPane))){ scene.setScrollFocus(PlanetDialog.this); if(debugSectorAttackEdit){ int timeShift = input.keyDown(KeyCode.rightBracket) ? 1 : input.keyDown(KeyCode.leftBracket) ? -1 : 0; if(timeShift != 0){ universe.setSeconds(universe.secondsf() + timeShift * Time.delta * 2.5f); } if(input.keyTap(KeyCode.r)){ state.planet.reloadMeshAsync(); } } if(debugSectorAttackEdit && input.ctrl() && input.keyTap(KeyCode.s)){ try{ PlanetData data = new PlanetData(); IntSeq attack = new IntSeq(); for(var sector : state.planet.sectors){ if(sector.preset == null && sector.generateEnemyBase){ attack.add(sector.id); } if(sector.preset != null && sector.preset.requireUnlock){ data.presets.put(sector.preset.name, sector.id); } } Log.info("Saving sectors for @: @ presets, @ procedural attack sectors", state.planet.name, data.presets.size, attack.size); data.attackSectors = attack.toArray(); files.local("planets/" + state.planet.name + ".json").writeString(JsonIO.write(data)); ui.showInfoFade("@editor.saved"); }catch(Exception e){ Log.err(e); } } } super.act(delta); } @Override public void draw(){ planets.render(state); } }, //info text new Table(t -> { t.touchable = Touchable.disabled; t.top(); t.label(() -> mode == select ? "@sectors.select" : mode == planetLaunch ? "@sectors.launchselect" : "" ).style(Styles.outlineLabel).color(Pal.accent); }), buttons, // planet selection new Table(t -> { t.top().left(); ScrollPane pane = new ScrollPane(null, Styles.smallPane); t.add(pane).colspan(2).row(); t.button("@campaign.difficulty", Icon.bookSmall, () -> { campaignRules.show(state.planet); }).margin(12f).size(208f, 40f).padTop(12f).visible(() -> state.planet.allowCampaignRules && mode != planetLaunch).row(); t.add().height(64f); //padding for close button Table starsTable = new Table(Styles.black); pane.setWidget(starsTable); pane.setScrollingDisabled(true, false); int starCount = 0; for(Planet star : content.planets()){ if(star.solarSystem != star || !content.planets().contains(p -> p.solarSystem == star && selectable(p))) continue; starCount++; if(starCount > 1) starsTable.add(star.localizedName).padLeft(10f).padBottom(10f).padTop(10f).left().width(190f).row(); Table planetTable = new Table(); planetTable.margin(4f); //less padding starsTable.add(planetTable).left().row(); for(Planet planet : content.planets()){ if(planet.solarSystem == star && selectable(planet)){ Button planetButton = planetTable.button(planet.localizedName, Icon.icons.get(planet.icon + "Small", Icon.icons.get(planet.icon, Icon.commandRallySmall)), Styles.flatTogglet, () -> { viewPlanet(planet, false); }).width(200).height(40).update(bb -> bb.setChecked(state.planet == planet)).with(w -> w.marginLeft(10f)).get(); planetButton.getChildren().get(1).setColor(planet.iconColor); planetButton.setColor(planet.iconColor); planetTable.background(Tex.pane).row(); } } } }).visible(() -> mode != select), new Table(c -> expandTable = c)).grow(); rebuildExpand(); } void rebuildExpand(){ Table c = expandTable; c.clear(); c.visible(() -> !(graphics.isPortrait() && mobile) && mode != planetLaunch); if(state.planet.sectors.contains(Sector::hasBase)){ int attacked = state.planet.sectors.count(Sector::isAttacked); //sector notifications & search c.top().right(); c.defaults().width(290f); c.button(bundle.get("sectorlist") + (attacked == 0 ? "" : "\n[red]⚠[lightgray] " + bundle.format("sectorlist.attacked", "[red]" + attacked + "[]")), Icon.downOpen, Styles.squareTogglet, () -> sectorsShown = !sectorsShown) .height(60f).checked(b -> { Image image = (Image)b.getCells().first().get(); image.setDrawable(sectorsShown ? Icon.upOpen : Icon.downOpen); return sectorsShown; }).with(t -> t.left().margin(7f)).with(t -> t.getLabelCell().grow().left()).row(); c.collapser(t -> { t.background(Styles.black8); notifs = t; rebuildList(); }, false, () -> sectorsShown).padBottom(64f).row(); } } void rebuildList(){ if(notifs == null) return; notifs.clear(); var all = state.planet.sectors.select(Sector::hasBase); all.sort(Structs.comps(Structs.comparingBool(s -> !s.isAttacked()), Structs.comparingInt(s -> s.save == null ? 0 : -(int)s.save.meta.timePlayed))); notifs.pane(p -> { Runnable[] readd = {null}; p.table(s -> { s.image(Icon.zoom).padRight(4); s.field(searchText, t -> { searchText = t; readd[0].run(); }).growX().height(50f); }).growX().row(); Table con = p.table().growX().get(); con.touchable = Touchable.enabled; readd[0] = () -> { con.clearChildren(); for(Sector sec : all){ if(sec.hasBase() && (searchText.isEmpty() || sec.name().toLowerCase().contains(searchText.toLowerCase()))){ con.button(t -> { t.marginRight(10f); t.left(); t.defaults().growX(); t.table(head -> { head.left().defaults(); if(sec.isAttacked()){ head.image(Icon.warningSmall).update(i -> { i.color.set(Pal.accent).lerp(Pal.remove, Mathf.absin(Time.globalTime, 9f, 1f)); }).padRight(4f); }else if(sec.preset != null && sec.preset.requireUnlock){ head.image(sec.preset.uiIcon).size(iconSmall).padRight(4f); } String ic = sec.iconChar() == null ? "" : sec.iconChar() + " "; head.add(ic + sec.name()).growX().wrap(); }).growX().row(); if(sec.isAttacked()){ addSurvivedInfo(sec, t, true); } }, Styles.underlineb, () -> { lookAt(sec); selected = sec; updateSelected(); }).margin(8f).marginLeft(13f).marginBottom(6f).marginTop(6f).padBottom(3f).padTop(3f).growX().checked(b -> selected == sec).row(); //for resources: .tooltip(sec.info.resources.toString("", u -> u.emoji())) } } if(con.getChildren().isEmpty()){ con.add("@none.found").pad(10f); } }; readd[0].run(); }).grow().scrollX(false); } public Planet getHoverPlanet(float mouseX, float mouseY){ Planet hoverPlanet = null; float nearest = Float.POSITIVE_INFINITY; for(Planet planet : content.planets()){ Ray r = planets.cam.getPickRay(mouseX, mouseY); // get planet we're hovering over Vec3 intersect = planet.intersect(r, outlineRad * planet.radius); if(intersect != null && selectable(planet) && intersect.dst(r.origin) < nearest){ nearest = intersect.dst(r.origin); hoverPlanet = planet; } } return hoverPlanet; } public void viewPlanet(Planet planet, boolean pan){ if(state.planet != planet){ selected = null; if(pan){ state.otherCamPos = state.planet.position; state.otherCamAlpha = 0; } newPresets.clear(); state.planet = planet; clampZoom(); selected = null; updateSelected(); rebuildExpand(); settings.put("lastplanet", planet.name); } } @Override public void draw(){ boolean doBuffer = color.a < 0.99f; if(doBuffer){ buffer.resize(Core.graphics.getWidth(), Core.graphics.getHeight()); buffer.begin(Color.clear); } super.draw(); if(doBuffer){ buffer.end(); Draw.color(color); Draw.rect(Draw.wrap(buffer.getTexture()), width/2f, height/2f, width, -height); Draw.color(); } } @Override public void act(float delta){ super.act(delta); //update lerp if(state.otherCamPos != null){ state.otherCamAlpha = Mathf.lerpDelta(state.otherCamAlpha, 1f, 0.05f); state.camPos.set(0f, camLength, 0.1f); if(Mathf.equal(state.otherCamAlpha, 1f, 0.01f)){ //TODO change zoom too state.camPos.set(Tmp.v31.set(state.otherCamPos).slerp(state.planet.position, state.otherCamAlpha).add(state.camPos).sub(state.planet.position)); state.otherCamPos = null; //announce new sector if(!state.planet.sectors.isEmpty()){ newPresets.add(state.planet.sectors.get(state.planet.startSector)); } } } //fade in sector dialog after panning if(sectorTop != null && state.otherCamPos == null){ sectorTop.color.a = Mathf.lerpDelta(sectorTop.color.a, 1f, 0.1f); } if(hovered != null && state.planet.hasGrid()){ addChild(hoverLabel); hoverLabel.toFront(); hoverLabel.touchable = Touchable.disabled; hoverLabel.color.a = state.uiAlpha; Vec3 pos = hovered.planet.project(hovered, planets.cam, Tmp.v31); hoverLabel.setPosition(pos.x - Core.scene.marginLeft, pos.y - Core.scene.marginBottom, Align.center); hoverLabel.getText().setLength(0); if(hovered != null){ StringBuilder tx = hoverLabel.getText(); if(!canSelect(hovered)){ if(!(hovered.preset == null && !hovered.planet.allowLaunchToNumbered)){ if(mode == planetLaunch){ tx.append("[gray]").append(Iconc.cancel); }else if(hovered.planet.generator != null && mode != select){ hovered.planet.generator.getLockedText(hovered, tx); } } }else{ tx.append("[accent][[ [white]").append(hovered.name()).append("[accent] ]"); } } hoverLabel.invalidateHierarchy(); }else{ hoverLabel.remove(); } if(launching && selected != null){ lookAt(selected, 0.1f); } if(showing()){ Sector to = newPresets.peek(); presetShow += Time.delta; lookAt(to, 0.11f); zoom = 0.75f; if(presetShow >= 20f && !showed && newPresets.size > 1){ showed = true; ui.announce(Iconc.lockOpen + " [accent]" + to.name(), 2f); } if(presetShow > sectorShowDuration){ newPresets.pop(); showed = false; presetShow = 0f; } } if(state.planet.hasGrid()){ hovered = Core.scene.getDialog() == this ? state.planet.getSector(planets.cam.getMouseRay(), PlanetRenderer.outlineRad * state.planet.radius) : null; }else if(state.planet.isLandable()){ boolean wasNull = selected == null; //always have the first sector selected. //TODO better support for multiple sectors in gridless planets? hovered = selected = state.planet.sectors.first(); //autoshow if(wasNull){ updateSelected(); } }else{ hovered = selected = null; } state.zoom = Mathf.lerpDelta(state.zoom, zoom, 0.4f); state.uiAlpha = Mathf.lerpDelta(state.uiAlpha, Mathf.num(state.zoom < 1.9f), 0.1f); } void displayItems(Table c, float scl, ObjectMap stats, String name){ displayItems(c, scl, stats, name, t -> {}); } void displayItems(Table c, float scl, ObjectMap stats, String name, Cons builder){ Table t = new Table().left(); int i = 0; for(var item : content.items()){ var stat = stats.get(item); if(stat == null) continue; int total = (int)(stat.mean * 60 * scl); if(total > 1){ t.image(item.uiIcon).padRight(3); t.add(UI.formatAmount(total) + " " + Core.bundle.get("unit.perminute")).color(Color.lightGray).padRight(3); if(++i % 3 == 0){ t.row(); } } } if(t.getChildren().any()){ c.defaults().left(); c.add(name).row(); builder.get(c); c.add(t).padLeft(10f).row(); } } void showStats(Sector sector){ BaseDialog dialog = new BaseDialog(sector.name()); dialog.cont.pane(c -> { c.defaults().padBottom(5); if(sector.preset != null && sector.preset.description != null){ c.add(sector.preset.displayDescription()).width(420f).wrap().left().row(); } c.add(Core.bundle.get("sectors.time") + " [accent]" + sector.save.getPlayTime()).left().row(); if(sector.info.waves && sector.hasBase()){ c.add(Core.bundle.get("sectors.wave") + " [accent]" + (sector.info.wave + sector.info.wavesPassed)).left().row(); } if(sector.isAttacked() || !sector.hasBase()){ c.add(Core.bundle.get("sectors.threat") + " [accent]" + sector.displayThreat()).left().row(); } if(sector.save != null && sector.info.resources.any()){ c.add("@sectors.resources").left().row(); c.table(t -> { for(UnlockableContent uc : sector.info.resources){ if(uc == null) continue; t.image(uc.uiIcon).scaling(Scaling.fit).padRight(3).size(iconSmall); } }).padLeft(10f).left().row(); } //production displayItems(c, sector.getProductionScale(), sector.info.production, "@sectors.production"); //export displayItems(c, sector.getProductionScale(), sector.info.export, "@sectors.export", t -> { if(sector.info.destination != null && sector.info.destination.hasBase()){ String ic = sector.info.destination.iconChar(); t.add(Iconc.rightOpen + " " + (ic == null || ic.isEmpty() ? "" : ic + " ") + sector.info.destination.name()).padLeft(10f).row(); } }); //import if(sector.hasBase()){ displayItems(c, 1f, sector.info.imports, "@sectors.import", t -> { sector.info.eachImport(sector.planet, other -> { String ic = other.iconChar(); t.add(Iconc.rightOpen + " " + (ic == null || ic.isEmpty() ? "" : ic + " ") + other.name()).padLeft(10f).row(); }); }); } ItemSeq items = sector.items(); //stored resources if(sector.hasBase() && items.total > 0){ c.add("@sectors.stored").left().row(); c.table(t -> { t.left(); t.table(res -> { int i = 0; for(ItemStack stack : items){ res.image(stack.item.uiIcon).padRight(3); res.add(UI.formatAmount(Math.max(stack.amount, 0))).color(Color.lightGray); if(++i % 4 == 0){ res.row(); } } }).padLeft(10f); }).left().row(); } }); dialog.addCloseButton(); if(sector.hasBase()){ dialog.buttons.button("@sector.abandon", Icon.cancel, () -> abandonSectorConfirm(sector, dialog::hide)); } dialog.show(); } public void abandonSectorConfirm(Sector sector, Runnable listener){ ui.showConfirm("@sector.abandon.confirm", () -> { if(listener != null) listener.run(); if(sector.isBeingPlayed()){ hide(); //after dialog is hidden Time.runTask(7f, () -> { //force game over in a more dramatic fashion for(var core : player.team().cores().copy()){ core.kill(); } }); }else{ sector.info.items.clear(); sector.info.damage = 1f; sector.info.hasCore = false; sector.info.production.clear(); sector.saveInfo(); } updateSelected(); rebuildList(); }); } void addSurvivedInfo(Sector sector, Table table, boolean wrap){ if(!wrap){ table.add(sector.planet.allowWaveSimulation ? Core.bundle.format("sectors.underattack", (int)(sector.info.damage * 100)) : "@sectors.underattack.nodamage").wrapLabel(wrap).row(); } if(sector.planet.allowWaveSimulation && sector.info.wavesSurvived >= 0 && sector.info.wavesSurvived - sector.info.wavesPassed >= 0 && !sector.isBeingPlayed()){ int toCapture = sector.info.attack || sector.info.winWave <= 1 ? -1 : sector.info.winWave - (sector.info.wave + sector.info.wavesPassed); boolean plus = (sector.info.wavesSurvived - sector.info.wavesPassed) >= SectorDamage.maxRetWave - 1; table.add(Core.bundle.format("sectors.survives", Math.min(sector.info.wavesSurvived - sector.info.wavesPassed, toCapture <= 0 ? 200 : toCapture) + (plus ? "+" : "") + (toCapture < 0 ? "" : "/" + toCapture))).wrapLabel(wrap).row(); } } public void selectSector(Sector sector){ selected = sector; updateSelected(); } void updateSelected(){ Sector sector = selected; Table stable = sectorTop; if(sector == null){ stable.clear(); stable.visible = false; return; } stable.visible = true; float x = stable.getX(Align.center), y = stable.getY(Align.center); stable.clear(); stable.background(Styles.black6); stable.table(title -> { title.add("[accent]" + sector.name() + (debugSelect && (sector.info.name != null || sector.preset != null) ? " [lightgray](" + sector.id + ")" : "")).padLeft(3); if(sector.preset == null){ title.add().growX(); title.button(Icon.pencilSmall, Styles.clearNonei, () -> { ui.showTextInput("@sectors.rename", "@name", 32, sector.name(), v -> { sector.setName(v); updateSelected(); rebuildList(); }); }).size(40f).padLeft(4); } var icon = sector.info.contentIcon != null ? new TextureRegionDrawable(sector.info.contentIcon.uiIcon) : Icon.icons.get(sector.info.icon + "Small"); title.button(icon == null ? Icon.noneSmall : icon, Styles.clearNonei, iconSmall, () -> { //TODO use IconSelectDialog new Dialog(""){{ closeOnBack(); setFillParent(true); Runnable refresh = () -> { sector.saveInfo(); hide(); updateSelected(); rebuildList(); }; cont.pane(t -> { resized(true, () -> { t.clearChildren(); t.marginRight(19f); t.defaults().size(48f); t.button(Icon.none, Styles.squareTogglei, () -> { sector.info.icon = null; sector.info.contentIcon = null; refresh.run(); }).checked(sector.info.icon == null && sector.info.contentIcon == null); int cols = (int)Math.min(20, Core.graphics.getWidth() / Scl.scl(52f)); int i = 1; for(var key : accessibleIcons){ var value = Icon.icons.get(key); t.button(value, Styles.squareTogglei, () -> { sector.info.icon = key; sector.info.contentIcon = null; refresh.run(); }).checked(key.equals(sector.info.icon)); if(++i % cols == 0) t.row(); } for(ContentType ctype : defaultContentIcons){ t.row(); t.image().colspan(cols).growX().width(Float.NEGATIVE_INFINITY).height(3f).color(Pal.accent); t.row(); i = 0; for(UnlockableContent u : content.getBy(ctype).as()){ if(!u.isHidden() && u.unlocked()){ t.button(new TextureRegionDrawable(u.uiIcon), Styles.squareTogglei, iconMed, () -> { sector.info.icon = null; sector.info.contentIcon = u; refresh.run(); }).checked(sector.info.contentIcon == u); if(++i % cols == 0) t.row(); } } } }); }); buttons.button("@back", Icon.left, this::hide).size(210f, 64f); }}.show(); }).size(40f).tooltip("@sector.changeicon"); }).row(); stable.image().color(Pal.accent).fillX().height(3f).pad(3f).row(); boolean locked = sector.preset != null && sector.preset.locked() && !sector.hasBase() && sector.preset.techNode != null; if(locked){ stable.table(r -> { r.add("@complete").colspan(2).left(); r.row(); for(Objective o : sector.preset.techNode.objectives){ if(o.complete()) continue; r.add("> " + o.display()).color(Color.lightGray).left(); r.image(o.complete() ? Icon.ok : Icon.cancel, o.complete() ? Color.lightGray : Color.scarlet).padLeft(3); r.row(); } }).row(); }else if(!sector.hasBase()){ stable.add(Core.bundle.get("sectors.threat") + " [accent]" + sector.displayThreat()).row(); } if(sector.isAttacked()){ addSurvivedInfo(sector, stable, false); }else if(sector.hasBase() && sector.planet.campaignRules.sectorInvasion && sector.near().contains(s -> s.hasEnemyBase() && (s.preset == null || !s.preset.requireUnlock))){ stable.add("@sectors.vulnerable"); stable.row(); }else if(!sector.hasBase() && sector.hasEnemyBase()){ stable.add("@sectors.enemybase"); stable.row(); } if(sector.save != null && sector.info.resources.any()){ stable.table(t -> { t.add("@sectors.resources").padRight(4); for(UnlockableContent c : sector.info.resources){ if(c == null) continue; //apparently this is possible. t.image(c.uiIcon).padRight(3).scaling(Scaling.fit).size(iconSmall); } }).padLeft(10f).fillX().row(); } stable.row(); if(sector.hasBase()){ stable.button("@stats", Icon.info, Styles.cleart, () -> showStats(sector)).height(40f).fillX().row(); } if((sector.hasBase() && mode == look) || canSelect(sector) || (sector.preset != null && sector.preset.alwaysUnlocked) || debugSelect){ if(Vars.showSectorSubmissions){ String link = SectorSubmissions.getSectorThread(sector); if(link != null){ stable.button("@sectors.viewsubmission", Icon.link, () -> { Core.app.openURI(link); }).growX().height(54f).minWidth(170f).padTop(2f).row(); } } stable.button( mode == select ? "@sectors.select" : sector.isBeingPlayed() ? "@sectors.resume" : sector.hasBase() ? "@sectors.go" : locked ? "@locked" : "@sectors.launch", locked ? Icon.lock : Icon.play, this::playSelected).growX().height(54f).minWidth(170f).padTop(4).disabled(locked); } stable.pack(); stable.setPosition(x, y, Align.center); stable.act(0f); } void playSelected(){ if(selected == null) return; Sector sector = selected; if(sector.isBeingPlayed()){ //already at this sector hide(); return; } if(sector.preset != null && sector.preset.requireUnlock && sector.preset.locked() && sector.preset.techNode != null && !sector.hasBase()){ return; } Planet planet = sector.planet; //make sure there are no under-attack sectors (other than this one) if(!planet.allowWaveSimulation && !debugSelect){ //if there are two or more attacked sectors... something went wrong, don't show the dialog to prevent softlock Sector attacked = planet.sectors.find(s -> s.isAttacked() && s != sector); if(attacked != null && planet.sectors.count(s -> s.isAttacked()) < 2){ BaseDialog dialog = new BaseDialog("@sector.noswitch.title"); dialog.cont.add(bundle.format("sector.noswitch", attacked.name(), attacked.planet.localizedName)).width(400f).labelAlign(Align.center).center().wrap(); dialog.addCloseButton(); dialog.buttons.button("@sector.view", Icon.eyeSmall, () -> { dialog.hide(); lookAt(attacked); selectSector(attacked); }); dialog.show(); return; } } boolean shouldHide = true; //save before launch. if(control.saves.getCurrent() != null && Vars.state.isGame() && mode != select){ try{ control.saves.getCurrent().save(); }catch(Throwable e){ e.printStackTrace(); ui.showException("[accent]" + Core.bundle.get("savefail"), e); } } if(mode == look && !sector.hasBase()){ shouldHide = false; Sector from = findLauncher(sector); if(from == null){ //clear loadout information, so only the basic loadout gets used universe.clearLoadoutInfo(); //free launch. control.playSector(sector); }else{ CoreBlock block = sector.allowLaunchSchematics() ? (from.info.bestCoreType instanceof CoreBlock b ? b : (CoreBlock)from.planet.defaultCore) : (CoreBlock)from.planet.defaultCore; loadouts.show(block, from, sector, () -> { var loadout = universe.getLastLoadout(); var schemCore = loadout.findCore(); from.removeItems(loadout.requirements()); from.removeItems(universe.getLaunchResources()); Events.fire(new SectorLaunchLoadoutEvent(sector, from, loadout)); CoreBuild core = player.team().core(); if(core == null || settings.getBool("skipcoreanimation")){ //just... go there control.playSector(from, sector); //hide only after load screen is shown Time.runTask(8f, this::hide); }else{ //hide immediately so launch sector is visible hide(); //allow planet dialog to finish hiding before actually launching Time.runTask(5f, () -> { Runnable doLaunch = () -> { renderer.showLaunch(core); //run with less delay, as the loading animation is delayed by several frames Time.runTask(core.launchDuration() - 8f, () -> control.playSector(from, sector)); }; //load launchFrom sector right before launching so animation is correct if(!from.isBeingPlayed()){ //run *after* the loading animation is done Time.runTask(9f, doLaunch); control.playSector(from); }else{ doLaunch.run(); } }); } }); } }else if(mode == select || mode == planetLaunch){ listener.get(sector); }else{ //sector should have base here control.playSector(sector); } if(shouldHide) hide(); } public enum Mode{ /** Look around for existing sectors. Can only deploy. */ look, /** Select a sector for some purpose. */ select, /** Launch between planets. */ planetLaunch } }