Merge branch 'master' into port-field

This commit is contained in:
Anuken
2025-02-08 19:36:26 -05:00
committed by GitHub
2681 changed files with 151338 additions and 45271 deletions

View File

@@ -8,6 +8,7 @@ import arc.math.*;
import arc.math.geom.*;
import arc.scene.*;
import arc.scene.style.*;
import arc.scene.ui.layout.*;
import arc.util.pooling.*;
import mindustry.gen.*;
@@ -15,9 +16,9 @@ public class Bar extends Element{
private static Rect scissor = new Rect();
private Floatp fraction;
private String name = "";
private float value, lastValue, blink;
private Color blinkColor = new Color();
private CharSequence name = "";
private float value, lastValue, blink, outlineRadius;
private Color blinkColor = new Color(), outlineColor = new Color();
public Bar(String name, Color color, Floatp fraction){
this.fraction = fraction;
@@ -27,21 +28,13 @@ public class Bar extends Element{
setColor(color);
}
public Bar(Prov<String> name, Prov<Color> color, Floatp fraction){
public Bar(Prov<CharSequence> name, Prov<Color> color, Floatp fraction){
this.fraction = fraction;
try{
lastValue = value = Mathf.clamp(fraction.get());
}catch(Exception e){ //getting the fraction may involve referring to invalid data
lastValue = value = 0f;
}
lastValue = value = Mathf.clamp(fraction.get());
update(() -> {
try{
this.name = name.get();
this.blinkColor.set(color.get());
setColor(color.get());
}catch(Exception e){ //getting the fraction may involve referring to invalid data
this.name = "";
}
this.name = name.get();
this.blinkColor.set(color.get());
setColor(color.get());
});
}
@@ -61,6 +54,20 @@ public class Bar extends Element{
update(() -> this.name = name.get());
}
public void snap(){
lastValue = value = fraction.get();
}
public Bar outline(Color color, float stroke){
outlineColor.set(color);
outlineRadius = Scl.scl(stroke);
return this;
}
public void flash(){
blink = 1f;
}
public Bar blink(Color color){
blinkColor.set(color);
return this;
@@ -70,12 +77,8 @@ public class Bar extends Element{
public void draw(){
if(fraction == null) return;
float computed;
try{
computed = Mathf.clamp(fraction.get());
}catch(Exception e){ //getting the fraction may involve referring to invalid data
computed = 0f;
}
float computed = Mathf.clamp(fraction.get());
if(lastValue > computed){
blink = 1f;
@@ -94,9 +97,16 @@ public class Bar extends Element{
Drawable bar = Tex.bar;
if(outlineRadius > 0){
Draw.color(outlineColor);
bar.draw(x - outlineRadius, y - outlineRadius, width + outlineRadius*2, height + outlineRadius*2);
}
Draw.colorl(0.1f);
Draw.alpha(parentAlpha);
bar.draw(x, y, width, height);
Draw.color(color, blinkColor, blink);
Draw.alpha(parentAlpha);
Drawable top = Tex.barTop;
float topWidth = width * value;
@@ -116,8 +126,10 @@ public class Bar extends Element{
GlyphLayout lay = Pools.obtain(GlyphLayout.class, GlyphLayout::new);
lay.setText(font, name);
font.setColor(Color.white);
font.draw(name, x + width / 2f - lay.width / 2f, y + height / 2f + lay.height / 2f + 1);
font.setColor(1f, 1f, 1f, 1f);
font.getCache().clear();
font.getCache().addText(name, x + width / 2f - lay.width / 2f, y + height / 2f + lay.height / 2f + 1);
font.getCache().draw(parentAlpha);
Pools.free(lay);
}

View File

@@ -2,12 +2,13 @@ package mindustry.ui;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import mindustry.graphics.*;
public class BorderImage extends Image{
public float thickness = 4f;
public float thickness = 4f, pad = 0f;
public Color borderColor = Pal.gray;
public BorderImage(){
@@ -28,6 +29,10 @@ public class BorderImage extends Image{
thickness = thick;
}
public BorderImage(Drawable region){
super(region);
}
public BorderImage border(Color color){
this.borderColor = color;
return this;
@@ -40,7 +45,7 @@ public class BorderImage extends Image{
Draw.color(borderColor);
Draw.alpha(parentAlpha);
Lines.stroke(Scl.scl(thickness));
Lines.rect(x + imageX, y + imageY, imageWidth * scaleX, imageHeight * scaleY);
Lines.rect(x + imageX - pad, y + imageY - pad, imageWidth * scaleX + pad*2, imageHeight * scaleY + pad*2);
Draw.reset();
}
}

View File

@@ -1,23 +0,0 @@
package mindustry.ui;
import java.util.*;
/** Defines sizes of a content's preview icon. */
public enum Cicon{
/** Full size. */
full(0),
tiny(8 * 2),
small(8 * 3),
medium(8 * 4),
large(8 * 5),
xlarge(8 * 6);
public static final Cicon[] all = values();
public static final Cicon[] scaled = Arrays.copyOfRange(all, 1, all.length);
public final int size;
Cicon(int size){
this.size = size;
}
}

View File

@@ -19,12 +19,15 @@ public class CoreItemsDisplay extends Table{
public void resetUsed(){
usedItems.clear();
background(null);
}
void rebuild(){
clear();
background(Styles.black6);
margin(4);
if(usedItems.size > 0){
background(Styles.black6);
margin(4);
}
update(() -> {
core = Vars.player.team().core();
@@ -38,9 +41,9 @@ public class CoreItemsDisplay extends Table{
for(Item item : content.items()){
if(usedItems.contains(item)){
image(item.icon(Cicon.small)).padRight(3);
image(item.uiIcon).size(iconSmall).padRight(3).tooltip(t -> t.background(Styles.black6).margin(4f).add(item.localizedName).style(Styles.outlineLabel));
//TODO leaks garbage
label(() -> core == null ? "0" : UI.formatAmount(core.items.get(item))).padRight(3).left();
label(() -> core == null ? "0" : UI.formatAmount(core.items.get(item))).padRight(3).minWidth(52f).left().tooltip(t -> t.background(Styles.black6).margin(4f).label(() -> core == null ? "0" : core.items.get(item) + "").style(Styles.outlineLabel));
if(++i % 4 == 0){
row();

View File

@@ -4,5 +4,9 @@ import arc.scene.ui.layout.*;
/** An interface for things that can be displayed when hovered over. */
public interface Displayable{
default boolean displayable(){
return true;
}
void display(Table table);
}

View File

@@ -3,14 +3,11 @@ package mindustry.ui;
import arc.*;
import arc.Graphics.Cursor.*;
import arc.assets.*;
import arc.assets.loaders.*;
import arc.assets.loaders.resolvers.*;
import arc.files.*;
import arc.freetype.*;
import arc.freetype.FreeTypeFontGenerator.*;
import arc.freetype.FreetypeFontLoader.*;
import arc.graphics.*;
import arc.graphics.Pixmap.*;
import arc.graphics.Texture.*;
import arc.graphics.g2d.*;
import arc.graphics.g2d.Font.*;
@@ -23,30 +20,20 @@ import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.game.*;
import mindustry.gen.*;
import java.util.*;
import java.io.*;
public class Fonts{
private static final String mainFont = "fonts/font.woff";
private static final ObjectSet<String> unscaled = ObjectSet.with("iconLarge");
private static ObjectIntMap<String> unicodeIcons = new ObjectIntMap<>();
private static IntMap<String> unicodeToName = new IntMap<>();
private static ObjectMap<String, String> stringIcons = new ObjectMap<>();
private static ObjectMap<String, TextureRegion> largeIcons = new ObjectMap<>();
private static TextureRegion[] iconTable;
private static int lastCid;
public static Font def;
public static Font outline;
public static Font chat;
public static Font icon;
public static Font iconLarge;
public static Font tech;
public static TextureRegion logicIcon(int id){
return iconTable[id];
}
public static Font def, outline, icon, iconLarge, tech, logic;
public static int getUnicode(String content){
return unicodeIcons.get(content, 0);
@@ -56,6 +43,10 @@ public class Fonts{
return stringIcons.get(content, "");
}
public static boolean hasUnicodeStr(String content){
return stringIcons.containsKey(content);
}
/** Called from a static context to make the cursor appear immediately upon startup.*/
public static void loadSystemCursors(){
SystemCursor.arrow.set(Core.graphics.newCursor("cursor", cursorScale()));
@@ -73,20 +64,33 @@ public class Fonts{
largeIcons.clear();
FreeTypeFontParameter param = fontParameter();
Core.assets.load("default", Font.class, new FreeTypeFontLoaderParameter(mainFont, param)).loaded = f -> Fonts.def = (Font)f;
Core.assets.load("chat", Font.class, new FreeTypeFontLoaderParameter(mainFont, param)).loaded = f -> Fonts.chat = (Font)f;
Core.assets.load("default", Font.class, new FreeTypeFontLoaderParameter(mainFont, param)).loaded = f -> Fonts.def = f;
Core.assets.load("icon", Font.class, new FreeTypeFontLoaderParameter("fonts/icon.ttf", new FreeTypeFontParameter(){{
size = 30;
incremental = true;
characters = "\0";
}})).loaded = f -> Fonts.icon = (Font)f;
}})).loaded = f -> Fonts.icon = f;
Core.assets.load("iconLarge", Font.class, new FreeTypeFontLoaderParameter("fonts/icon.ttf", new FreeTypeFontParameter(){{
size = 48;
incremental = false;
characters = "\0" + Iconc.all;
borderWidth = 5f;
borderColor = Color.darkGray;
}})).loaded = f -> Fonts.iconLarge = (Font)f;
}})).loaded = f -> Fonts.iconLarge = f;
Core.assets.load("logic", Font.class, new FreeTypeFontLoaderParameter("fonts/logic.ttf", new FreeTypeFontParameter(){{
size = 16;
//generated all at once, it's fast enough anyway
incremental = false;
//ASCII only
characters = "\0ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890\"!`?'.,;:()[]{}<>|/@\\^$€-%+=#_&~*";
}})).loaded = f -> Fonts.logic = f;
}
public static @Nullable String unicodeToName(int unicode){
return unicodeToName.get(unicode, () -> Iconc.codeToName.get(unicode));
}
public static TextureRegion getLargeIcon(String name){
@@ -94,7 +98,7 @@ public class Fonts{
var region = new TextureRegion();
int code = Iconc.codes.get(name, '\uF8D4');
var glyph = iconLarge.getData().getGlyph((char)code);
if(glyph == null) return Core.atlas.find("error");
if(glyph == null) return Core.atlas.find(name);
region.set(iconLarge.getRegion().texture);
region.set(glyph.u, glyph.v2, glyph.u2, glyph.v);
return region;
@@ -102,13 +106,13 @@ public class Fonts{
}
public static void loadContentIcons(){
Seq<Font> fonts = Seq.with(Fonts.chat, Fonts.def, Fonts.outline);
Seq<Font> fonts = Seq.with(Fonts.def, Fonts.outline);
Texture uitex = Core.atlas.find("logo").texture;
int size = (int)(Fonts.def.getData().lineHeight/Fonts.def.getData().scaleY);
try(Scanner scan = new Scanner(Core.files.internal("icons/icons.properties").read(512))){
while(scan.hasNextLine()){
String line = scan.nextLine();
try(var reader = Core.files.internal("icons/icons.properties").reader(Vars.bufferSize)){
String line;
while((line = reader.readLine()) != null){
String[] split = line.split("=");
String[] nametex = split[1].split("\\|");
String character = split[0], texture = nametex[1];
@@ -117,18 +121,20 @@ public class Fonts{
if(region.texture != uitex){
continue;
//throw new IllegalArgumentException("Font icon '" + texture + "' is not in the UI texture.");
}
unicodeIcons.put(nametex[0], ch);
stringIcons.put(nametex[0], ((char)ch) + "");
unicodeToName.put(ch, texture);
Vec2 out = Scaling.fit.apply(region.width, region.height, size, size);
Glyph glyph = new Glyph();
glyph.id = ch;
glyph.srcX = 0;
glyph.srcY = 0;
glyph.width = size;
glyph.height = (int)((float)region.height / region.width * size);
glyph.width = (int)out.x;
glyph.height = (int)out.y;
glyph.u = region.u;
glyph.v = region.v2;
glyph.u2 = region.u2;
@@ -141,30 +147,47 @@ public class Fonts{
glyph.page = 0;
fonts.each(f -> f.getData().setGlyph(ch, glyph));
}
}catch(IOException e){
throw new RuntimeException(e);
}
iconTable = new TextureRegion[512];
iconTable[0] = Core.atlas.find("error");
lastCid = 1;
stringIcons.put("alphachan", stringIcons.get("alphaaaa"));
Vars.content.each(c -> {
if(c instanceof UnlockableContent u){
TextureRegion region = Core.atlas.find(u.name + "-icon-logic");
if(region.found()){
iconTable[u.iconId = lastCid++] = region;
}
for(Team team : Team.baseTeams){
team.emoji = stringIcons.get(team.name, "");
}
}
public static void loadContentIconsHeadless(){
try(var reader = Core.files.internal("icons/icons.properties").reader(Vars.bufferSize)){
String line;
while((line = reader.readLine()) != null){
String[] split = line.split("=");
String[] nametex = split[1].split("\\|");
String character = split[0];
int ch = Integer.parseInt(character);
unicodeIcons.put(nametex[0], ch);
stringIcons.put(nametex[0], ((char)ch) + "");
}
});
}catch(IOException e){
throw new RuntimeException(e);
}
stringIcons.put("alphachan", stringIcons.get("alphaaaa"));
for(Team team : Team.baseTeams){
team.emoji = stringIcons.get(team.name, "");
}
}
/** Called from a static context for use in the loading screen.*/
public static void loadDefaultFont(){
int max = Gl.getInt(Gl.maxTextureSize);
UI.packer = new PixmapPacker(max >= 4096 ? 4096 : 2048, 2048, Format.rgba8888, 2, true);
FileHandleResolver resolver = new InternalFileHandleResolver();
Core.assets.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(resolver));
Core.assets.setLoader(Font.class, null, new FreetypeFontLoader(resolver){
UI.packer = new PixmapPacker(max >= 4096 ? 4096 : 2048, 2048, 2, true);
Core.assets.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(Core.files::internal));
Core.assets.setLoader(Font.class, null, new FreetypeFontLoader(Core.files::internal){
ObjectSet<FreeTypeFontParameter> scaled = new ObjectSet<>();
@Override
@@ -192,19 +215,19 @@ public class Fonts{
size = 18;
}};
Core.assets.load("outline", Font.class, new FreeTypeFontLoaderParameter(mainFont, param)).loaded = t -> Fonts.outline = (Font)t;
Core.assets.load("outline", Font.class, new FreeTypeFontLoaderParameter(mainFont, param)).loaded = t -> Fonts.outline = t;
Core.assets.load("tech", Font.class, new FreeTypeFontLoaderParameter("fonts/tech.ttf", new FreeTypeFontParameter(){{
size = 18;
}})).loaded = f -> {
Fonts.tech = (Font)f;
((Font)f).getData().down *= 1.5f;
Fonts.tech = f;
Fonts.tech.getData().down *= 1.5f;
};
}
/** Merges the UI and font atlas together for better performance. */
public static void mergeFontAtlas(TextureAtlas atlas){
//grab all textures from the ui page, remove all the regions assigned to it, then copy them over to Fonts.packer and replace the texture in this atlas.
//grab all textures from the ui page, remove all the regions assigned to it, then copy them over to UI.packer and replace the texture in this atlas.
//grab old UI texture and regions...
Texture texture = atlas.find("logo").texture;
@@ -215,13 +238,16 @@ public class Fonts{
for(AtlasRegion region : regions){
//get new pack rect
page.setDirty(false);
Rect rect = UI.packer.pack(region.name + (region.splits != null ? ".9" : ""), atlas.getPixmap(region));
Rect rect = UI.packer.pack(region.name, atlas.getPixmap(region), region.splits, region.pads);
//set new texture
region.texture = UI.packer.getPages().first().getTexture();
//set its new position
region.set((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height);
//add old texture
atlas.getTextures().add(region.texture);
//clear it
region.pixmapRegion = null;
}
//remove old texture, it will no longer be used
@@ -234,8 +260,12 @@ public class Fonts{
}
public static TextureRegionDrawable getGlyph(Font font, char glyph){
Glyph g = font.getData().getGlyph(glyph);
if(g == null) throw new IllegalArgumentException("No glyph: " + glyph + " (" + (int)glyph + ")");
Glyph found = font.getData().getGlyph(glyph);
if(found == null){
Log.warn("No icon found for glyph: @ (@)", glyph, (int)glyph);
found = font.getData().getGlyph('F');
}
Glyph g = found;
float size = Math.max(g.width, g.height);
TextureRegionDrawable draw = new TextureRegionDrawable(new TextureRegion(font.getRegion().texture, g.u, g.v2, g.u2, g.v)){
@@ -258,7 +288,7 @@ public class Fonts{
cy = (int)cy;
originX = g.width/2f;
originY = g.height/2f;
Draw.rect(region, cx + g.width/2f, cy + g.height/2f, g.width, g.height, originX, originY, rotation);
Draw.rect(region, cx + g.width/2f, cy + g.height/2f, g.width * scaleX, g.height * scaleY, originX, originY, rotation);
}
@Override

View File

@@ -1,14 +0,0 @@
package mindustry.ui;
public enum IconSize{
def(48),
small(32),
smaller(30),
tiny(16);
public final int size;
IconSize(int size){
this.size = size;
}
}

View File

@@ -1,26 +0,0 @@
package mindustry.ui;
import arc.scene.ui.layout.*;
import mindustry.type.*;
/** An item image with text. */
public class ItemDisplay extends Table{
public final Item item;
public final int amount;
public ItemDisplay(Item item){
this(item, 0);
}
public ItemDisplay(Item item, int amount, boolean showName){
add(new ItemImage(new ItemStack(item, amount)));
if(showName) add(item.localizedName).padLeft(4 + amount > 99 ? 4 : 0);
this.item = item;
this.amount = amount;
}
public ItemDisplay(Item item, int amount){
this(item, amount, true);
}
}

View File

@@ -1,47 +0,0 @@
package mindustry.ui;
import arc.graphics.g2d.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import mindustry.core.*;
import mindustry.type.*;
public class ItemImage extends Stack{
public ItemImage(TextureRegion region, int amount){
add(new Table(o -> {
o.left();
o.add(new Image(region)).size(32f);
}));
add(new Table(t -> {
t.left().bottom();
t.add(amount > 1000 ? UI.formatAmount(amount) : amount + "");
t.pack();
}));
}
public ItemImage(TextureRegion region){
Table t = new Table().left().bottom();
add(new Image(region));
add(t);
}
public ItemImage(ItemStack stack){
add(new Table(o -> {
o.left();
o.add(new Image(stack.item.icon(Cicon.medium))).size(32f);
}));
if(stack.amount != 0){
add(new Table(t -> {
t.left().bottom();
t.add(stack.amount > 1000 ? UI.formatAmount(stack.amount) : stack.amount + "").style(Styles.outlineLabel);
t.pack();
}));
}
}
}

View File

@@ -15,6 +15,7 @@ import static mindustry.Vars.*;
/** Displays a list of items, e.g. launched items.*/
public class ItemsDisplay extends Table{
boolean collapsed;
public ItemsDisplay(){
rebuild(new ItemSeq());
@@ -29,6 +30,8 @@ public class ItemsDisplay extends Table{
top().left();
margin(0);
if(items == null) return;
table(Tex.button, c -> {
c.margin(10).marginLeft(12).marginTop(15f);
c.marginRight(12f);
@@ -41,7 +44,7 @@ public class ItemsDisplay extends Table{
if(!items.has(item)) continue;
Label label = t.add(UI.formatAmount(items.get(item))).left().get();
t.image(item.icon(Cicon.small)).size(8 * 3).padLeft(4).padRight(4);
t.image(item.uiIcon).size(8 * 3).padLeft(4).padRight(4);
t.add(item.localizedName).color(Color.lightGray).left();
t.row();
@@ -50,10 +53,13 @@ public class ItemsDisplay extends Table{
label.actions(Actions.color(Color.white, 0.75f, Interp.fade));
}
}
}).get().setScrollingDisabled(true, false), false).setDuration(0.3f);
}).scrollX(false), false).setDuration(0.3f);
c.button("@globalitems", Icon.downOpen, Styles.clearTogglet, col::toggle).update(t -> {
col.setCollapsed(collapsed, false);
c.button("@globalitems", Icon.downOpen, Styles.flatTogglet, col::toggle).update(t -> {
t.setChecked(col.isCollapsed());
collapsed = col.isCollapsed();
((Image)t.getChildren().get(1)).setDrawable(col.isCollapsed() ? Icon.upOpen : Icon.downOpen);
}).padBottom(4).left().fillX().margin(12f).minWidth(200f);
c.row();

View File

@@ -26,7 +26,7 @@ public class Links{
new LinkEntry("f-droid", "https://f-droid.org/packages/io.anuke.mindustry/", Icon.android, Color.valueOf("026aa7")),
new LinkEntry("github", "https://github.com/Anuken/Mindustry/", Icon.github, Color.valueOf("24292e")),
new LinkEntry("dev-builds", "https://github.com/Anuken/MindustryBuilds", Icon.githubSquare, Color.valueOf("fafbfc")),
new LinkEntry("bug", report(), Icon.wrench, Color.valueOf("cbd97f"))
new LinkEntry("bug", "https://github.com/Anuken/Mindustry/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml", Icon.wrench, Color.valueOf("cbd97f"))
};
}
@@ -52,23 +52,4 @@ public class Links{
this.title = Core.bundle.get("link." + name + ".title", Strings.capitalize(name.replace("-", " ")));
}
}
private static String report(){
return "https://github.com/Anuken/Mindustry/issues/new?assignees=&labels=bug&body=" +
Strings.encode(Strings.format(
"**Platform**: `@`\n" +
"\n**Build**: `@`\n" +
"\n**Issue**: *Explain your issue in detail.*\n" +
"\n**Steps to reproduce**: *How you happened across the issue, and what exactly you did to make the bug happen.*\n" +
"\n**Link(s) to mod(s) used**: `@`\n" +
"\n**Save file**: *The (zipped) save file you were playing on when the bug happened. THIS IS REQUIRED FOR ANY ISSUE HAPPENING IN-GAME, REGARDLESS OF WHETHER YOU THINK IT HAPPENS EVERYWHERE. DO NOT DELETE OR OMIT THIS LINE UNLESS YOU ARE SURE THAT THE ISSUE DOES NOT HAPPEN IN-GAME.*\n" +
"\n**Crash report**: *The contents of relevant crash report files. REQUIRED if you are reporting a crash.*\n" +
"\n---\n" +
"\n*Place an X (no spaces) between the brackets to confirm that you have read the line below.*" +
"\n- [ ] **I have updated to the latest release (https://github.com/Anuken/Mindustry/releases) to make sure my issue has not been fixed.**" +
"\n- [ ] **I have searched the closed and open issues to make sure that this problem has not already been reported.**",
OS.isAndroid ? "Android " + Core.app.getVersion() : (System.getProperty("os.name") + (OS.is64Bit ? " x64" : " x32")),
Version.combined(),
Vars.mods.list().any() ? Vars.mods.list().select(LoadedMod::enabled).map(l -> l.meta.author + "/" + l.name + ":" + l.meta.version) : "none"));
}
}
}

View File

@@ -1,37 +0,0 @@
package mindustry.ui;
import arc.graphics.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.type.*;
import mindustry.world.meta.*;
/** An ItemDisplay, but for liquids. */
public class LiquidDisplay extends Table{
public final Liquid liquid;
public final float amount;
public final boolean perSecond;
public LiquidDisplay(Liquid liquid, float amount, boolean perSecond){
this.liquid = liquid;
this.amount = amount;
this.perSecond = perSecond;
add(new Stack(){{
add(new Image(liquid.icon(Cicon.medium)));
if(amount != 0){
Table t = new Table().left().bottom();
t.add(Strings.autoFixed(amount, 1)).style(Styles.outlineLabel);
add(t);
}
}}).size(8 * 4).padRight(3 + (amount != 0 && Strings.autoFixed(amount, 1).length() > 2 ? 8 : 0));
if(perSecond){
add(StatUnit.perSecond.localized()).padLeft(2).padRight(5).color(Color.lightGray).style(Styles.outlineLabel);
}
add(liquid.localizedName);
}
}

View File

@@ -0,0 +1,187 @@
package mindustry.ui;
import arc.*;
import arc.struct.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import static mindustry.Vars.*;
/** Class for handling menus and notifications across the network. Unstable API! */
public class Menus{
private static final Seq<MenuListener> menuListeners = new Seq<>();
private static final Seq<TextInputListener> textInputListeners = new Seq<>();
/** Register a *global* menu listener. If no option is chosen, the option is returned as -1. */
public static int registerMenu(MenuListener listener){
menuListeners.add(listener);
return menuListeners.size - 1;
}
/** Register a *global* text input listener. If no text is provided, the text is returned as null. */
public static int registerTextInput(TextInputListener listener){
textInputListeners.add(listener);
return textInputListeners.size - 1;
}
//do not invoke any of the methods below directly, use Call
@Remote(variants = Variant.both)
public static void menu(int menuId, String title, String message, String[][] options){
if(title == null) title = "";
if(message == null) message = "";
if(options == null) options = new String[0][0];
ui.showMenu(title, message, options, (option) -> Call.menuChoose(player, menuId, option));
}
@Remote(variants = Variant.both)
public static void followUpMenu(int menuId, String title, String message, String[][] options){
if(title == null) title = "";
if(message == null) message = "";
if(options == null) options = new String[0][0];
ui.showFollowUpMenu(menuId, title, message, options, (option) -> Call.menuChoose(player, menuId, option));
}
@Remote(variants = Variant.both)
public static void hideFollowUpMenu(int menuId) {
ui.hideFollowUpMenu(menuId);
}
@Remote(targets = Loc.both, called = Loc.both)
public static void menuChoose(@Nullable Player player, int menuId, int option){
if(player != null){
Events.fire(new MenuOptionChooseEvent(player, menuId, option));
if(menuId >= 0 && menuId < menuListeners.size){
menuListeners.get(menuId).get(player, option);
}
}
}
@Remote(variants = Variant.both)
public static void textInput(int textInputId, String title, String message, int textLength, String def, boolean numeric){
textInput(textInputId, title, message, textLength, def, numeric, false);
}
@Remote(variants = Variant.both)
public static void textInput(int textInputId, String title, String message, int textLength, String def, boolean numeric, boolean allowEmpty){
if(title == null) title = "";
if(message == null) message = "";
if(def == null) def = "";
ui.showTextInput(title, message, textLength, def, numeric, allowEmpty, (text) -> {
Call.textInputResult(player, textInputId, text);
}, () -> {
Call.textInputResult(player, textInputId, null);
});
}
@Remote(targets = Loc.both, called = Loc.both)
public static void textInputResult(@Nullable Player player, int textInputId, @Nullable String text){
if(player != null){
Events.fire(new TextInputEvent(player, textInputId, text));
if(textInputId >= 0 && textInputId < textInputListeners.size){
textInputListeners.get(textInputId).get(player, text);
}
}
}
@Remote(variants = Variant.both, unreliable = true)
public static void setHudText(String message){
if(message == null) return;
ui.hudfrag.setHudText(message);
}
@Remote(variants = Variant.both)
public static void hideHudText(){
ui.hudfrag.toggleHudText(false);
}
/** TCP version */
@Remote(variants = Variant.both)
public static void setHudTextReliable(String message){
setHudText(message);
}
@Remote(variants = Variant.both)
public static void announce(String message){
if(message == null) return;
ui.announce(message);
}
@Remote(variants = Variant.both)
public static void infoMessage(String message){
if(message == null) return;
ui.showText("", message);
}
@Remote(variants = Variant.both, unreliable = true)
public static void infoPopup(String message, float duration, int align, int top, int left, int bottom, int right){
if(message == null) return;
ui.showInfoPopup(message, duration, align, top, left, bottom, right);
}
@Remote(variants = Variant.both, unreliable = true)
public static void label(String message, float duration, float worldx, float worldy){
if(message == null) return;
ui.showLabel(message, duration, worldx, worldy);
}
@Remote(variants = Variant.both)
public static void infoPopupReliable(String message, float duration, int align, int top, int left, int bottom, int right){
if(message == null) return;
ui.showInfoPopup(message, duration, align, top, left, bottom, right);
}
@Remote(variants = Variant.both)
public static void labelReliable(String message, float duration, float worldx, float worldy){
label(message, duration, worldx, worldy);
}
@Remote(variants = Variant.both)
public static void infoToast(String message, float duration){
if(message == null) return;
ui.showInfoToast(message, duration);
}
@Remote(variants = Variant.both)
public static void warningToast(int unicode, String text){
if(text == null || Fonts.icon.getData().getGlyph((char)unicode) == null) return;
ui.hudfrag.showToast(Fonts.getGlyph(Fonts.icon, (char)unicode), text);
}
@Remote(variants = Variant.both)
public static void openURI(String uri){
if(uri == null) return;
ui.showConfirm(Core.bundle.format("linkopen", uri), () -> Core.app.openURI(uri));
}
//internal use only
@Remote
public static void removeWorldLabel(int id){
var label = Groups.label.getByID(id);
if(label != null){
label.remove();
}
}
public interface MenuListener{
void get(Player player, int option);
}
public interface TextInputListener{
void get(Player player, @Nullable String text);
}
}

View File

@@ -3,9 +3,11 @@ package mindustry.ui;
import arc.*;
import arc.graphics.g2d.*;
import arc.input.*;
import arc.math.*;
import arc.scene.*;
import arc.scene.event.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.gen.*;
import static mindustry.Vars.*;
@@ -20,6 +22,22 @@ public class Minimap extends Table{
add(new Element(){
{
setSize(Scl.scl(140f));
addListener(new ClickListener(KeyCode.mouseRight){
@Override
public void clicked(InputEvent event, float cx, float cy){
var region = renderer.minimap.getRegion();
if(region == null) return;
float
sx = (cx - x) / width,
sy = (cy - y) / height,
scaledX = Mathf.lerp(region.u, region.u2, sx) * world.width() * tilesize,
scaledY = Mathf.lerp(1f - region.v2, 1f - region.v, sy) * world.height() * tilesize;
control.input.panCamera(Tmp.v1.set(scaledX, scaledY));
}
});
}
@Override
@@ -37,6 +55,7 @@ public class Minimap extends Table{
Draw.rect(renderer.minimap.getRegion(), x + width / 2f, y + height / 2f, width, height);
if(renderer.minimap.getTexture() != null){
Draw.alpha(parentAlpha);
renderer.minimap.drawEntities(x, y, width, height, 0.75f, false);
}
@@ -92,7 +111,7 @@ public class Minimap extends Table{
update(() -> {
Element e = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true);
Element e = Core.scene.getHoverElement();
if(e != null && e.isDescendantOf(this)){
requestScroll();
}else if(hasScroll()){

View File

@@ -5,6 +5,7 @@ import arc.graphics.g2d.*;
import arc.scene.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.graphics.*;
public class ReqImage extends Stack{
@@ -30,7 +31,7 @@ public class ReqImage extends Stack{
}
public ReqImage(TextureRegion region, Boolp valid){
this(new Image(region), valid);
this(new Image(region).setScaling(Scaling.fit), valid);
}
public boolean valid(){

View File

@@ -1,62 +0,0 @@
package mindustry.ui;
import arc.func.*;
import arc.graphics.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import mindustry.gen.*;
public class SearchBar{
public static <T> Table add(Table parent, Seq<T> list, Func<String, String> queryf,
Func<T, String> namef, Cons2<Table, T> itemc, boolean show){
Table[] pane = {null};
Cons<String> rebuild = str -> {
String query = queryf.get(str);
pane[0].clear();
boolean any = false;
for(T item : list){
if(query.isEmpty() || matches(query, namef.get(item))){
any = true;
itemc.get(pane[0], item);
}
}
if(!any){
pane[0].add("@none.found").color(Color.lightGray).pad(4);
}
};
if(show){
parent.table(search -> {
search.image(Icon.zoom).padRight(8f);
search.field("", rebuild).growX();
}).fillX().padBottom(4);
}
parent.row();
parent.pane(table -> {
pane[0] = table;
rebuild.get("");
});
return pane[0];
}
public static <T> Table add(Table parent, Seq<T> list, Func<String, String> queryf, Func<T, String> namef, Cons2<Table, T> itemc){
return add(parent, list, queryf, namef, itemc, true);
}
public static <T> Table add(Table parent, Seq<T> list, Func<T, String> namef, Cons2<Table, T> itemc, boolean show){
return add(parent, list, String::toLowerCase, namef, itemc, show);
}
/** Match a list item with the search query, case insensitive */
public static boolean matches(String query, String name){
if(name == null || name.isEmpty()){
return false;
}
return name.toLowerCase().contains(query);
}
}

View File

@@ -9,12 +9,12 @@ import arc.scene.ui.Button.*;
import arc.scene.ui.CheckBox.*;
import arc.scene.ui.Dialog.*;
import arc.scene.ui.ImageButton.*;
import arc.scene.ui.KeybindDialog.*;
import arc.scene.ui.Label.*;
import arc.scene.ui.ScrollPane.*;
import arc.scene.ui.Slider.*;
import arc.scene.ui.TextButton.*;
import arc.scene.ui.TextField.*;
import arc.scene.ui.TreeElement.*;
import mindustry.annotations.Annotations.*;
import mindustry.gen.*;
import mindustry.graphics.*;
@@ -24,19 +24,83 @@ import static mindustry.gen.Tex.*;
@StyleDefaults
public class Styles{
//TODO all these names are inconsistent and not descriptive
public static Drawable black, black9, black8, black6, black3, black5, none, flatDown, flatOver;
public static ButtonStyle defaultb, waveb, modsb;
public static TextButtonStyle defaultt, squaret, nodet, cleart, discordt, nonet, infot, clearPartialt, clearTogglet, clearToggleMenut, togglet, transt, fullTogglet, logict;
public static ImageButtonStyle defaulti, nodei, righti, emptyi, emptytogglei, selecti, logici, geni, colori, accenti, cleari, clearFulli, clearPartiali, clearPartial2i, clearTogglei, clearTransi, clearToggleTransi, clearTogglePartiali;
public static ScrollPaneStyle defaultPane, horizontalPane, smallPane;
public static KeybindDialogStyle defaultKeybindDialog;
public static SliderStyle defaultSlider, vSlider;
public static Drawable black, black9, black8, black6, black3, black5, grayPanel, none, flatDown, flatOver, accentDrawable;
public static ButtonStyle defaultb, underlineb;
/** Default text button style - gray corners at 45 degrees. */
public static TextButtonStyle defaultt,
/** Flat, square, opaque. */
flatt,
/** Flat, square, opaque, gray. */
grayt,
/** Flat, square, toggleable. */
flatTogglet,
/** Flat, square, gray border.*/
flatBordert,
/** No background whatsoever, only text. */
nonet,
/** Similar to flatToggle, but slightly tweaked for logic. */
logicTogglet,
/** Similar to flatToggle, but with a transparent base background. */
flatToggleMenut,
/** Toggle variant of default style. */
togglet,
/** Partially transparent square button. */
cleart,
/** Clear, square, orange border, toggleable. */
clearTogglet,
/** Similar to flatToggle, but without a darker border. */
fullTogglet,
/** Toggle-able version of flatBorder. */
squareTogglet,
/** Special square button for logic dialogs. */
logict;
/** Default image button style - gray corners at 45 degrees. */
public static ImageButtonStyle defaulti,
/** Used for research nodes in the tech tree. */
nodei,
/** No background, tints the image itself when hovered. */
emptyi,
/** Toggleable variant of emptyi */
emptyTogglei,
/** Displays border around image when selected; used in placement fragment. */
selecti,
/** Pure black version of emptyi, used for logic toolbar. */
logici,
/** Used for toolbar in map generation filters. */
geni,
/** Gray, toggleable, no background. */
grayi,
/** Gray square background, standard behavior. Equivalent to grayt. */
graySquarei,
/** Flat, square, black background. */
flati,
/** Square border. */
squarei,
/** Square border, toggleable. */
squareTogglei,
/** No background unless focused, no border. */
clearNonei,
/** Partially transparent black background. */
cleari,
/** Toggleable variant of cleari. */
clearTogglei,
/** clearNone, but toggleable. */
clearNoneTogglei;
public static ScrollPaneStyle defaultPane, horizontalPane, smallPane, noBarPane;
public static SliderStyle defaultSlider;
public static LabelStyle defaultLabel, outlineLabel, techLabel;
public static TextFieldStyle defaultField, nodeField, areaField, nodeArea;
public static CheckBoxStyle defaultCheck;
public static DialogStyle defaultDialog, fullDialog;
public static TreeStyle defaultTree;
public static void load(){
var whiteui = (TextureRegionDrawable)Tex.whiteui;
black = whiteui.tint(0f, 0f, 0f, 1f);
black9 = whiteui.tint(0f, 0f, 0f, 0.9f);
black8 = whiteui.tint(0f, 0f, 0f, 0.8f);
@@ -44,8 +108,10 @@ public class Styles{
black5 = whiteui.tint(0f, 0f, 0f, 0.5f);
black3 = whiteui.tint(0f, 0f, 0f, 0.3f);
none = whiteui.tint(0f, 0f, 0f, 0f);
grayPanel = whiteui.tint(Pal.darkestGray);
flatDown = createFlatDown();
flatOver = whiteui.tint(Color.valueOf("454545"));
accentDrawable = whiteui.tint(Pal.accent);
defaultb = new ButtonStyle(){{
down = buttonDown;
@@ -54,16 +120,11 @@ public class Styles{
disabled = buttonDisabled;
}};
modsb = new ButtonStyle(){{
underlineb = new ButtonStyle(){{
down = flatOver;
up = underline;
over = underlineWhite;
}};
waveb = new ButtonStyle(){{
up = wavepane;
over = wavepane; //TODO wrong
disabled = wavepane;
up = sideline;
over = sidelineOver;
checked = flatOver;
}};
defaultt = new TextButtonStyle(){{
@@ -75,23 +136,6 @@ public class Styles{
down = buttonDown;
up = button;
}};
squaret = new TextButtonStyle(){{
font = Fonts.def;
fontColor = Color.white;
disabledFontColor = Color.gray;
over = buttonSquareOver;
disabled = buttonDisabled;
down = buttonSquareDown;
up = buttonSquare;
}};
nodet = new TextButtonStyle(){{
disabled = button;
font = Fonts.def;
fontColor = Color.white;
disabledFontColor = Color.gray;
up = buttonOver;
over = buttonDown;
}};
nonet = new TextButtonStyle(){{
font = Fonts.outline;
fontColor = Color.lightGray;
@@ -99,7 +143,7 @@ public class Styles{
disabledFontColor = Color.gray;
up = none;
}};
cleart = new TextButtonStyle(){{
flatt = new TextButtonStyle(){{
over = flatOver;
font = Fonts.def;
fontColor = Color.white;
@@ -107,6 +151,14 @@ public class Styles{
down = flatOver;
up = black;
}};
grayt = new TextButtonStyle(){{
over = flatOver;
font = Fonts.def;
fontColor = Color.white;
disabledFontColor = Color.lightGray;
down = flatOver;
up = grayPanel;
}};
logict = new TextButtonStyle(){{
over = flatOver;
font = Fonts.def;
@@ -115,17 +167,7 @@ public class Styles{
down = flatOver;
up = underlineWhite;
}};
discordt = new TextButtonStyle(){{
font = Fonts.def;
fontColor = Color.white;
up = discordBanner;
}};
infot = new TextButtonStyle(){{
font = Fonts.def;
fontColor = Color.white;
up = infoBanner;
}};
clearPartialt = new TextButtonStyle(){{
flatBordert = new TextButtonStyle(){{
down = flatOver;
up = pane;
over = flatDownBase;
@@ -133,7 +175,7 @@ public class Styles{
fontColor = Color.white;
disabledFontColor = Color.gray;
}};
transt = new TextButtonStyle(){{
cleart = new TextButtonStyle(){{
down = flatDown;
up = none;
over = flatOver;
@@ -141,7 +183,7 @@ public class Styles{
fontColor = Color.white;
disabledFontColor = Color.gray;
}};
clearTogglet = new TextButtonStyle(){{
flatTogglet = new TextButtonStyle(){{
font = Fonts.def;
fontColor = Color.white;
checked = flatDown;
@@ -151,7 +193,17 @@ public class Styles{
disabled = black;
disabledFontColor = Color.gray;
}};
clearToggleMenut = new TextButtonStyle(){{
logicTogglet = new TextButtonStyle(){{
font = Fonts.outline;
fontColor = Color.white;
checked = accentDrawable;
down = accentDrawable;
up = black;
over = flatOver;
disabled = black;
disabledFontColor = Color.gray;
}};
flatToggleMenut = new TextButtonStyle(){{
font = Fonts.def;
fontColor = Color.white;
checked = flatDown;
@@ -171,6 +223,16 @@ public class Styles{
disabled = buttonDisabled;
disabledFontColor = Color.gray;
}};
clearTogglet = new TextButtonStyle(){{
font = Fonts.def;
fontColor = Color.white;
down = flatDown;
checked = flatDown;
up = black6;
over = flatOver;
disabled = black;
disabledFontColor = Color.gray;
}};
fullTogglet = new TextButtonStyle(){{
font = Fonts.def;
fontColor = Color.white;
@@ -181,6 +243,16 @@ public class Styles{
disabled = black;
disabledFontColor = Color.gray;
}};
squareTogglet = new TextButtonStyle(){{
font = Fonts.def;
fontColor = Color.white;
checked = flatOver;
down = flatOver;
up = pane;
over = flatOver;
disabled = black;
disabledFontColor = Color.gray;
}};
defaulti = new ImageButtonStyle(){{
down = buttonDown;
up = button;
@@ -193,19 +265,12 @@ public class Styles{
up = buttonOver;
over = buttonDown;
}};
righti = new ImageButtonStyle(){{
over = buttonRightOver;
down = buttonRightDown;
up = buttonRight;
disabled = buttonRightDisabled;
imageDisabledColor = Color.clear;
imageUpColor = Color.white;
}};
emptyi = new ImageButtonStyle(){{
imageDownColor = Pal.accent;
imageOverColor = Color.lightGray;
imageUpColor = Color.white;
}};
emptytogglei = new ImageButtonStyle(){{
emptyTogglei = new ImageButtonStyle(){{
imageCheckedColor = Color.white;
imageDownColor = Color.white;
imageUpColor = Color.gray;
@@ -215,33 +280,35 @@ public class Styles{
up = none;
}};
logici = new ImageButtonStyle(){{
//imageDownColor = Pal.accent;
imageUpColor = Color.black;
}};
geni = new ImageButtonStyle(){{
imageDownColor = Pal.accent;
imageUpColor = Color.black;
}};
colori = new ImageButtonStyle(){{
//imageDownColor = Pal.accent;
imageUpColor = Color.white;
}};
accenti = new ImageButtonStyle(){{
//imageDownColor = Pal.accent;
grayi = new ImageButtonStyle(){{
imageUpColor = Color.lightGray;
imageDownColor = Color.white;
}};
cleari = new ImageButtonStyle(){{
graySquarei = new ImageButtonStyle(){{
imageUpColor = Color.white;
imageDownColor = Color.lightGray;
over = flatOver;
down = flatOver;
up = grayPanel;
}};
flati = new ImageButtonStyle(){{
down = flatOver;
up = black;
over = flatOver;
}};
clearFulli = new ImageButtonStyle(){{
squarei = new ImageButtonStyle(){{
down = whiteui;
up = pane;
over = flatDown;
}};
clearPartiali = new ImageButtonStyle(){{
clearNonei = new ImageButtonStyle(){{
down = flatDown;
up = none;
over = flatOver;
@@ -249,18 +316,13 @@ public class Styles{
imageDisabledColor = Color.gray;
imageUpColor = Color.white;
}};
clearPartial2i = new ImageButtonStyle(){{
down = whiteui;
up = pane;
over = flatDown;
}};
clearTogglei = new ImageButtonStyle(){{
squareTogglei = new ImageButtonStyle(){{
down = flatDown;
checked = flatDown;
up = black;
over = flatOver;
}};
clearTransi = new ImageButtonStyle(){{
cleari = new ImageButtonStyle(){{
down = flatDown;
up = black6;
over = flatOver;
@@ -268,13 +330,13 @@ public class Styles{
imageDisabledColor = Color.lightGray;
imageUpColor = Color.white;
}};
clearToggleTransi = new ImageButtonStyle(){{
clearTogglei = new ImageButtonStyle(){{
down = flatDown;
checked = flatDown;
up = black6;
over = flatOver;
}};
clearTogglePartiali = new ImageButtonStyle(){{
clearNoneTogglei = new ImageButtonStyle(){{
down = flatDown;
checked = flatDown;
up = none;
@@ -295,21 +357,10 @@ public class Styles{
vScroll = clear;
vScrollKnob = scrollKnobVerticalThin;
}};
defaultKeybindDialog = new KeybindDialogStyle(){{
keyColor = Pal.accent;
keyNameColor = Color.white;
controllerColor = Color.lightGray;
}};
noBarPane = new ScrollPaneStyle();
defaultSlider = new SliderStyle(){{
background = slider;
knob = sliderKnob;
knobOver = sliderKnobOver;
knobDown = sliderKnobDown;
}};
vSlider = new SliderStyle(){{
background = sliderVertical;
background = sliderBack;
knob = sliderKnob;
knobOver = sliderKnobOver;
knobDown = sliderKnobDown;
@@ -329,7 +380,7 @@ public class Styles{
}};
defaultField = new TextFieldStyle(){{
font = Fonts.chat;
font = Fonts.def;
fontColor = Color.white;
disabledFontColor = Color.gray;
disabledBackground = underlineDisabled;
@@ -342,7 +393,7 @@ public class Styles{
}};
nodeField = new TextFieldStyle(){{
font = Fonts.chat;
font = Fonts.def;
fontColor = Color.white;
disabledFontColor = Color.gray;
disabledBackground = underlineDisabled;
@@ -355,7 +406,7 @@ public class Styles{
}};
areaField = new TextFieldStyle(){{
font = Fonts.chat;
font = Fonts.def;
fontColor = Color.white;
disabledFontColor = Color.gray;
selection = Tex.selection;
@@ -366,7 +417,7 @@ public class Styles{
}};
nodeArea = new TextFieldStyle(){{
font = Fonts.chat;
font = Fonts.def;
fontColor = Color.white;
disabledFontColor = Color.gray;
selection = Tex.selection;
@@ -400,6 +451,13 @@ public class Styles{
background = windowEmpty;
titleFontColor = Pal.accent;
}};
defaultTree = new TreeStyle(){{
plus = Icon.downOpen;
minus = Icon.upOpen;
background = black5;
over = flatOver;
}};
}
private static Drawable createFlatDown(){

View File

@@ -2,6 +2,7 @@ package mindustry.ui;
import arc.graphics.g2d.*;
import arc.scene.*;
import arc.scene.ui.layout.*;
import mindustry.graphics.*;
public class WarningBar extends Element{
@@ -27,7 +28,7 @@ public class WarningBar extends Element{
rx + barWidth, y
);
}
Lines.stroke(3f);
Lines.stroke(Scl.scl(3f));
Lines.line(x, y, x + width, y);
Lines.line(x, y + height, x + width, y + height);

View File

@@ -35,36 +35,36 @@ public class AboutDialog extends BaseDialog{
buttons.clear();
float h = Core.graphics.isPortrait() ? 90f : 80f;
float w = Core.graphics.isPortrait() ? 330f : 600f;
float w = Core.graphics.isPortrait() ? 400f : 600f;
Table in = new Table();
ScrollPane pane = new ScrollPane(in);
for(LinkEntry link : Links.getLinks()){
if((ios || OS.isMac || steam) && bannedItems.contains(link.name)){
if((ios || steam) && bannedItems.contains(link.name)){
continue;
}
Table table = new Table(Tex.underline);
Table table = new Table(Styles.grayPanel);
table.margin(0);
table.table(img -> {
img.image().height(h - 5).width(40f).color(link.color);
img.row();
img.image().height(5).width(40f).color(link.color.cpy().mul(0.8f, 0.8f, 0.8f, 1f));
img.image().height(5).width(40f).color(link.color.cpy().mul(0.6f, 0.6f, 0.8f, 1f));
}).expandY();
table.table(i -> {
i.background(Tex.buttonEdge3);
i.background(Styles.grayPanel);
i.image(link.icon);
}).size(h - 5, h);
table.table(inset -> {
inset.add("[accent]" + link.title).growX().left();
inset.row();
inset.labelWrap(link.description).width(w - 100f).color(Color.lightGray).growX();
inset.labelWrap(link.description).width(w - 100f - h).color(Color.lightGray).growX();
}).padLeft(8);
table.button(Icon.link, () -> {
table.button(Icon.link, Styles.clearNonei, () -> {
if(link.name.equals("wiki")) Events.fire(Trigger.openWiki);
if(!Core.app.openURI(link.link)){
@@ -84,12 +84,6 @@ public class AboutDialog extends BaseDialog{
buttons.button("@credits", this::showCredits).size(200f, 64f);
if(Core.graphics.isPortrait()){
for(Cell<?> cell : buttons.getCells()){
cell.width(140f);
}
}
}
public void showCredits(){

View File

@@ -1,5 +1,6 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import mindustry.gen.*;
@@ -39,7 +40,7 @@ public class AdminsDialog extends BaseDialog{
res.labelWrap("[lightgray]" + info.lastName).width(w - h - 24f);
res.add().growX();
res.button(Icon.cancel, () -> {
ui.showConfirm("@confirm", "@confirmunadmin", () -> {
ui.showConfirm("@confirm", Core.bundle.format("@confirmunadmin", info.lastName), () -> {
netServer.admins.unAdminPlayer(info.id);
Groups.player.each(player -> {
if(player != null && !player.isLocal() && player.uuid().equals(info.id)){

View File

@@ -11,7 +11,8 @@ import mindustry.graphics.*;
import static mindustry.Vars.*;
public class BaseDialog extends Dialog{
private boolean wasPaused;
protected boolean wasPaused;
/** If true, this dialog will pause the game while open. */
protected boolean shouldPause;
public BaseDialog(String title, DialogStyle style){
@@ -19,20 +20,17 @@ public class BaseDialog extends Dialog{
setFillParent(true);
this.title.setAlignment(Align.center);
titleTable.row();
titleTable.image(Tex.whiteui, Pal.accent)
.growX().height(3f).pad(4f);
titleTable.image(Tex.whiteui, Pal.accent).growX().height(3f).pad(4f);
hidden(() -> {
if(shouldPause && state.isGame()){
if(!wasPaused || net.active()){
state.set(State.playing);
}
if(shouldPause && state.isGame() && !net.active() && !wasPaused){
state.set(State.playing);
}
Sounds.back.play();
});
shown(() -> {
if(shouldPause && state.isGame()){
if(shouldPause && state.isGame() && !net.active()){
wasPaused = state.is(State.paused);
state.set(State.paused);
}
@@ -43,6 +41,14 @@ public class BaseDialog extends Dialog{
this(title, Core.scene.getStyle(DialogStyle.class));
}
/** Places the buttons as an overlay on top of the content. Used when the content can be scrolled through.*/
protected void makeButtonOverlay(){
clearChildren();
add(titleTable).growX().row();
stack(cont, buttons).grow();
buttons.bottom();
}
protected void onResize(Runnable run){
Events.on(ResizeEvent.class, event -> {
if(isShown() && Core.scene.getDialog() == this){
@@ -56,11 +62,15 @@ public class BaseDialog extends Dialog{
closeOnBack();
}
@Override
public void addCloseButton(){
buttons.defaults().size(210f, 64f);
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
public void addCloseButton(float width){
buttons.defaults().size(width, 64f);
buttons.button("@back", Icon.left, this::hide).size(width, 64f);
addCloseListener();
}
@Override
public void addCloseButton(){
addCloseButton(210f);
}
}

View File

@@ -0,0 +1,45 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.math.*;
import arc.scene.actions.*;
import mindustry.*;
import mindustry.core.*;
import mindustry.gen.*;
import mindustry.type.*;
import static arc.scene.actions.Actions.*;
public class CampaignCompleteDialog extends BaseDialog{
public CampaignCompleteDialog(){
super("");
addCloseListener();
shouldPause = true;
buttons.defaults().size(210f, 64f);
buttons.button("@menu", Icon.left, () -> {
hide();
Vars.ui.paused.runExitSave();
});
buttons.button("@continue", Icon.ok, this::hide);
}
public void show(Planet planet){
cont.clear();
cont.add(Core.bundle.format("campaign.complete", "[#" + planet.iconColor + "]" + planet.localizedName + "[]")).row();
float playtime = planet.sectors.sumf(s -> s.hasSave() ? s.save.meta.timePlayed : 0) / 1000f;
//TODO needs more info?
cont.add(Core.bundle.format("campaign.playtime", UI.formatTime(playtime))).left().row();
setTranslation(0f, -Core.graphics.getHeight());
color.a = 0f;
show(Core.scene, Actions.sequence(parallel(fadeIn(1.1f, Interp.fade), translateBy(0f, Core.graphics.getHeight(), 6f, Interp.pow5Out))));
}
}

View File

@@ -0,0 +1,100 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import mindustry.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.ui.*;
public class CampaignRulesDialog extends BaseDialog{
Planet planet;
Table current;
public CampaignRulesDialog(){
super("@campaign.difficulty");
addCloseButton();
hidden(() -> {
if(planet != null){
planet.saveRules();
if(Vars.state.isGame() && Vars.state.isCampaign() && Vars.state.getPlanet() == planet){
planet.campaignRules.apply(planet, Vars.state.rules);
Call.setRules(Vars.state.rules);
}
}
});
onResize(() -> {
rebuild();
});
}
void rebuild(){
CampaignRules rules = planet.campaignRules;
cont.clear();
cont.top().pane(inner -> {
inner.top().left().defaults().fillX().left().pad(5);
current = inner;
current.table(Tex.button, t -> {
t.margin(10f);
var group = new ButtonGroup<>();
var style = Styles.flatTogglet;
t.defaults().size(140f, 50f);
for(Difficulty diff : Difficulty.all){
t.button(diff.localized(), style, () -> {
rules.difficulty = diff;
}).group(group).checked(b -> rules.difficulty == diff);
if(Core.graphics.isPortrait() && diff.ordinal() % 2 == 1){
t.row();
}
}
}).left().fill(false).expand(false, false).row();
if(planet.allowSectorInvasion){
check("@rules.invasions", b -> rules.sectorInvasion = b, () -> rules.sectorInvasion);
}
check("@rules.fog", b -> rules.fog = b, () -> rules.fog);
check("@rules.showspawns", b -> rules.showSpawns = b, () -> rules.showSpawns);
check("@rules.randomwaveai", b -> rules.randomWaveAI = b, () -> rules.randomWaveAI);
//TODO: this is intentionally hidden until the new mechanics have been well-tested. I don't want people immediately switching to the old mechanics
if(planet.allowLegacyLaunchPads){
// check("@rules.legacylaunchpads", b -> rules.legacyLaunchPads = b, () -> rules.legacyLaunchPads);
}
}).growY();
}
public void show(Planet planet){
this.planet = planet;
rebuild();
show();
}
void check(String text, Boolc cons, Boolp prov){
check(text, cons, prov, () -> true);
}
void check(String text, Boolc cons, Boolp prov, Boolp condition){
String infoText = text.substring(1) + ".info";
var cell = current.check(text, cons).checked(prov.get()).update(a -> a.setDisabled(!condition.get()));
if(Core.bundle.has(infoText)){
cell.tooltip(text + ".info");
}
cell.get().left();
current.row();
}
}

View File

@@ -2,13 +2,21 @@ package mindustry.ui.dialogs;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.Texture.*;
import arc.graphics.g2d.*;
import arc.scene.*;
import arc.scene.ui.*;
import arc.util.*;
import mindustry.gen.*;
import mindustry.graphics.*;
public class ColorPicker extends BaseDialog{
private static Texture hueTex;
private Cons<Color> cons = c -> {};
Color current = new Color();
float h, s, v, a;
TextField hexField;
Slider hSlider, sSlider, vSlider, aSlider;
public ColorPicker(){
super("@pickcolor");
@@ -23,6 +31,17 @@ public class ColorPicker extends BaseDialog{
this.cons = consumer;
show();
if(hueTex == null){
hueTex = Pixmaps.hueTexture(128, 1);
hueTex.setFilter(TextureFilter.linear);
}
float[] values = color.toHsv(new float[3]);
h = values[0];
s = values[1];
v = values[2];
a = color.a;
cont.clear();
cont.pane(t -> {
t.table(Tex.pane, i -> {
@@ -32,26 +51,114 @@ public class ColorPicker extends BaseDialog{
}}).size(200f);
}).colspan(2).padBottom(5);
float w = 150f;
t.row();
t.defaults().padBottom(4);
t.add("R").color(Pal.remove);
t.slider(0f, 1f, 0.01f, current.r, current::r).width(w);
t.row();
t.add("G").color(Color.lime);
t.slider(0f, 1f, 0.01f, current.g, current::g).width(w);
t.row();
t.add("B").color(Color.royal);
t.slider(0f, 1f, 0.01f, current.b, current::b).width(w);
t.row();
t.defaults().padBottom(6).width(370f).height(44f);
t.stack(new Image(new TextureRegion(hueTex)), hSlider = new Slider(0f, 360f, 0.3f, false){{
setValue(h);
moved(value -> {
h = value;
updateColor();
});
}}).row();
t.stack(new Element(){
@Override
public void draw(){
float first = Tmp.c1.set(current).saturation(0f).a(parentAlpha).toFloatBits();
float second = Tmp.c1.set(current).saturation(1f).a(parentAlpha).toFloatBits();
Fill.quad(
x, y, first,
x + width, y, second,
x + width, y + height, second,
x, y + height, first
);
}
}, sSlider = new Slider(0f, 1f, 0.001f, false){{
setValue(s);
moved(value -> {
s = value;
updateColor();
});
}}).row();
t.stack(new Element(){
@Override
public void draw(){
float first = Tmp.c1.set(current).value(0f).a(parentAlpha).toFloatBits();
float second = Tmp.c1.fromHsv(h, s, 1f).a(parentAlpha).toFloatBits();
Fill.quad(
x, y, first,
x + width, y, second,
x + width, y + height, second,
x, y + height, first
);
}
}, vSlider = new Slider(0f, 1f, 0.001f, false){{
setValue(v);
moved(value -> {
v = value;
updateColor();
});
}}).row();
if(alpha){
t.add("A");
t.slider(0f, 1f, 0.01f, current.a, current::a).width(w);
t.row();
t.stack(new Image(Tex.alphaBgLine), new Element(){
@Override
public void draw(){
float first = Tmp.c1.set(current).a(0f).toFloatBits();
float second = Tmp.c1.set(current).a(parentAlpha).toFloatBits();
Fill.quad(
x, y, first,
x + width, y, second,
x + width, y + height, second,
x, y + height, first
);
}
}, aSlider = new Slider(0f, 1f, 0.001f, false){{
setValue(a);
moved(value -> {
a = value;
updateColor();
});
}}).row();
}
});
hexField = t.field(current.toString(), value -> {
try{
Color.valueOf(current, value);
current.toHsv(values);
h = values[0];
s = values[1];
v = values[2];
a = current.a;
hSlider.setValue(h);
sSlider.setValue(s);
vSlider.setValue(v);
if(aSlider != null){
aSlider.setValue(a);
}
updateColor(false);
}catch(Exception ignored){
}
}).size(130f, 40f).valid(text -> {
//garbage performance but who cares this runs only every key type anyway
try{
Color.valueOf(text);
return true;
}catch(Exception e){
return false;
}
}).get();
}).grow();
buttons.clear();
addCloseButton();
@@ -60,4 +167,21 @@ public class ColorPicker extends BaseDialog{
hide();
});
}
void updateColor(){
updateColor(true);
}
void updateColor(boolean updateField){
current.fromHsv(h, s, v);
current.a = a;
if(hexField != null && updateField){
String val = current.toString();
if(current.a >= 0.9999f){
val = val.substring(0, 6);
}
hexField.setText(val);
}
}
}

View File

@@ -1,20 +1,32 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.scene.actions.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import mindustry.input.*;
import mindustry.world.meta.*;
import static arc.Core.*;
import static mindustry.Vars.*;
public class ContentInfoDialog extends BaseDialog{
public ContentInfoDialog(){
super("@info.title");
addCloseButton();
keyDown(key -> {
if(key == keybinds.get(Binding.block_info).key){
Core.app.post(this::hide);
}
});
}
public void show(UnlockableContent content){
@@ -27,10 +39,8 @@ public class ContentInfoDialog extends BaseDialog{
content.checkStats();
table.table(title1 -> {
int size = 8 * 6;
title1.image(content.icon(Cicon.xlarge)).size(size).scaling(Scaling.fit);
title1.add("[accent]" + content.localizedName).padLeft(5);
title1.image(content.uiIcon).size(iconXLarge).scaling(Scaling.fit);
title1.add("[accent]" + content.localizedName + (settings.getBool("console") ? "\n[gray]" + content.name : "")).padLeft(5);
});
table.row();
@@ -59,36 +69,43 @@ public class ContentInfoDialog extends BaseDialog{
if(map.size == 0) continue;
//TODO check
if(stats.useCategories){
table.add("@category." + cat.name()).color(Pal.accent).fillX();
table.add("@category." + cat.name).color(Pal.accent).fillX();
table.row();
}
for(Stat stat : map.keys()){
table.table(inset -> {
inset.left();
inset.add("[lightgray]" + stat.localized() + ":[] ").left();
inset.add("[lightgray]" + stat.localized() + ":[] ").left().top();
Seq<StatValue> arr = map.get(stat);
for(StatValue value : arr){
value.display(inset);
inset.add().size(10f);
}
}).fillX().padLeft(10);
table.row();
}
}
if(content.details != null){
table.add("[gray]" + content.details).pad(6).padTop(20).width(400f).wrap().fillX();
table.add("[gray]" + (content.unlocked() || !content.hideDetails ? content.details : Iconc.lock + " " + Core.bundle.get("unlock.incampaign"))).pad(6).padTop(20).width(400f).wrap().fillX();
table.row();
}
content.displayExtra(table);
ScrollPane pane = new ScrollPane(table);
table.marginRight(30f);
//TODO: some things (e.g. reconstructor requirements) are too long and screw up the layout
//pane.setScrollingDisabled(true, false);
cont.add(pane);
show();
if(isShown()){
show(scene, Actions.fadeIn(0f));
}else{
show();
}
}
}

View File

@@ -1,26 +0,0 @@
package mindustry.ui.dialogs;
import arc.input.*;
import arc.scene.ui.*;
import arc.util.*;
import mindustry.gen.*;
import mindustry.graphics.*;
public class ControlsDialog extends KeybindDialog{
public ControlsDialog(){
setFillParent(true);
title.setAlignment(Align.center);
titleTable.row();
titleTable.add(new Image()).growX().height(3f).pad(4f).get().setColor(Pal.accent);
}
@Override
public void addCloseButton(){
buttons.button("@back", Icon.left, this::hide).size(230f, 64f);
keyDown(key -> {
if(key == KeyCode.escape || key == KeyCode.back) hide();
});
}
}

View File

@@ -1,92 +1,16 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.graphics.g2d.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.maps.*;
import mindustry.ui.*;
public class CustomGameDialog extends BaseDialog{
public class CustomGameDialog extends MapListDialog{
private MapPlayDialog dialog = new MapPlayDialog();
public CustomGameDialog(){
super("@customgame");
addCloseButton();
shown(this::setup);
onResize(this::setup);
super("@customgame", false);
}
void setup(){
clearChildren();
add(titleTable);
row();
stack(cont, buttons).grow();
buttons.bottom();
cont.clear();
Table maps = new Table();
maps.marginRight(14);
maps.marginBottom(55f);
ScrollPane pane = new ScrollPane(maps);
pane.setFadeScrollBars(false);
int maxwidth = Math.max((int)(Core.graphics.getWidth() / Scl.scl(210)), 1);
float images = 146f;
int i = 0;
maps.defaults().width(170).fillY().top().pad(4f);
for(Map map : Vars.maps.all()){
if(i % maxwidth == 0){
maps.row();
}
ImageButton image = new ImageButton(new TextureRegion(map.safeTexture()), Styles.cleari);
image.margin(5);
image.top();
Image img = image.getImage();
img.remove();
image.row();
image.table(t -> {
t.left();
for(Gamemode mode : Gamemode.all){
TextureRegionDrawable icon = Vars.ui.getIcon("mode" + Strings.capitalize(mode.name()) + "Small");
if(mode.valid(map) && Core.atlas.isFound(icon.getRegion())){
t.image(icon).size(16f).pad(4f);
}
}
}).left();
image.row();
image.add(map.name()).pad(1f).growX().wrap().left().get().setEllipsis(true);
image.row();
image.image(Tex.whiteui, Pal.gray).growX().pad(3).height(4f);
image.row();
image.add(img).size(images);
BorderImage border = new BorderImage(map.safeTexture(), 3f);
border.setScaling(Scaling.fit);
image.replaceImage(border);
image.clicked(() -> dialog.show(map));
maps.add(image);
i++;
}
if(Vars.maps.all().size == 0){
maps.add("@maps.none").pad(50);
}
cont.add(pane).uniformX();
@Override
void showMap(Map map){
dialog.show(map);
}
}

View File

@@ -3,18 +3,20 @@ package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.graphics.*;
import arc.math.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.ImageButton.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.ctype.*;
import mindustry.editor.BannedContentDialog;
import mindustry.game.*;
import mindustry.game.Rules.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.type.*;
import mindustry.type.Weather.*;
import mindustry.ui.*;
@@ -28,87 +30,84 @@ public class CustomRulesDialog extends BaseDialog{
private Table main;
private Prov<Rules> resetter;
private LoadoutDialog loadoutDialog;
private BaseDialog banDialog;
private BannedContentDialog<Block> bannedBlocks = new BannedContentDialog<>("@bannedblocks", ContentType.block, Block::canBeBuilt);
private BannedContentDialog<UnitType> bannedUnits = new BannedContentDialog<>("@bannedunits", ContentType.unit, u -> !u.isHidden());
public boolean showRuleEditRule;
public Seq<Table> categories;
public Table current;
public Seq<String> categoryNames;
public String currentName = "";
public String ruleSearch = "";
public Seq<Runnable> additionalSetup; // for modding to easily add new rules
public CustomRulesDialog(){
this(false);
}
public CustomRulesDialog(boolean showRuleEditRule){
super("@mode.custom");
this.showRuleEditRule = showRuleEditRule;
loadoutDialog = new LoadoutDialog();
banDialog = new BaseDialog("@bannedblocks");
banDialog.addCloseButton();
banDialog.shown(this::rebuildBanned);
banDialog.buttons.button("@addall", Icon.add, () -> {
rules.bannedBlocks.addAll(content.blocks().select(Block::canBeBuilt));
rebuildBanned();
}).size(180, 64f);
banDialog.buttons.button("@clear", Icon.trash, () -> {
rules.bannedBlocks.clear();
rebuildBanned();
}).size(180, 64f);
setFillParent(true);
shown(this::setup);
addCloseButton();
}
private void rebuildBanned(){
float previousScroll = banDialog.cont.getChildren().isEmpty() ? 0f : ((ScrollPane)banDialog.cont.getChildren().first()).getScrollY();
banDialog.cont.clear();
banDialog.cont.pane(t -> {
t.margin(10f);
additionalSetup = new Seq<>();
categories = new Seq<>();
categoryNames = new Seq<>();
if(rules.bannedBlocks.isEmpty()){
t.add("@empty");
}
buttons.button("@edit", Icon.pencil, () -> {
BaseDialog dialog = new BaseDialog("@waves.edit");
dialog.addCloseButton();
dialog.setFillParent(false);
Seq<Block> array = Seq.with(rules.bannedBlocks);
array.sort();
dialog.cont.table(Tex.button, t -> {
var style = Styles.cleart;
t.defaults().size(280f, 64f).pad(2f);
int cols = mobile && Core.graphics.isPortrait() ? 1 : mobile ? 2 : 3;
int i = 0;
t.button("@waves.copy", Icon.copy, style, () -> {
ui.showInfoFade("@copied");
for(Block block : array){
t.table(Tex.underline, b -> {
b.left().margin(4f);
b.image(block.icon(Cicon.medium)).size(Cicon.medium.size).padRight(3);
b.add(block.localizedName).color(Color.lightGray).padLeft(3).growX().left().wrap();
//hack: don't write the spawns, they just waste space
var spawns = rules.spawns;
rules.spawns = new Seq<>();
Core.app.setClipboardText(JsonIO.write(rules));
rules.spawns = spawns;
dialog.hide();
}).marginLeft(12f).row();
b.button(Icon.cancel, Styles.clearPartiali, () -> {
rules.bannedBlocks.remove(block);
rebuildBanned();
}).size(70f).pad(-4f).padLeft(0f);
}).size(300f, 70f).padRight(5);
if(++i % cols == 0){
t.row();
}
}
}).get().setScrollYForce(previousScroll);
banDialog.cont.row();
banDialog.cont.button("@add", Icon.add, () -> {
BaseDialog dialog = new BaseDialog("@add");
dialog.cont.pane(t -> {
t.left().margin(14f);
int[] i = {0};
content.blocks().each(b -> !rules.bannedBlocks.contains(b) && b.canBeBuilt(), b -> {
int cols = mobile && Core.graphics.isPortrait() ? 4 : 12;
t.button(new TextureRegionDrawable(b.icon(Cicon.medium)), Styles.cleari, () -> {
rules.bannedBlocks.add(b);
rebuildBanned();
dialog.hide();
}).size(60f).get().resizeImage(Cicon.medium.size);
if(++i[0] % cols == 0){
t.row();
t.button("@waves.load", Icon.download, style, () -> {
try{
Rules newRules = JsonIO.read(Rules.class, Core.app.getClipboardText());
//objectives and spawns are considered to be map-specific; don't use them
newRules.spawns = rules.spawns;
newRules.objectives = rules.objectives;
rules = newRules;
refresh();
}catch(Throwable e){
Log.err(e);
ui.showErrorMessage("@rules.invaliddata");
}
});
dialog.hide();
}).disabled(Core.app.getClipboardText() == null || !Core.app.getClipboardText().startsWith("{")).marginLeft(12f).row();
t.button("@settings.reset", Icon.refresh, style, () -> {
rules = resetter.get();
refresh();
}).marginLeft(12f);
});
dialog.addCloseButton();
dialog.show();
}).size(300f, 64f);
});
}
void refresh(){
setup();
requestKeyboard();
requestScroll();
}
public void show(Rules rules, Prov<Rules> resetter){
@@ -118,125 +117,330 @@ public class CustomRulesDialog extends BaseDialog{
}
void setup(){
categories.clear();
cont.clear();
cont.pane(m -> main = m).get().setScrollingDisabled(true, false);
cont.table(t -> {
t.add("@search").padRight(10);
var field = t.field(ruleSearch, text -> {
ruleSearch = text.trim().replaceAll(" +", " ").toLowerCase();
setup();
}).grow().pad(8).get();
field.setCursorPosition(ruleSearch.length());
Core.scene.setKeyboardFocus(field);
t.button(Icon.cancel, Styles.emptyi, () -> {
ruleSearch = "";
setup();
}).padLeft(10f).size(35f);
t.button(Icon.zoom, Styles.emptyi, this::setup).size(54f);
}).row();
cont.pane(m -> main = m).scrollX(false);
main.margin(10f);
main.button("@settings.reset", () -> {
rules = resetter.get();
setup();
requestKeyboard();
requestScroll();
}).size(300f, 50f);
main.left().defaults().fillX().left().pad(5);
main.left().defaults().fillX().left();
main.row();
title("@rules.title.waves");
category("waves");
check("@rules.waves", b -> rules.waves = b, () -> rules.waves);
check("@rules.wavetimer", b -> rules.waveTimer = b, () -> rules.waveTimer);
check("@rules.waitForWaveToEnd", b -> rules.waitEnemies = b, () -> rules.waitEnemies);
number("@rules.wavespacing", false, f -> rules.waveSpacing = f * 60f, () -> rules.waveSpacing / 60f, () -> true, 1, Float.MAX_VALUE);
number("@rules.dropzoneradius", false, f -> rules.dropZoneRadius = f * tilesize, () -> rules.dropZoneRadius / tilesize, () -> true);
check("@rules.wavesending", b -> rules.waveSending = b, () -> rules.waveSending, () -> rules.waves);
check("@rules.wavetimer", b -> rules.waveTimer = b, () -> rules.waveTimer, () -> rules.waves);
check("@rules.waitForWaveToEnd", b -> rules.waitEnemies = b, () -> rules.waitEnemies, () -> rules.waves && rules.waveTimer);
check("@rules.randomwaveai", b -> rules.randomWaveAI = b, () -> rules.randomWaveAI, () -> rules.waves);
check("@rules.airUseSpawns", b -> rules.airUseSpawns = b, () -> rules.airUseSpawns, () -> rules.waves);
numberi("@rules.wavelimit", f -> rules.winWave = f, () -> rules.winWave, () -> rules.waves, 0, Integer.MAX_VALUE);
number("@rules.wavespacing", false, f -> rules.waveSpacing = f * 60f, () -> rules.waveSpacing / 60f, () -> rules.waves && rules.waveTimer, 1, Float.MAX_VALUE);
number("@rules.initialwavespacing", false, f -> rules.initialWaveSpacing = f * 60f, () -> rules.initialWaveSpacing / 60f, () -> rules.waves && rules.waveTimer, 0, Float.MAX_VALUE);
number("@rules.dropzoneradius", false, f -> rules.dropZoneRadius = f * tilesize, () -> rules.dropZoneRadius / tilesize, () -> rules.waves);
title("@rules.title.resourcesbuilding");
category("resourcesbuilding");
check("@rules.alloweditworldprocessors", b -> rules.allowEditWorldProcessors = b, () -> rules.allowEditWorldProcessors);
check("@rules.infiniteresources", b -> rules.infiniteResources = b, () -> rules.infiniteResources);
check("@rules.onlydepositcore", b -> rules.onlyDepositCore = b, () -> rules.onlyDepositCore);
check("@rules.derelictrepair", b -> rules.derelictRepair = b, () -> rules.derelictRepair);
check("@rules.reactorexplosions", b -> rules.reactorExplosions = b, () -> rules.reactorExplosions);
check("@rules.schematic", b -> rules.schematicsAllowed = b, () -> rules.schematicsAllowed);
check("@rules.coreincinerates", b -> rules.coreIncinerates = b, () -> rules.coreIncinerates);
check("@rules.cleanupdeadteams", b -> rules.cleanupDeadTeams = b, () -> rules.cleanupDeadTeams, () -> rules.pvp);
check("@rules.disableworldprocessors", b -> rules.disableWorldProcessors = b, () -> rules.disableWorldProcessors);
number("@rules.buildcostmultiplier", false, f -> rules.buildCostMultiplier = f, () -> rules.buildCostMultiplier, () -> !rules.infiniteResources);
number("@rules.buildspeedmultiplier", f -> rules.buildSpeedMultiplier = f, () -> rules.buildSpeedMultiplier, 0.00001f, 10000f);
number("@rules.buildspeedmultiplier", f -> rules.buildSpeedMultiplier = f, () -> rules.buildSpeedMultiplier, 0.001f, 50f);
number("@rules.deconstructrefundmultiplier", false, f -> rules.deconstructRefundMultiplier = f, () -> rules.deconstructRefundMultiplier, () -> !rules.infiniteResources);
number("@rules.blockhealthmultiplier", f -> rules.blockHealthMultiplier = f, () -> rules.blockHealthMultiplier);
number("@rules.blockdamagemultiplier", f -> rules.blockDamageMultiplier = f, () -> rules.blockDamageMultiplier);
main.button("@configure",
() -> loadoutDialog.show(Blocks.coreShard.itemCapacity, rules.loadout,
i -> true,
() -> rules.loadout.clear().add(new ItemStack(Items.copper, 100)),
() -> {}, () -> {}
)).left().width(300f);
main.row();
if(Core.bundle.get("configure").toLowerCase().contains(ruleSearch)){
current.button("@configure",
() -> loadoutDialog.show(999999, rules.loadout,
i -> true,
() -> rules.loadout.clear().add(new ItemStack(Items.copper, 100)),
() -> {}, () -> {}
)).left().width(300f).row();
}
main.button("@bannedblocks", banDialog::show).left().width(300f);
main.row();
if(Core.bundle.get("bannedblocks").toLowerCase().contains(ruleSearch)){
current.button("@bannedblocks", () -> bannedBlocks.show(rules.bannedBlocks)).left().width(300f).row();
}
check("@rules.hidebannedblocks", b -> rules.hideBannedBlocks = b, () -> rules.hideBannedBlocks);
check("@bannedblocks.whitelist", b -> rules.blockWhitelist = b, () -> rules.blockWhitelist);
title("@rules.title.unit");
check("@rules.unitammo", b -> rules.unitAmmo = b, () -> rules.unitAmmo);
number("@rules.unithealthmultiplier", f -> rules.unitHealthMultiplier = f, () -> rules.unitHealthMultiplier);
category("unit");
check("@rules.unitcapvariable", b -> rules.unitCapVariable = b, () -> rules.unitCapVariable);
check("@rules.unitpayloadsexplode", b -> rules.unitPayloadsExplode = b, () -> rules.unitPayloadsExplode);
numberi("@rules.unitcap", f -> rules.unitCap = f, () -> rules.unitCap, -999, 999);
number("@rules.unitdamagemultiplier", f -> rules.unitDamageMultiplier = f, () -> rules.unitDamageMultiplier);
number("@rules.unitbuildspeedmultiplier", f -> rules.unitBuildSpeedMultiplier = f, () -> rules.unitBuildSpeedMultiplier, 0.00001f, 100f);
number("@rules.unitcrashdamagemultiplier", f -> rules.unitCrashDamageMultiplier = f, () -> rules.unitCrashDamageMultiplier);
number("@rules.unitminespeedmultiplier", f -> rules.unitMineSpeedMultiplier = f, () -> rules.unitMineSpeedMultiplier);
number("@rules.unitbuildspeedmultiplier", f -> rules.unitBuildSpeedMultiplier = f, () -> rules.unitBuildSpeedMultiplier, 0f, 50f);
number("@rules.unitcostmultiplier", f -> rules.unitCostMultiplier = f, () -> rules.unitCostMultiplier);
number("@rules.unithealthmultiplier", f -> rules.unitHealthMultiplier = f, () -> rules.unitHealthMultiplier);
title("@rules.title.enemy");
if(Core.bundle.get("bannedunits").toLowerCase().contains(ruleSearch)){
current.button("@bannedunits", () -> bannedUnits.show(rules.bannedUnits)).left().width(300f).row();
}
check("@bannedunits.whitelist", b -> rules.unitWhitelist = b, () -> rules.unitWhitelist);
category("enemy");
check("@rules.attack", b -> rules.attackMode = b, () -> rules.attackMode);
check("@rules.buildai", b -> rules.teams.get(rules.waveTeam).ai = rules.teams.get(rules.waveTeam).infiniteResources = b, () -> rules.teams.get(rules.waveTeam).ai);
number("@rules.enemycorebuildradius", f -> rules.enemyCoreBuildRadius = f * tilesize, () -> Math.min(rules.enemyCoreBuildRadius / tilesize, 200));
check("@rules.corecapture", b -> rules.coreCapture = b, () -> rules.coreCapture);
check("@rules.placerangecheck", b -> rules.placeRangeCheck = b, () -> rules.placeRangeCheck);
check("@rules.polygoncoreprotection", b -> rules.polygonCoreProtection = b, () -> rules.polygonCoreProtection);
number("@rules.enemycorebuildradius", f -> rules.enemyCoreBuildRadius = f * tilesize, () -> Math.min(rules.enemyCoreBuildRadius / tilesize, 200), () -> !rules.polygonCoreProtection);
title("@rules.title.environment");
category("environment");
check("@rules.explosions", b -> rules.damageExplosions = b, () -> rules.damageExplosions);
check("@rules.fire", b -> rules.fire = b, () -> rules.fire);
check("@rules.fog", b -> rules.fog = b, () -> rules.fog);
check("@rules.lighting", b -> rules.lighting = b, () -> rules.lighting);
check("@rules.enemyLights", b -> rules.enemyLights = b, () -> rules.enemyLights);
main.button(b -> {
b.left();
b.table(Tex.pane, in -> {
in.stack(new Image(Tex.alphaBg), new Image(Tex.whiteui){{
update(() -> setColor(rules.ambientLight));
}}).grow();
}).margin(4).size(50f).padRight(10);
b.add("@rules.ambientlight");
}, () -> ui.picker.show(rules.ambientLight, rules.ambientLight::set)).left().width(250f).row();
check("@rules.limitarea", b -> rules.limitMapArea = b, () -> rules.limitMapArea);
numberi("x", x -> rules.limitX = x, () -> rules.limitX, () -> rules.limitMapArea, 0, 10000);
numberi("y", y -> rules.limitY = y, () -> rules.limitY, () -> rules.limitMapArea, 0, 10000);
numberi("w", w -> rules.limitWidth = w, () -> rules.limitWidth, () -> rules.limitMapArea, 0, 10000);
numberi("h", h -> rules.limitHeight = h, () -> rules.limitHeight, () -> rules.limitMapArea, 0, 10000);
main.button("@rules.weather", this::weatherDialog).width(250f).left().row();
number("@rules.solarmultiplier", f -> rules.solarMultiplier = f, () -> rules.solarMultiplier);
if(Core.bundle.get("rules.ambientlight").toLowerCase().contains(ruleSearch)){
current.button(b -> {
b.left();
b.table(Tex.pane, in -> {
in.stack(new Image(Tex.alphaBg), new Image(Tex.whiteui){{
update(() -> setColor(rules.ambientLight));
}}).grow();
}).margin(4).size(50f).padRight(10);
b.add("@rules.ambientlight");
}, () -> ui.picker.show(rules.ambientLight, rules.ambientLight::set)).left().width(250f).row();
}
if(Core.bundle.get("rules.weather").toLowerCase().contains(ruleSearch)){
current.button("@rules.weather", this::weatherDialog).width(250f).left().row();
}
category("planet");
if(Core.bundle.get("rules.title.planet").toLowerCase().contains(ruleSearch)){
current.table(Tex.button, t -> {
t.margin(10f);
var group = new ButtonGroup<>();
var style = Styles.flatTogglet;
t.defaults().size(140f, 50f);
for(Planet planet : content.planets().select(p -> p.accessible && p.visible && p.isLandable())){
t.button(planet.localizedName, style, () -> {
planet.applyRules(rules, true);
}).group(group).checked(b -> rules.planet == planet);
if(t.getChildren().size % 3 == 0){
t.row();
}
}
t.button("@rules.anyenv", style, () -> {
rules.env = Vars.defaultEnv;
rules.planet = Planets.sun;
}).group(group).checked(b -> rules.planet == Planets.sun);
}).left().fill(false).expand(false, false).row();
}
category("teams");
//not sure where else to put this
if(showRuleEditRule){
check("@rules.allowedit", b -> rules.allowEditRules = b, () -> rules.allowEditRules);
}
team("@rules.playerteam", t -> rules.defaultTeam = t, () -> rules.defaultTeam);
team("@rules.enemyteam", t -> rules.waveTeam = t, () -> rules.waveTeam);
for(Team team : Team.baseTeams){
boolean[] shown = {false};
Table wasCurrent = current;
Table teamRules = new Table(); // just button and collapser in one table
teamRules.button(team.coloredName(), Icon.downOpen, Styles.togglet, () -> {
shown[0] = !shown[0];
}).marginLeft(14f).width(260f).height(55f).update(t -> {
((Image)t.getChildren().get(1)).setDrawable(shown[0] ? Icon.upOpen : Icon.downOpen);
t.setChecked(shown[0]);
}).left().padBottom(2f).row();
teamRules.collapser(c -> {
c.left().defaults().fillX().left().pad(5);
current = c;
TeamRule teams = rules.teams.get(team);
number("@rules.blockhealthmultiplier", f -> teams.blockHealthMultiplier = f, () -> teams.blockHealthMultiplier);
number("@rules.blockdamagemultiplier", f -> teams.blockDamageMultiplier = f, () -> teams.blockDamageMultiplier);
check("@rules.rtsai", b -> teams.rtsAi = b, () -> teams.rtsAi, () -> team != rules.defaultTeam);
numberi("@rules.rtsminsquadsize", f -> teams.rtsMinSquad = f, () -> teams.rtsMinSquad, () -> teams.rtsAi, 0, 100);
numberi("@rules.rtsmaxsquadsize", f -> teams.rtsMaxSquad = f, () -> teams.rtsMaxSquad, () -> teams.rtsAi, 1, 1000);
number("@rules.rtsminattackweight", f -> teams.rtsMinWeight = f, () -> teams.rtsMinWeight, () -> teams.rtsAi);
//disallow on Erekir (this is broken for mods I'm sure, but whatever)
check("@rules.buildai", b -> teams.buildAi = b, () -> teams.buildAi, () -> team != rules.defaultTeam && rules.env != Planets.erekir.defaultEnv && !rules.pvp);
number("@rules.buildaitier", false, f -> teams.buildAiTier = f, () -> teams.buildAiTier, () -> teams.buildAi && rules.env != Planets.erekir.defaultEnv && !rules.pvp, 0, 1);
number("@rules.extracorebuildradius", f -> teams.extraCoreBuildRadius = f * tilesize, () -> Math.min(teams.extraCoreBuildRadius / tilesize, 200), () -> !rules.polygonCoreProtection);
check("@rules.infiniteresources", b -> teams.infiniteResources = b, () -> teams.infiniteResources);
number("@rules.buildspeedmultiplier", f -> teams.buildSpeedMultiplier = f, () -> teams.buildSpeedMultiplier, 0.001f, 50f);
number("@rules.unitdamagemultiplier", f -> teams.unitDamageMultiplier = f, () -> teams.unitDamageMultiplier);
number("@rules.unitcrashdamagemultiplier", f -> teams.unitCrashDamageMultiplier = f, () -> teams.unitCrashDamageMultiplier);
number("@rules.unitminespeedmultiplier", f -> teams.unitMineSpeedMultiplier = f, () -> teams.unitMineSpeedMultiplier);
number("@rules.unitbuildspeedmultiplier", f -> teams.unitBuildSpeedMultiplier = f, () -> teams.unitBuildSpeedMultiplier, 0.001f, 50f);
number("@rules.unitcostmultiplier", f -> teams.unitCostMultiplier = f, () -> teams.unitCostMultiplier);
number("@rules.unithealthmultiplier", f -> teams.unitHealthMultiplier = f, () -> teams.unitHealthMultiplier);
if(!current.hasChildren()){
teamRules.clear();
}else{
wasCurrent.add(teamRules).row();
}
current = wasCurrent;
}, () -> shown[0]).left().growX().row();
}
additionalSetup.each(Runnable::run);
for(var i = 0; i < categories.size; i++){
addToMain(categories.get(i), Core.bundle.get("rules.title." + categoryNames.get(i)));
}
}
void number(String text, Floatc cons, Floatp prov){
public void category(String name){
current = new Table();
current.left().defaults().fillX().left().pad(5);
currentName = name;
categories.add(current);
categoryNames.add(currentName);
}
void addToMain(Table category, String title){
if(category.hasChildren()){
main.add(title).color(Pal.accent).padTop(20).padRight(100f).padBottom(-3).fillX().left().pad(5).row();
main.image().color(Pal.accent).height(3f).padRight(100f).padBottom(20).fillX().left().pad(5).row();
main.add(category).row();
}
}
public void team(String text, Cons<Team> cons, Prov<Team> prov){
if(!Core.bundle.get(text.substring(1)).toLowerCase().contains(ruleSearch)) return;
current.table(t -> {
t.left();
t.add(text).left().padRight(5);
for(Team team : Team.baseTeams){
t.button(Tex.whiteui, Styles.squareTogglei, 38f, () -> {
cons.get(team);
}).pad(1f).checked(b -> prov.get() == team).size(60f).tooltip(team.coloredName()).with(i -> i.getStyle().imageUpColor = team.color);
}
}).padTop(0).row();
}
public void number(String text, Floatc cons, Floatp prov){
number(text, false, cons, prov, () -> true, 0, Float.MAX_VALUE);
}
void number(String text, Floatc cons, Floatp prov, float min, float max){
public void number(String text, Floatc cons, Floatp prov, float min, float max){
number(text, false, cons, prov, () -> true, min, max);
}
void number(String text, boolean integer, Floatc cons, Floatp prov, Boolp condition){
public void number(String text, boolean integer, Floatc cons, Floatp prov, Boolp condition){
number(text, integer, cons, prov, condition, 0, Float.MAX_VALUE);
}
void number(String text, Floatc cons, Floatp prov, Boolp condition){
public void number(String text, Floatc cons, Floatp prov, Boolp condition){
number(text, false, cons, prov, condition, 0, Float.MAX_VALUE);
}
void number(String text, boolean integer, Floatc cons, Floatp prov, Boolp condition, float min, float max){
main.table(t -> {
public void numberi(String text, Intc cons, Intp prov, int min, int max){
numberi(text, cons, prov, () -> true, min, max);
}
public void numberi(String text, Intc cons, Intp prov, Boolp condition, int min, int max){
if(!Core.bundle.get(text.substring(1)).toLowerCase().contains(ruleSearch)) return;
var cell = current.table(t -> {
t.left();
t.add(text).left().padRight(5)
.update(a -> a.setColor(condition.get() ? Color.white : Color.gray));
t.field((prov.get()) + "", s -> cons.get(Strings.parseInt(s)))
.update(a -> a.setDisabled(!condition.get()))
.padRight(100f)
.valid(f -> Strings.parseInt(f) >= min && Strings.parseInt(f) <= max).width(120f).left();
}).padTop(0);
ruleInfo(cell, text);
current.row();
}
public void number(String text, boolean integer, Floatc cons, Floatp prov, Boolp condition, float min, float max){
if(!Core.bundle.get(text.substring(1)).toLowerCase().contains(ruleSearch)) return;
var cell = current.table(t -> {
t.left();
t.add(text).left().padRight(5)
.update(a -> a.setColor(condition.get() ? Color.white : Color.gray));
t.field((integer ? (int)prov.get() : prov.get()) + "", s -> cons.get(Strings.parseFloat(s)))
.padRight(100f)
.update(a -> a.setDisabled(!condition.get()))
.valid(f -> Strings.canParsePositiveFloat(f) && Strings.parseFloat(f) >= min && Strings.parseFloat(f) <= max).width(120f).left().addInputDialog();
.valid(f -> Strings.canParsePositiveFloat(f) && Strings.parseFloat(f) >= min && Strings.parseFloat(f) <= max).width(120f).left();
}).padTop(0);
main.row();
ruleInfo(cell, text);
current.row();
}
void check(String text, Boolc cons, Boolp prov){
public void check(String text, Boolc cons, Boolp prov){
check(text, cons, prov, () -> true);
}
void check(String text, Boolc cons, Boolp prov, Boolp condition){
main.check(text, cons).checked(prov.get()).update(a -> a.setDisabled(!condition.get())).padRight(100f).get().left();
main.row();
public void check(String text, Boolc cons, Boolp prov, Boolp condition){
if(!Core.bundle.get(text.substring(1)).toLowerCase().contains(ruleSearch)) return;
var cell = current.check(text, cons).checked(prov.get()).update(a -> a.setDisabled(!condition.get()));
cell.get().left();
ruleInfo(cell, text);
current.row();
}
void title(String text){
main.add(text).color(Pal.accent).padTop(20).padRight(100f).padBottom(-3);
main.row();
main.image().color(Pal.accent).height(3f).padRight(100f).padBottom(20);
main.row();
public void ruleInfo(Cell<?> cell, String text){
if(Core.bundle.has(text.substring(1) + ".info")){
if(mobile){
Table table = new Table();
table.add(cell.get()).left().expandX().fillX();
cell.clearElement();
table.button(Icon.infoSmall, () -> ui.showInfo(text + ".info")).size(32f).padRight(24f).right();
cell.setElement(table).left().expandX().fillX();
}else{
cell.tooltip(text + ".info");
}
}
}
Cell<TextField> field(Table table, float value, Floatc setter){
return table.field(Strings.autoFixed(value, 2), v -> setter.get(Strings.parseFloat(v)))
.valid(Strings::canParsePositiveFloat)
.size(90f, 40f).pad(2f).addInputDialog();
.size(90f, 40f).pad(2f);
}
void weatherDialog(){
@@ -247,7 +451,7 @@ public class CustomRulesDialog extends BaseDialog{
rebuild[0] = () -> {
base.clearChildren();
int cols = Math.max(1, Core.graphics.getWidth() / 460);
int cols = Math.max(1, (int)(Core.graphics.getWidth() / Scl.scl(450)));
int idx = 0;
for(WeatherEntry entry : rules.weather){
@@ -323,8 +527,9 @@ public class CustomRulesDialog extends BaseDialog{
t.background(Tex.button);
int i = 0;
for(Weather weather : content.<Weather>getBy(ContentType.weather)){
if(weather.hidden) continue;
t.button(weather.localizedName, Styles.cleart, () -> {
t.button(weather.localizedName, Styles.flatt, () -> {
rules.weather.add(new WeatherEntry(weather));
rebuild[0].run();
@@ -337,19 +542,6 @@ public class CustomRulesDialog extends BaseDialog{
add.show();
}).width(170f);
//reset cooldown to random number
dialog.hidden(() -> {
float sum = 0;
Seq<WeatherEntry> sh = rules.weather.copy();
sh.shuffle();
for(WeatherEntry w : sh){
//add the previous cooldowns to the sum so weather events are staggered and don't happen all at once.
w.cooldown = sum + Mathf.random(w.minFrequency, w.maxFrequency);
sum += w.cooldown;
}
});
dialog.show();
}
}

View File

@@ -5,64 +5,137 @@ import arc.graphics.*;
import arc.input.*;
import arc.math.*;
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.ctype.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.world.*;
import static arc.Core.*;
import static mindustry.Vars.*;
public class DatabaseDialog extends BaseDialog{
private TextField search;
private Table all = new Table();
private @Nullable Seq<UnlockableContent> allTabs;
//sun means "all content"
private UnlockableContent tab = Planets.sun;
public DatabaseDialog(){
super("@database");
shouldPause = true;
addCloseButton();
shown(this::rebuild);
shown(() -> {
checkTabList();
if(state.isCampaign() && allTabs.contains(state.getPlanet())){
tab = state.getPlanet();
}else if(state.isGame() && state.rules.planet != null && allTabs.contains(state.rules.planet)){
tab = state.rules.planet;
}
rebuild();
});
onResize(this::rebuild);
all.margin(20).marginTop(0f).marginRight(30f);
cont.top();
cont.table(s -> {
s.image(Icon.zoom).padRight(8);
search = s.field(null, text -> rebuild()).growX().get();
search.setMessageText("@players.search");
}).fillX().padBottom(4).row();
cont.pane(all).scrollX(false);
}
void checkTabList(){
if(allTabs == null){
Seq<Content>[] allContent = Vars.content.getContentMap();
ObjectSet<UnlockableContent> all = new ObjectSet<>();
for(var contents : allContent){
for(var content : contents){
if(content instanceof UnlockableContent u){
all.addAll(u.databaseTabs);
}
}
}
allTabs = all.toSeq().sort();
allTabs.insert(0, Planets.sun);
}
}
void rebuild(){
cont.clear();
checkTabList();
Table table = new Table();
table.margin(20);
ScrollPane pane = new ScrollPane(table);
all.clear();
var text = search.getText().toLowerCase();
Seq<Content>[] allContent = Vars.content.getContentMap();
all.table(t -> {
int i = 0;
for(var content : allTabs){
t.button(content == Planets.sun ? Icon.eyeSmall : content instanceof Planet p ? Icon.icons.get(p.icon, Icon.commandRally) : new TextureRegionDrawable(content.uiIcon), Styles.clearNoneTogglei, iconMed, () -> {
tab = content;
rebuild();
}).size(50f).checked(b -> tab == content).tooltip(content == Planets.sun ? "@all" : content.localizedName).with(but -> {
but.getStyle().imageUpColor = content instanceof Planet p ? p.iconColor : Color.white.cpy();
});
if(++i % 10 == 0) t.row();
}
}).row();;
for(int j = 0; j < allContent.length; j++){
ContentType type = ContentType.all[j];
Seq<Content> array = allContent[j].select(c -> c instanceof UnlockableContent u && (!u.isHidden() || u.node() != null));
Seq<UnlockableContent> array = allContent[j]
.select(c -> c instanceof UnlockableContent u && !u.isHidden() && !u.hideDatabase && (tab == Planets.sun || u.allDatabaseTabs || u.databaseTabs.contains(tab)) &&
(text.isEmpty() || u.localizedName.toLowerCase().contains(text))).as();
if(array.size == 0) continue;
table.add("@content." + type.name() + ".name").growX().left().color(Pal.accent);
table.row();
table.image().growX().pad(5).padLeft(0).padRight(0).height(3).color(Pal.accent);
table.row();
table.table(list -> {
all.add("@content." + type.name() + ".name").growX().left().color(Pal.accent);
all.row();
all.image().growX().pad(5).padLeft(0).padRight(0).height(3).color(Pal.accent);
all.row();
all.table(list -> {
list.left();
int cols = Mathf.clamp((Core.graphics.getWidth() - 30) / (32 + 10), 1, 18);
int cols = (int)Mathf.clamp((Core.graphics.getWidth() - Scl.scl(30)) / Scl.scl(32 + 12), 1, 22);
int count = 0;
for(int i = 0; i < array.size; i++){
UnlockableContent unlock = (UnlockableContent)array.get(i);
UnlockableContent unlock = array.get(i);
Image image = unlocked(unlock) ? new Image(unlock.uiIcon).setScaling(Scaling.fit) : new Image(Icon.lock, Pal.gray);
//banned cross
if(state.isGame() && (unlock instanceof UnitType u && u.isBanned() || unlock instanceof Block b && state.rules.isBanned(b))){
list.stack(image, new Image(Icon.cancel){{
setColor(Color.scarlet);
touchable = Touchable.disabled;
}}).size(8 * 4).pad(3);
}else{
list.add(image).size(8 * 4).pad(3);
}
Image image = unlocked(unlock) ? new Image(unlock.icon(Cicon.medium)).setScaling(Scaling.fit) : new Image(Icon.lock, Pal.gray);
list.add(image).size(8 * 4).pad(3);
ClickListener listener = new ClickListener();
image.addListener(listener);
if(!Vars.mobile && unlocked(unlock)){
if(!mobile && unlocked(unlock)){
image.addListener(new HandCursorListener());
image.update(() -> image.color.lerp(!listener.isOver() ? Color.lightGray : Color.white, 0.4f * Time.delta));
image.update(() -> image.color.lerp(!listener.isOver() ? Color.lightGray : Color.white, Mathf.clamp(0.4f * Time.delta)));
}
if(unlocked(unlock)){
@@ -71,10 +144,10 @@ public class DatabaseDialog extends BaseDialog{
Core.app.setClipboardText((char)Fonts.getUnicode(unlock.name) + "");
ui.showInfoFade("@copied");
}else{
Vars.ui.content.show(unlock);
ui.content.show(unlock);
}
});
image.addListener(new Tooltip(t -> t.background(Tex.button).add(unlock.localizedName)));
image.addListener(new Tooltip(t -> t.background(Tex.button).add(unlock.localizedName + (settings.getBool("console") ? "\n[gray]" + unlock.name : ""))));
}
if((++count) % cols == 0){
@@ -82,10 +155,12 @@ public class DatabaseDialog extends BaseDialog{
}
}
}).growX().left().padBottom(10);
table.row();
all.row();
}
cont.add(pane);
if(all.getChildren().isEmpty()){
all.add("@none.found");
}
}
boolean unlocked(UnlockableContent content){

View File

@@ -29,20 +29,20 @@ public class DiscordDialog extends Dialog{
}).expandY();
t.table(i -> {
i.background(Tex.button);
i.image(Icon.discord);
}).size(h).left();
t.add("@discord").color(Pal.accent).growX().padLeft(10f);
}).size(440f, h).pad(10f);
}).size(520f, h).pad(10f);
buttons.defaults().size(150f, 50);
buttons.defaults().size(170f, 50);
buttons.button("@back", this::hide);
buttons.button("@copylink", () -> {
buttons.button("@back", Icon.left, this::hide);
buttons.button("@copylink", Icon.copy, () -> {
Core.app.setClipboardText(discordURL);
ui.showInfoFade("@copied");
});
buttons.button("@openlink", () -> {
buttons.button("@openlink", Icon.discord, () -> {
if(!Core.app.openURI(discordURL)){
ui.showErrorMessage("@linkfail");
Core.app.setClipboardText(discordURL);

View File

@@ -8,48 +8,26 @@ import arc.util.*;
import mindustry.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.maps.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class MapsDialog extends BaseDialog{
private BaseDialog dialog;
public class EditorMapsDialog extends MapListDialog{
public MapsDialog(){
super("@maps");
buttons.remove();
addCloseListener();
shown(this::setup);
onResize(() -> {
if(dialog != null){
dialog.hide();
}
setup();
});
public EditorMapsDialog(){
super("@maps", true);
}
void setup(){
buttons.clearChildren();
if(Core.graphics.isPortrait()){
buttons.button("@back", Icon.left, this::hide).size(210f*2f, 64f).colspan(2);
buttons.row();
}else{
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
}
@Override
void buildButtons(){
buttons.button("@editor.newmap", Icon.add, () -> {
ui.showTextInput("@editor.newmap", "@editor.mapname", "", text -> {
Runnable show = () -> ui.loadAnd(() -> {
hide();
ui.editor.show();
ui.editor.editor.tags.put("name", text);
editor.tags.put("name", text);
Events.fire(new MapMakeEvent());
});
@@ -72,12 +50,11 @@ public class MapsDialog extends BaseDialog{
Map map = MapIO.createMap(file, true);
//when you attempt to import a save, it will have no name, so generate one
String name = map.tags.get("name", () -> {
String result = "unknown";
int number = 0;
while(maps.byName(result + number++) != null);
while(maps.byName(result + number++) != null) ;
return result + number;
});
@@ -108,51 +85,11 @@ public class MapsDialog extends BaseDialog{
});
});
}).size(210f, 64f);
cont.clear();
Table maps = new Table();
maps.marginRight(24);
ScrollPane pane = new ScrollPane(maps);
pane.setFadeScrollBars(false);
int maxwidth = Math.max((int)(Core.graphics.getWidth() / Scl.scl(230)), 1);
float mapsize = 200f;
int i = 0;
for(Map map : Vars.maps.all()){
if(i % maxwidth == 0){
maps.row();
}
TextButton button = maps.button("", Styles.cleart, () -> showMapInfo(map)).width(mapsize).pad(8).get();
button.clearChildren();
button.margin(9);
button.add(map.name()).width(mapsize - 18f).center().get().setEllipsis(true);
button.row();
button.image().growX().pad(4).color(Pal.gray);
button.row();
button.stack(new Image(map.safeTexture()).setScaling(Scaling.fit), new BorderImage(map.safeTexture()).setScaling(Scaling.fit)).size(mapsize - 20f);
button.row();
button.add(map.custom ? "@custom" : map.workshop ? "@workshop" : map.mod != null ? "[lightgray]" + map.mod.meta.displayName() : "@builtin").color(Color.gray).padTop(3);
i++;
}
if(Vars.maps.all().size == 0){
maps.add("@maps.none");
}
cont.add(buttons).growX();
cont.row();
cont.add(pane).uniformX();
}
void showMapInfo(Map map){
dialog = new BaseDialog("@editor.mapinfo");
@Override
void showMap(Map map){
BaseDialog dialog = activeDialog = new BaseDialog("@editor.mapinfo");
dialog.addCloseButton();
float mapsize = Core.graphics.isPortrait() ? 160f : 300f;
@@ -195,7 +132,7 @@ public class MapsDialog extends BaseDialog{
dialog.hide();
hide();
}catch(Exception e){
e.printStackTrace();
Log.err(e);
ui.showErrorMessage("@error.mapnotfound");
}
}).fillX().height(54f).marginLeft(10);
@@ -214,4 +151,5 @@ public class MapsDialog extends BaseDialog{
dialog.show();
}
}

View File

@@ -0,0 +1,235 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.scene.*;
import arc.scene.event.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.gen.*;
import mindustry.logic.*;
import mindustry.logic.LogicFx.*;
import mindustry.ui.*;
import mindustry.world.*;
public class EffectsDialog extends BaseDialog{
static BoundsBatch bounds = new BoundsBatch();
Iterable<EffectEntry> entries;
@Nullable Cons<EffectEntry> listener;
public EffectsDialog(Iterable<EffectEntry> entries){
super("Effects");
this.entries = entries;
addCloseButton();
makeButtonOverlay();
onResize(this::setup);
shown(this::setup);
setup();
}
public EffectsDialog(){
this(LogicFx.entries());
}
public static EffectsDialog withAllEffects(){
return new EffectsDialog(Seq.select(Fx.class.getFields(), f -> f.getType() == Effect.class).map(f -> new EffectEntry(Reflect.get(f)).name(f.getName())));
}
public Dialog show(Cons<EffectEntry> listener){
this.listener = listener;
return super.show();
}
@Override
public Dialog show(){
this.listener = null;
return super.show();
}
void setup(){
float size = 280f;
int cols = (int)Math.max(1, Core.graphics.getWidth() / Scl.scl(size + 12f));
cont.clearChildren();
cont.pane(t -> {
int i = 0;
for(var entry : entries){
float bounds = calculateSize(entry);
if(bounds <= 0) continue;
ClickListener cl = new ClickListener();
t.stack(
new EffectCell(entry, cl),
new Table(af -> af.add(entry.name).grow().labelAlign(Align.bottomLeft).style(Styles.outlineLabel).bottom().left())
).size(size).with(a -> {
a.clicked(() -> {
if(listener != null){
listener.get(entry);
hide();
}
});
a.addListener(cl);
a.addListener(new HandCursorListener(() -> listener != null, true));
});
if(++i % cols == 0) t.row();
}
}).grow().scrollX(false);
}
static Object getData(Class<?> type){
if(type == Block.class) return Blocks.router;
return null;
}
static float calculateSize(EffectEntry entry){
if(entry.bounds >= 0) return entry.bounds;
var effect = entry.effect;
try{
effect.init();
Batch prev = Core.batch;
bounds.reset();
Core.batch = bounds;
Object data = getData(entry.data);
float lifetime = effect.lifetime;
float rot = 1f;
int steps = 60;
int seeds = 4;
for(int s = 0; s < seeds; s++){
for(int i = 0; i <= steps; i++){
effect.render(1, Color.white, i / (float)steps * lifetime, lifetime, rot, 0f, 0f, data);
}
}
Core.batch = prev;
return entry.bounds = bounds.max * 2f;
}catch(Exception e){
//might crash with invalid data
return -1f;
}
}
static class BoundsBatch extends Batch{
float max;
void reset(){
max = 0f;
}
void max(float... xs){
for(float f : xs){
if(Float.isNaN(f)) continue;
max = Math.max(max, Math.abs(f));
}
}
@Override
protected void draw(Texture texture, float[] spriteVertices, int offset, int count){
for(int i = offset; i < count; i += SpriteBatch.VERTEX_SIZE){
max(spriteVertices[i], spriteVertices[i + 1]);
}
}
@Override
protected void draw(TextureRegion region, float x, float y, float originX, float originY, float width, float height, float rotation){
float worldOriginX = x + originX;
float worldOriginY = y + originY;
float fx = -originX;
float fy = -originY;
float fx2 = width - originX;
float fy2 = height - originY;
float cos = Mathf.cosDeg(rotation);
float sin = Mathf.sinDeg(rotation);
float x1 = cos * fx - sin * fy + worldOriginX;
float y1 = sin * fx + cos * fy + worldOriginY;
float x2 = cos * fx - sin * fy2 + worldOriginX;
float y2 = sin * fx + cos * fy2 + worldOriginY;
float x3 = cos * fx2 - sin * fy2 + worldOriginX;
float y3 = sin * fx2 + cos * fy2 + worldOriginY;
max(x1, y1, x2, y2, x3, y3, x1 + (x3 - x2), y3 - (y2 - y1));
}
@Override
protected void flush(){}
}
class EffectCell extends Element{
EffectEntry effect;
float size = -1f;
int id = 1;
float time = 0f;
float lifetime;
float rotation = 1f;
Object data;
ClickListener cl;
public EffectCell(EffectEntry effect, ClickListener cl){
this.effect = effect;
this.lifetime = effect.effect.lifetime;
this.cl = cl;
data = getData(effect.data);
}
@Override
public void draw(){
if(size < 0){
size = calculateSize(effect) + 1f;
}
color.fromHsv((Time.globalTime * 2f) % 360f, 1f, 1f);
if(clipBegin(x, y, width, height)){
Draw.colorl(cl.isOver() && listener != null ? 0.4f : 0.5f);
Draw.alpha(parentAlpha);
Tex.alphaBg.draw(x, y, width, height);
Draw.reset();
Draw.flush();
float scale = width / size;
Tmp.m1.set(Draw.trans());
Draw.trans().translate(x + width/2f, y + height/2f).scale(scale, scale);
Draw.flush();
this.lifetime = effect.effect.render(id, color, time, lifetime, rotation, 0f, 0f, data);
Draw.flush();
Draw.trans().set(Tmp.m1);
clipEnd();
}
Lines.stroke(Scl.scl(3f), Color.black);
Lines.rect(x, y, width, height);
Draw.reset();
}
@Override
public void act(float delta){
super.act(delta);
time += Time.delta;
if(time >= lifetime){
id ++;
}
time %= lifetime;
}
}
}

View File

@@ -20,8 +20,8 @@ public class FileChooser extends BaseDialog{
private static final Fi homeDirectory = Core.files.absolute(Core.files.getExternalStoragePath());
static Fi lastDirectory = Core.files.absolute(Core.settings.getString("lastDirectory", homeDirectory.absolutePath()));
Fi directory;
private Table files;
Fi directory = lastDirectory;
private ScrollPane pane;
private TextField navigation, filefield;
private TextButton ok;
@@ -37,6 +37,8 @@ public class FileChooser extends BaseDialog{
this.filter = filter;
this.selectListener = result;
directory = getLastDirectory();
onResize(() -> {
cont.clear();
setupWidgets();
@@ -191,7 +193,7 @@ public class FileChooser extends BaseDialog{
Fi[] names = getFileNames();
Image upimage = new Image(Icon.upOpen);
TextButton upbutton = new TextButton(".." + directory.toString(), Styles.clearTogglet);
TextButton upbutton = new TextButton(".." + directory.toString(), Styles.flatTogglet);
upbutton.clicked(() -> {
directory = directory.parent();
setLastDirectory(directory);
@@ -213,7 +215,7 @@ public class FileChooser extends BaseDialog{
String filename = file.name();
TextButton button = new TextButton(filename, Styles.clearTogglet);
TextButton button = new TextButton(filename.replace("[", "[["), Styles.flatTogglet);
button.getLabel().setWrap(false);
button.getLabel().setEllipsis(true);
group.add(button);
@@ -249,18 +251,16 @@ public class FileChooser extends BaseDialog{
if(open) filefield.clearText();
}
public static void setLastDirectory(Fi directory){
public static synchronized void setLastDirectory(Fi directory){
lastDirectory = directory;
Core.settings.put("lastDirectory", directory.absolutePath());
}
private String shorten(String string){
int max = 30;
if(string.length() <= max){
return string;
}else{
return string.substring(0, max - 3).concat("...");
public static synchronized Fi getLastDirectory(){
if(!lastDirectory.exists()){
lastDirectory = homeDirectory;
}
return lastDirectory;
}
public class FileHistory{
@@ -300,19 +300,5 @@ public class FileChooser extends BaseDialog{
public boolean canBack(){
return !(index == 1) && index > 0;
}
void print(){
System.out.println("\n\n\n\n\n\n");
int i = 0;
for(Fi file : history){
i++;
if(index == i){
System.out.println("[[" + file.toString() + "]]");
}else{
System.out.println("--" + file.toString() + "--");
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
package mindustry.ui.dialogs;
import arc.util.*;
public class FullTextDialog extends BaseDialog{
public FullTextDialog(){
super("");
shouldPause = true;
addCloseButton();
}
public void show(String titleText, String text){
title.setText(titleText);
cont.clear();
cont.add(text).grow().wrap().labelAlign(Align.center);
super.show();
}
}

View File

@@ -1,21 +1,38 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.flabel.*;
import arc.math.*;
import arc.scene.actions.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.core.GameState.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.type.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class GameOverDialog extends BaseDialog{
private Team winner;
private boolean hudShown;
public GameOverDialog(){
super("@gameover");
setFillParent(true);
shown(this::rebuild);
titleTable.remove();
shown(() -> {
hudShown = ui.hudfrag.shown;
ui.hudfrag.shown = false;
rebuild();
});
hidden(() -> ui.hudfrag.shown = hudShown);
Events.on(ResetEvent.class, e -> hide());
}
@@ -31,73 +48,119 @@ public class GameOverDialog extends BaseDialog{
}
void rebuild(){
title.setText(state.isCampaign() ? "@sector.curlost" : "@gameover");
buttons.clear();
cont.clear();
buttons.margin(10);
if(state.rules.pvp && winner != null){
cont.add(Core.bundle.format("gameover.pvp", winner.localized())).pad(6);
cont.table(t -> {
if(state.rules.pvp && winner != null){
t.add(Core.bundle.format("gameover.pvp", winner.coloredName())).center().pad(6);
}else{
t.add(state.isCampaign() ? Core.bundle.format("sector.lost", state.getSector().name()) : "@gameover").center().pad(6);
}
t.row();
if(control.isHighScore()){
t.add("@highscore").pad(6);
t.row();
}
t.pane(p -> {
p.margin(13f);
p.left().defaults().left();
p.setBackground(Styles.black3);
p.table(stats -> {
if(state.rules.waves) addStat(stats, Core.bundle.get("stats.wave"), state.stats.wavesLasted, 0f);
addStat(stats, Core.bundle.get("stats.unitsCreated"), state.stats.unitsCreated, 0.05f);
addStat(stats, Core.bundle.get("stats.enemiesDestroyed"), state.stats.enemyUnitsDestroyed, 0.1f);
addStat(stats, Core.bundle.get("stats.built"), state.stats.buildingsBuilt, 0.15f);
addStat(stats, Core.bundle.get("stats.destroyed"), state.stats.buildingsDestroyed, 0.2f);
addStat(stats, Core.bundle.get("stats.deconstructed"), state.stats.buildingsDeconstructed, 0.25f);
}).top().grow().row();
if(control.saves.getCurrent() != null){
p.table(pt -> {
pt.add(new FLabel(Core.bundle.get("stats.playtime"))).left().pad(5).growX();
pt.add(new FLabel("[accent]" + control.saves.getCurrent().getPlayTime())).right().pad(5);
}).growX();
}
}).grow().pad(12).top();
}).center().minWidth(370).maxSize(600, 550).grow();
if(state.isCampaign() && net.client()){
cont.row();
cont.add("@gameover.waiting").padTop(20f).row();
}
if(state.isCampaign()){
if(net.client()){
buttons.button("@gameover.disconnect", () -> {
logic.reset();
net.reset();
hide();
state.set(State.menu);
}).size(170f, 60f);
}else{
buttons.button("@continue", () -> {
hide();
ui.planet.show();
}).size(170f, 60f);
}
}else{
buttons.button("@menu", () -> {
hide();
logic.reset();
}).size(130f, 60f);
}else{
if(control.isHighScore()){
cont.add("@highscore").pad(6);
cont.row();
}
cont.pane(t -> {
t.margin(13f);
t.left().defaults().left();
t.add(Core.bundle.format("stat.wave", state.stats.wavesLasted)).row();
t.add(Core.bundle.format("stat.enemiesDestroyed", state.stats.enemyUnitsDestroyed)).row();
t.add(Core.bundle.format("stat.built", state.stats.buildingsBuilt)).row();
t.add(Core.bundle.format("stat.destroyed", state.stats.buildingsDestroyed)).row();
t.add(Core.bundle.format("stat.deconstructed", state.stats.buildingsDeconstructed)).row();
if(control.saves.getCurrent() != null){
t.add(Core.bundle.format("stat.playtime", control.saves.getCurrent().getPlayTime())).row();
}
if(state.isCampaign() && !state.stats.itemsDelivered.isEmpty()){
t.add("@stat.delivered").row();
for(Item item : content.items()){
if(state.stats.itemsDelivered.get(item, 0) > 0){
t.table(items -> {
items.add(" [lightgray]" + state.stats.itemsDelivered.get(item, 0));
items.image(item.icon(Cicon.small)).size(8 * 3).pad(4);
}).left().row();
}
}
}
if(state.isCampaign() && net.client()){
t.add("@gameover.waiting").padTop(20f).row();
}
}).pad(12);
if(state.isCampaign()){
if(net.client()){
buttons.button("@gameover.disconnect", () -> {
logic.reset();
net.reset();
hide();
state.set(State.menu);
}).size(170f, 60f);
}else{
buttons.button("@continue", () -> {
hide();
ui.planet.show();
}).size(170f, 60f);
}
}else{
buttons.button("@menu", () -> {
hide();
if(!ui.paused.checkPlaytest()){
logic.reset();
}).size(140f, 60f);
}
}
}).size(140f, 60f);
}
}
private void addStat(Table parent, String stat, int value, float delay){
parent.add(new StatLabel(stat, value, delay)).top().pad(5).growX().height(50).row();
}
private static class StatLabel extends Table {
private float progress = 0;
public StatLabel(String stat, int value, float delay){
setTransform(true);
setClip(true);
setBackground(Tex.whiteui);
setColor(Pal.accent);
margin(2f);
FLabel statLabel = new FLabel(stat);
statLabel.setStyle(Styles.outlineLabel);
statLabel.setWrap(true);
statLabel.pause();
Label valueLabel = new Label("", Styles.outlineLabel);
valueLabel.setAlignment(Align.right);
add(statLabel).left().growX().padLeft(5);
add(valueLabel).right().growX().padRight(5);
actions(
Actions.scaleTo(0, 1),
Actions.delay(delay),
Actions.parallel(
Actions.scaleTo(1, 1, 0.3f, Interp.pow3Out),
Actions.color(Pal.darkestGray, 0.3f, Interp.pow3Out),
Actions.sequence(
Actions.delay(0.3f),
Actions.run(() -> {
valueLabel.update(() -> {
progress = Math.min(1, progress + (Time.delta / 60));
valueLabel.setText("" + (int)Mathf.lerp(0, value, value < 10 ? progress : Interp.slowFast.apply(progress)));
});
statLabel.resume();
})
)
)
);
}
}
}

View File

@@ -4,10 +4,11 @@ import arc.*;
import arc.scene.ui.*;
import arc.util.*;
import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.ui.*;
import java.io.*;
import java.util.*;
import static mindustry.Vars.*;
@@ -28,15 +29,24 @@ public class HostDialog extends BaseDialog{
ui.listfrag.rebuild();
}).grow().pad(8).get().setMaxLength(40);
ImageButton button = t.button(Tex.whiteui, Styles.clearFulli, 40, () -> {
ImageButton button = t.button(Tex.whiteui, Styles.squarei, 40, () -> {
new PaletteDialog().show(color -> {
player.color().set(color);
player.color.set(color);
Core.settings.put("color-0", color.rgba());
});
}).size(54f).get();
button.update(() -> button.getStyle().imageUpColor = player.color());
}).width(w).height(70f).pad(4).colspan(3);
if(steam){
cont.row();
cont.add().width(65f);
cont.check("@steam.friendsonly", !Core.settings.getBool("steampublichost"), val -> Core.settings.put("steampublichost", !val)).colspan(2).left()
.with(c -> ui.addDescTooltip(c, "@steam.friendsonly.tooltip")).padBottom(15f).row();
}
cont.row();
cont.table(t -> {
@@ -68,7 +78,11 @@ public class HostDialog extends BaseDialog{
runHost();
}).width(w).height(70f).disabled(b -> !portField.isValid());
cont.button("?", () -> ui.showInfo("@host.info")).size(65f, 70f).padLeft(6f);
if(!steam){
cont.button("?", () -> ui.showInfo("@host.info")).size(65f, 70f).padLeft(6f);
}else{
cont.add().size(65f, 70f).padLeft(6f);
}
shown(() -> {
if(!steam){
@@ -82,34 +96,20 @@ public class HostDialog extends BaseDialog{
Time.runTask(5f, () -> {
try{
net.host(Core.settings.getInt("port", port));
player.admin(true);
if(steam){
Core.app.post(() -> Core.settings.getBoolOnce("steampublic3", () -> {
ui.showCustomConfirm("@setting.publichost.name", "@public.confirm", "@yes", "@no", () -> {
ui.showCustomConfirm("@setting.publichost.name", "@public.confirm.really", "@no", "@yes", () -> {
Core.settings.put("publichost", true);
platform.updateLobby();
}, () -> {
Core.settings.put("publichost", false);
platform.updateLobby();
});
}, () -> {
Core.settings.put("publichost", false);
platform.updateLobby();
});
}));
player.admin = true;
Events.fire(new HostEvent());
if(steam && Core.settings.getBool("steampublichost")){
if(Version.modifier.contains("beta") || Version.modifier.contains("alpha")){
Core.settings.put("publichost", false);
Core.settings.put("steampublichost", false);
platform.updateLobby();
Core.settings.getBoolOnce("betapublic", () -> ui.showInfo("@public.beta"));
}
}
}catch(IOException e){
ui.showException("@server.error", e);
}catch(Exception e){
ui.showException(e.getMessage() != null && e.getMessage().toLowerCase(Locale.ROOT).contains("address already in use") ? "@server.error.addressinuse" : "@server.error", e);
}
ui.loadfrag.hide();
hide();

View File

@@ -0,0 +1,79 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class IconSelectDialog extends Dialog{
private Intc consumer = i -> Log.info("you have mere seconds");
private boolean allowLocked;
public IconSelectDialog(){
this(true);
}
public IconSelectDialog(boolean allowLocked){
closeOnBack();
setFillParent(true);
cont.pane(t -> {
resized(true, () -> {
t.clearChildren();
t.marginRight(19f);
t.defaults().size(48f);
t.button(Icon.none, Styles.flati, () -> {
hide();
consumer.get(0);
});
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.flati, () -> {
hide();
consumer.get(Iconc.codes.get(key));
});
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).<UnlockableContent>as()){
if(!u.isHidden() && (allowLocked || u.unlocked())){
t.button(new TextureRegionDrawable(u.uiIcon), Styles.flati, iconMed, () -> {
hide();
consumer.get(u.emojiChar());
});
if(++i % cols == 0) t.row();
}
}
}
});
});
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
}
public void show(Intc listener){
consumer = listener;
super.show();
}
}

View File

@@ -1,11 +1,12 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.Net.*;
import arc.freetype.FreeTypeFontGenerator.*;
import arc.graphics.*;
import arc.input.*;
import arc.math.*;
import arc.scene.ui.*;
import arc.scene.ui.TextButton.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
@@ -16,7 +17,7 @@ import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.legacy.*;
import mindustry.io.versions.*;
import mindustry.net.*;
import mindustry.net.Packets.*;
import mindustry.ui.*;
@@ -34,32 +35,53 @@ public class JoinDialog extends BaseDialog{
int totalHosts;
int refreshes;
boolean showHidden;
TextButtonStyle style;
Task fontIgnoreDirtyTask;
String lastIp;
int lastPort;
int lastPort, lastColumns = -1;
Task ping;
String serverSearch = "";
public JoinDialog(){
super("@joingame");
makeButtonOverlay();
style = new TextButtonStyle(){{
over = Styles.flatOver;
font = Fonts.def;
fontColor = Color.white;
disabledFontColor = Color.gray;
down = Styles.flatOver;
up = Styles.black5;
}};
loadServers();
if(!steam) buttons.add().width(60f);
//mobile players don't get information >:(
boolean infoButton = !steam && !mobile;
if(infoButton) buttons.add().width(60f);
buttons.add().growX().width(-1);
addCloseButton();
addCloseButton(mobile ? 190f : 210f);
buttons.button("@server.add", Icon.add, () -> {
renaming = null;
add.show();
});
buttons.add().growX().width(-1);
if(!steam){
buttons.button("?", () -> ui.showInfo("@join.info")).size(60f, 64f).width(-1);
}
if(infoButton) buttons.button("?", () -> ui.showInfo("@join.info")).size(60f, 64f);
add = new BaseDialog("@joingame.title");
add.cont.add("@joingame.ip").padRight(5f).left();
TextField field = add.cont.field(Core.settings.getString("ip"), text -> {
Core.settings.put("ip", text);
}).size(320f, 54f).maxTextLength(100).addInputDialog().get();
}).size(320f, 54f).maxTextLength(100).get();
add.cont.row();
add.buttons.defaults().size(140f, 60f).pad(4f);
@@ -97,8 +119,17 @@ public class JoinDialog extends BaseDialog{
});
onResize(() -> {
setup();
refreshAll();
//only refresh on resize when the minimum dimension is smaller than the maximum preferred width
//this means that refreshes on resize will only happen for small phones that need the list to fit in portrait mode
//also resize if number of cols changes
if(Math.min(Core.graphics.getWidth(), Core.graphics.getHeight()) / Scl.scl() * 0.9f < 500f || lastColumns != columns()){
setup();
refreshAll();
}
lastColumns = columns();
});
}
@@ -107,7 +138,9 @@ public class JoinDialog extends BaseDialog{
refreshLocal();
refreshRemote();
refreshGlobal();
if(Core.settings.getBool("communityservers", true)){
refreshCommunity();
}
}
void setupRemote(){
@@ -115,26 +148,30 @@ public class JoinDialog extends BaseDialog{
for(Server server : servers){
//why are java lambdas this bad
TextButton[] buttons = {null};
Button[] buttons = {null};
TextButton button = buttons[0] = remote.button("[accent]" + server.displayIP(), Styles.cleart, () -> {
Button button = buttons[0] = remote.button(b -> {}, style, () -> {
if(!buttons[0].childrenPressed()){
if(server.lastHost != null){
Events.fire(new ClientPreConnectEvent(server.lastHost));
safeConnect(server.ip, server.port, server.lastHost.version);
safeConnect(server.lastHost.address, server.lastHost.port, server.lastHost.version);
}else{
connect(server.ip, server.port);
}
}
}).width(targetWidth()).pad(4f).get();
}).width(targetWidth()).growY().top().left().pad(4f).get();
button.getLabel().setWrap(true);
if(remote.getChildren().size % columns() == 0){
remote.row();
}
Table inner = new Table(Tex.whiteui);
inner.setColor(Pal.gray);
Table inner = new Table();
button.clearChildren();
button.add(inner).growX();
inner.add(button.getLabel()).growX();
inner.add("[accent]" + server.displayIP()).left().padLeft(10f).wrap().style(Styles.outlineLabel).growX();
inner.button(Icon.upOpen, Styles.emptyi, () -> {
moveRemote(server, -1);
@@ -167,8 +204,6 @@ public class JoinDialog extends BaseDialog{
button.row();
server.content = button.table(t -> {}).grow().get();
remote.row();
}
}
@@ -200,55 +235,76 @@ public class JoinDialog extends BaseDialog{
void refreshServer(Server server){
server.content.clear();
server.content.label(() -> Core.bundle.get("server.refreshing") + Strings.animated(Time.time, 4, 11, "."));
server.content.background(Tex.whitePane).setColor(Pal.gray);
server.content.label(() -> Core.bundle.get("server.refreshing") + Strings.animated(Time.time, 4, 11, ".")).grow().center().labelAlign(Align.center).padBottom(4);
net.pingHost(server.ip, server.port, host -> setupServer(server, host), e -> {
server.content.clear();
server.content.add("@host.invalid").padBottom(4);
server.content.background(Tex.whitePane).setColor(Pal.gray);
server.content.add("@host.invalid").grow().center().labelAlign(Align.center);
});
}
void setupServer(Server server, Host host){
server.lastHost = host;
server.content.clear();
buildServer(host, server.content);
buildServer(host, server.content, false, true);
}
void buildServer(Host host, Table content){
String versionString;
void buildServer(Host host, Table content, boolean local, boolean addName){
content.top().left();
boolean isBanned = local && Vars.steam && host.description != null && host.description.equals("[banned]");
String versionString = getVersionString(host) + (isBanned ? "[red] [banned]" : "");
if(host.version == -1){
versionString = Core.bundle.format("server.version", Core.bundle.get("server.custombuild"), "");
}else if(host.version == 0){
versionString = Core.bundle.get("server.outdated");
}else if(host.version < Version.build && Version.build != -1){
versionString = Core.bundle.get("server.outdated") + "\n" +
Core.bundle.format("server.version", host.version, "");
}else if(host.version > Version.build && Version.build != -1){
versionString = Core.bundle.get("server.outdated.client") + "\n" +
Core.bundle.format("server.version", host.version, "");
}else if(host.version == Version.build && Version.type.equals(host.versionType)){
//not important
versionString = "";
}else{
versionString = Core.bundle.format("server.version", host.version, host.versionType);
float twidth = targetWidth() - 40f;
content.background(null);
Color color = Pal.gray;
if(addName){
content.table(Tex.whiteui, t -> {
t.left();
t.setColor(color);
t.add(host.name + " " + versionString).style(Styles.outlineLabel).padLeft(10f).width(twidth).left().ellipsis(true);
}).growX().height(36f).row();
}
content.table(t -> {
t.add("[lightgray]" + host.name + " " + versionString).width(targetWidth() - 10f).left().get().setEllipsis(true);
t.row();
if(!host.description.isEmpty()){
t.add("[gray]" + host.description).width(targetWidth() - 10f).left().wrap();
content.table(Tex.whitePane, t -> {
t.top().left();
t.setColor(color);
t.left();
if(!host.description.isEmpty() && !isBanned){
//limit newlines.
int count = 0;
StringBuilder result = new StringBuilder(host.description.length());
for(int i = 0; i < host.description.length(); i++){
char c = host.description.charAt(i);
if(c == '\n'){
count ++;
if(count < 3) result.append(c);
}else{
result.append(c);
}
}
t.add("[gray]" + result).width(twidth).left().wrap();
t.row();
}
t.add("[lightgray]" + (Core.bundle.format("players" + (host.players == 1 && host.playerLimit <= 0 ? ".single" : ""), (host.players == 0 ? "[lightgray]" : "[accent]") + host.players + (host.playerLimit > 0 ? "[lightgray]/[accent]" + host.playerLimit : "")+ "[lightgray]"))).left();
t.row();
t.add("[lightgray]" + Core.bundle.format("save.map", host.mapname) + "[lightgray] / " + (host.modeName == null ? host.mode.toString() : host.modeName)).width(targetWidth() - 10f).left().get().setEllipsis(true);
t.add("[lightgray]" + (Core.bundle.format("players" + (host.players == 1 && host.playerLimit <= 0 ? ".single" : ""),
(host.players == 0 ? "[lightgray]" : "[accent]") + host.players + (host.playerLimit > 0 ? "[lightgray]/[accent]" + host.playerLimit : "")+ "[lightgray]"))).left().row();
t.add("[lightgray]" + Core.bundle.format("save.map", host.mapname) + "[lightgray] / " + (host.modeName == null ? host.mode.toString() : host.modeName)).width(twidth).left().ellipsis(true).row();
if(host.ping > 0){
t.row();
t.add(Iconc.chartBar + " " + host.ping + "ms").color(Color.gray).left();
t.add(Iconc.chartBar + " " + host.ping + "ms").style(Styles.outlineLabel).color(Pal.gray).left();
}
}).expand().left().bottom().padLeft(12f).padBottom(8);
}).growY().growX().left().bottom();
}
void setup(){
@@ -258,10 +314,14 @@ public class JoinDialog extends BaseDialog{
float w = targetWidth();
hosts.clear();
//since the buttons are an overlay, make room for that
hosts.marginBottom(70f);
section("@servers.local", local, false);
section(steam ? "@servers.local.steam" : "@servers.local", local, false);
section("@servers.remote", remote, false);
section("@servers.global", global, true);
if(Core.settings.getBool("communityservers", true)){
section("@servers.global", global, true);
}
ScrollPane pane = new ScrollPane(hosts);
pane.setFadeScrollBars(false);
@@ -275,9 +335,9 @@ public class JoinDialog extends BaseDialog{
t.field(Core.settings.getString("name"), text -> {
player.name(text);
Core.settings.put("name", text);
}).grow().pad(8).addInputDialog(maxNameLength);
}).grow().pad(8).maxTextLength(maxNameLength);
ImageButton button = t.button(Tex.whiteui, Styles.clearFulli, 40, () -> {
ImageButton button = t.button(Tex.whiteui, Styles.squarei, 40, () -> {
new PaletteDialog().show(color -> {
player.color().set(color);
Core.settings.put("color-0", color.rgba8888());
@@ -286,27 +346,8 @@ public class JoinDialog extends BaseDialog{
button.update(() -> button.getStyle().imageUpColor = player.color());
}).width(w).height(70f).pad(4);
cont.row();
cont.add(pane).width(w + 38).pad(0);
cont.add(pane).width((w + 5) * columns() + 33).pad(0);
cont.row();
cont.buttonCenter("@server.add", Icon.add, () -> {
renaming = null;
add.show();
}).marginLeft(10).width(w).height(80f).update(button -> {
float pw = w;
float pad = 0f;
if(pane.getChildren().first().getPrefHeight() > pane.getHeight()){
pw = w + 30;
pad = 6;
}
var cell = ((Table)pane.parent).getCell(button);
if(!Mathf.equal(cell.minWidth(), pw)){
cell.width(pw);
cell.padLeft(pad);
pane.parent.invalidateHierarchy();
}
});
}
void section(String label, Table servers, boolean eye){
@@ -319,7 +360,7 @@ public class JoinDialog extends BaseDialog{
if(eye){
name.button(Icon.eyeSmall, Styles.emptyi, () -> {
showHidden = !showHidden;
refreshGlobal();
refreshCommunity();
}).update(i -> i.getStyle().imageUp = (showHidden ? Icon.eyeSmall : Icon.eyeOffSmall))
.size(40f).right().padRight(3).tooltip("@servers.showhidden");
}
@@ -332,7 +373,7 @@ public class JoinDialog extends BaseDialog{
hosts.row();
hosts.image().growX().pad(5).padLeft(10).padRight(10).height(3).color(Pal.accent);
hosts.row();
hosts.add(coll).width(targetWidth());
hosts.add(coll).width((targetWidth() + 5f) * columns());
hosts.row();
}
@@ -345,51 +386,61 @@ public class JoinDialog extends BaseDialog{
net.discoverServers(this::addLocalHost, this::finishLocalHosts);
}
void refreshGlobal(){
void refreshCommunity(){
int cur = refreshes;
global.clear();
global.background(null);
for(ServerGroup group : defaultServers){
global.table(t -> {
t.add("@search").padRight(10);
t.field(serverSearch, text ->
serverSearch = text.trim().replaceAll(" +", " ").toLowerCase()
).grow().pad(8).get().keyDown(KeyCode.enter, this::refreshCommunity);
t.button(Icon.zoom, Styles.emptyi, this::refreshCommunity).size(54f);
}).width((targetWidth() + 5f) * columns()).height(70f).pad(4).row();
for(int i = 0; i < defaultServers.size; i ++){
ServerGroup group = defaultServers.get((i + defaultServers.size/2) % defaultServers.size);
boolean hidden = group.hidden();
if(hidden && !showHidden){
continue;
}
Table[] groupTable = {null};
Table[] groupTable = {null, null};
if(group.prioritized){
addHeader(groupTable, group, hidden, false);
}
//table containing all groups
for(String address : group.addresses){
String resaddress = address.contains(":") ? address.split(":")[0] : address;
int resport = address.contains(":") ? Strings.parseInt(address.split(":")[1]) : port;
net.pingHost(resaddress, resport, res -> {
if(refreshes != cur) return;
res.port = resport;
//add header
if(groupTable[0] == null){
global.table(t -> groupTable[0] = t).row();
groupTable[0].table(head -> {
if(!group.name.isEmpty()){
head.add(group.name).color(Color.lightGray).padRight(4);
}
head.image().height(3f).growX().color(Color.lightGray);
//button for showing/hiding servers
ImageButton[] image = {null};
image[0] = head.button(hidden ? Icon.eyeOffSmall : Icon.eyeSmall, Styles.accenti, () -> {
group.setHidden(!group.hidden());
image[0].getStyle().imageUp = group.hidden() ? Icon.eyeOffSmall : Icon.eyeSmall;
if(group.hidden() && !showHidden){
groupTable[0].remove();
}
}).size(40f).get();
image[0].addListener(new Tooltip(t -> t.background(Styles.black6).margin(4).label(() -> !group.hidden() ? "@server.shown" : "@server.hidden")));
}).width(targetWidth()).padBottom(-2).row();
//don't recache the texture for a while
if(fontIgnoreDirtyTask == null){
FreeTypeFontData.ignoreDirty = true;
fontIgnoreDirtyTask = Time.runTask(0.6f * 60f, () -> {
FreeTypeFontData.ignoreDirty = false;
fontIgnoreDirtyTask = null;
});
}
addGlobalHost(res, groupTable[0]);
if(!serverSearch.isEmpty() && !(group.name.toLowerCase().contains(serverSearch)
|| res.name.toLowerCase().contains(serverSearch)
|| res.description.toLowerCase().contains(serverSearch)
|| res.mapname.toLowerCase().contains(serverSearch)
|| (res.modeName != null && res.modeName.toLowerCase().contains(serverSearch)))) return;
if(groupTable[0] == null){
addHeader(groupTable, group, hidden, true);
}else if(!groupTable[0].visible){
addHeader(groupTable, group, hidden, true);
}
addCommunityHost(res, groupTable[1]);
groupTable[0].margin(5f);
groupTable[0].pack();
@@ -398,11 +449,53 @@ public class JoinDialog extends BaseDialog{
}
}
void addGlobalHost(Host host, Table container){
void addHeader(Table[] groupTable, ServerGroup group, boolean hidden, boolean doInit){ // outlined separately
if(groupTable[0] == null){
global.table(t -> groupTable[0] = t).fillX().left().row();
}
groupTable[0].visible(() -> doInit);
if(!doInit){
return;
}
groupTable[0].table(head -> {
Color col = group.prioritized ? Pal.accent : Color.lightGray;
if(!group.name.isEmpty()){
head.add(group.name).color(col).padRight(4);
}
head.image().height(3f).growX().color(col);
//button for showing/hiding servers
ImageButton[] image = {null};
image[0] = head.button(hidden ? Icon.eyeOffSmall : Icon.eyeSmall, Styles.grayi, () -> {
group.setHidden(!group.hidden());
image[0].getStyle().imageUp = group.hidden() ? Icon.eyeOffSmall : Icon.eyeSmall;
if(group.hidden() && !showHidden){
groupTable[0].remove();
}
}).size(40f).get();
image[0].addListener(new Tooltip(t -> t.background(Styles.black6).margin(4).label(() -> !group.hidden() ? "@server.shown" : "@server.hidden")));
}).width(targetWidth() * columns()).padBottom(-2).row();
groupTable[1] = groupTable[0].row().table().top().left().grow().get();
}
int columns(){
return Mathf.clamp((int)((Core.graphics.getWidth() / Scl.scl() * 0.9f) / targetWidth()), 1, 4);
}
void addCommunityHost(Host host, Table container){
global.background(null);
String versionString = getVersionString(host);
float w = targetWidth();
container.button(b -> buildServer(host, b), Styles.cleart, () -> {
container.left().top();
Button[] button = {null};
button[0] = container.button(b -> {}, style, () -> {
if(button[0].childrenPressed()) return;
Events.fire(new ClientPreConnectEvent(host));
if(!Core.settings.getBool("server-disclaimer", false)){
ui.showCustomConfirm("@warning", "@servers.disclaimer", "@ok", "@back", () -> {
@@ -414,7 +507,32 @@ public class JoinDialog extends BaseDialog{
}else{
safeConnect(host.address, host.port, host.version);
}
}).width(w).row();
}).width(w).padBottom(7).padRight(4f).top().left().growY().uniformY().get();
Table inner = new Table(Tex.whiteui);
inner.setColor(Pal.gray);
button[0].clearChildren();
button[0].add(inner).growX();
inner.add(host.name + " " + versionString).left().padLeft(10f).wrap().style(Styles.outlineLabel).growX();
inner.button(Icon.add, Styles.emptyi, () -> {
Server server = new Server();
server.setIP(host.address + ":" + host.port);
servers.add(server);
saveServers();
setupRemote();
refreshRemote();
}).margin(3f).pad(8f).padRight(4f).top().right();
button[0].row();
buildServer(host, button[0].table(t -> {}).grow().get(), false, false);
if((container.getChildren().size) % columns() == 0){
container.row();
}
}
void finishLocalHosts(){
@@ -437,12 +555,14 @@ public class JoinDialog extends BaseDialog{
totalHosts++;
float w = targetWidth();
local.row();
if((local.getChildren().size) % columns() == 0){
local.row();
}
local.button(b -> buildServer(host, b), Styles.cleart, () -> {
local.button(b -> buildServer(host, b, true, true), style, () -> {
Events.fire(new ClientPreConnectEvent(host));
safeConnect(host.address, host.port, host.version);
}).width(w);
}).width(w).top().left().growY();
}
public void connect(String ip, int port){
@@ -463,8 +583,10 @@ public class JoinDialog extends BaseDialog{
net.reset();
Vars.netClient.beginConnecting();
net.connect(lastIp = ip, lastPort = port, () -> {
hide();
add.hide();
if(net.client()){
hide();
add.hide();
}
});
});
}
@@ -481,7 +603,7 @@ public class JoinDialog extends BaseDialog{
connect(lastIp, lastPort);
}, exception -> {});
}, 1, 1);
ui.loadfrag.setButton(() -> {
ui.loadfrag.hide();
if(ping == null) return;
@@ -492,7 +614,7 @@ public class JoinDialog extends BaseDialog{
void safeConnect(String ip, int port, int version){
if(version != Version.build && Version.build != -1 && version != -1){
ui.showInfo("[scarlet]" + (version > Version.build ? KickReason.clientOutdated : KickReason.serverOutdated).toString() + "\n[]" +
ui.showInfo("[scarlet]" + (version > Version.build ? KickReason.clientOutdated : KickReason.serverOutdated) + "\n[]" +
Core.bundle.format("server.versions", Version.build, version));
}else{
connect(ip, port);
@@ -500,7 +622,7 @@ public class JoinDialog extends BaseDialog{
}
float targetWidth(){
return Math.min(Core.graphics.getWidth() / Scl.scl() * 0.9f, 500f);
return Math.min(Core.graphics.getWidth() / Scl.scl() * 0.9f, 550f);
}
@SuppressWarnings("unchecked")
@@ -513,48 +635,76 @@ public class JoinDialog extends BaseDialog{
Core.settings.remove("server-list");
}
var url = becontrol.active() ? serverJsonBeURL : serverJsonV6URL;
Log.info("Fetching community servers at @", url);
fetchServers();
}
public static void fetchServers(){
var urls = Version.type.equals("bleeding-edge") || Vars.forceBeServers ? serverJsonBeURLs : serverJsonURLs;
if(Core.settings.getBool("communityservers", true)){
fetchServers(urls, 0);
}
}
private static void fetchServers(String[] urls, int index){
if(index >= urls.length) return;
//get servers
Core.net.httpGet(url, result -> {
try{
if(result.getStatus() != HttpStatus.OK){
Log.warn("Failed to fetch community servers: @", result.getStatus());
return;
}
Jval val = Jval.read(result.getResultAsString());
Core.app.post(() -> {
try{
defaultServers.clear();
val.asArray().each(child -> {
String name = child.getString("name", "");
String[] addresses;
if(child.has("addresses") || (child.has("address") && child.get("address").isArray())){
addresses = (child.has("addresses") ? child.get("addresses") : child.get("address")).asArray().map(Jval::asString).toArray(String.class);
}else{
addresses = new String[]{child.getString("address", "<invalid>")};
}
defaultServers.add(new ServerGroup(name, addresses));
});
Log.info("Fetched @ community servers.", defaultServers.size);
}catch(Throwable e){
Log.err("Failed to parse community servers.");
Log.err(e);
}
});
}catch(Throwable e){
Log.err("Failed to fetch community servers.");
Log.err(e);
Http.get(urls[index])
.error(t -> {
if(index < urls.length - 1){
//attempt fetching from the next URL upon failure
fetchServers(urls, index + 1);
}else{
Log.err("Failed to fetch community servers", t);
}
}, Log::err);
})
.submit(result -> {
Jval val = Jval.read(result.getResultAsString());
Seq<ServerGroup> servers = new Seq<>();
val.asArray().each(child -> {
String name = child.getString("name", "");
boolean prioritized = child.getBool("prioritized", false);
String[] addresses;
if(child.has("addresses") || (child.has("address") && child.get("address").isArray())){
addresses = (child.has("addresses") ? child.get("addresses") : child.get("address")).asArray().map(Jval::asString).toArray(String.class);
}else{
addresses = new String[]{child.getString("address", "<invalid>")};
}
servers.add(new ServerGroup(name, addresses, prioritized));
});
//modify default servers on main thread
Core.app.post(() -> {
servers.sort(s -> s.name == null ? Integer.MAX_VALUE : s.name.hashCode());
defaultServers.addAll(servers);
Log.info("Fetched @ community servers.", defaultServers.sum(s -> s.addresses.length));
});
});
}
private void saveServers(){
Core.settings.putJson("servers", Server.class, servers);
}
private String getVersionString(Host host){
if(host.version == -1){
return Core.bundle.format("server.version", Core.bundle.get("server.custombuild"), "");
}else if(host.version == 0){
return Core.bundle.get("server.outdated");
}else if(host.version < Version.build && Version.build != -1){
return Core.bundle.get("server.outdated") + "\n" +
Core.bundle.format("server.version", host.version, "");
}else if(host.version > Version.build && Version.build != -1){
return Core.bundle.get("server.outdated.client") + "\n" +
Core.bundle.format("server.version", host.version, "");
}else if(host.version == Version.build && Version.type.equals(host.versionType)){
//not important
return "";
}else{
return Core.bundle.format("server.version", host.version, host.versionType);
}
}
public static class Server{
public String ip;
public int port;
@@ -568,7 +718,7 @@ public class JoinDialog extends BaseDialog{
if(isIpv6 && ip.lastIndexOf("]:") != -1 && ip.lastIndexOf("]:") != ip.length() - 1){
int idx = ip.indexOf("]:");
this.ip = ip.substring(1, idx);
this.port = Integer.parseInt(ip.substring(idx + 2, ip.length()));
this.port = Integer.parseInt(ip.substring(idx + 2));
}else if(!isIpv6 && ip.lastIndexOf(':') != -1 && ip.lastIndexOf(':') != ip.length() - 1){
int idx = ip.lastIndexOf(':');
this.ip = ip.substring(0, idx);

View File

@@ -0,0 +1,233 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.KeyBinds.*;
import arc.graphics.*;
import arc.input.*;
import arc.input.InputDevice.*;
import arc.scene.event.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import static arc.Core.*;
public class KeybindDialog extends Dialog{
protected Section section;
protected KeyBind rebindKey = null;
protected boolean rebindAxis = false;
protected boolean rebindMin = true;
protected KeyCode minKey = null;
protected Dialog rebindDialog;
protected float scroll;
protected ObjectIntMap<Section> sectionControls = new ObjectIntMap<>();
public KeybindDialog(){
super(bundle.get("keybind.title"));
setup();
addCloseButton();
setFillParent(true);
title.setAlignment(Align.center);
titleTable.row();
titleTable.add(new Image()).growX().height(3f).pad(4f).get().setColor(Pal.accent);
}
@Override
public void addCloseButton(){
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
keyDown(key -> {
if(key == KeyCode.escape || key == KeyCode.back) hide();
});
}
private void setup(){
cont.clear();
Section[] sections = Core.keybinds.getSections();
Stack stack = new Stack();
ButtonGroup<TextButton> group = new ButtonGroup<>();
ScrollPane pane = new ScrollPane(stack);
pane.setFadeScrollBars(false);
pane.update(() -> scroll = pane.getScrollY());
this.section = sections[0];
for(Section section : sections){
if(!sectionControls.containsKey(section))
sectionControls.put(section, input.getDevices().indexOf(section.device, true));
if(sectionControls.get(section, 0) >= input.getDevices().size){
sectionControls.put(section, 0);
section.device = input.getDevices().get(0);
}
if(sections.length != 1){
TextButton button = new TextButton(bundle.get("section." + section.name + ".name", Strings.capitalize(section.name)));
if(section.equals(this.section))
button.toggle();
button.clicked(() -> this.section = section);
group.add(button);
cont.add(button).fill();
}
Table table = new Table();
Label device = new Label("Keyboard");
//device.setColor(style.controllerColor);
device.setAlignment(Align.center);
Seq<InputDevice> devices = input.getDevices();
Table stable = new Table();
stable.button("<", () -> {
int i = sectionControls.get(section, 0);
if(i - 1 >= 0){
sectionControls.put(section, i - 1);
section.device = devices.get(i - 1);
setup();
}
}).disabled(sectionControls.get(section, 0) - 1 < 0).size(40);
stable.add(device).minWidth(device.getMinWidth() + 60);
device.setText(input.getDevices().get(sectionControls.get(section, 0)).name());
stable.button(">", () -> {
int i = sectionControls.get(section, 0);
if(i + 1 < devices.size){
sectionControls.put(section, i + 1);
section.device = devices.get(i + 1);
setup();
}
}).disabled(sectionControls.get(section, 0) + 1 >= devices.size).size(40);
//no alternate devices until further notice
//table.add(stable).colspan(4).row();
table.add().height(10);
table.row();
if(section.device.type() == DeviceType.controller){
table.table(info -> info.add("Controller Type: [lightGray]" +
Strings.capitalize(section.device.name())).left());
}
table.row();
String lastCategory = null;
var tstyle = Styles.defaultt;
for(KeyBind keybind : keybinds.getKeybinds()){
if(lastCategory != keybind.category() && keybind.category() != null){
table.add(bundle.get("category." + keybind.category() + ".name", Strings.capitalize(keybind.category()))).color(Color.gray).colspan(4).pad(10).padBottom(4).row();
table.image().color(Color.gray).fillX().height(3).pad(6).colspan(4).padTop(0).padBottom(10).row();
lastCategory = keybind.category();
}
if(keybind.defaultValue(section.device.type()) instanceof Axis){
table.add(bundle.get("keybind." + keybind.name() + ".name", Strings.capitalize(keybind.name())), Color.white).left().padRight(40).padLeft(8);
table.labelWrap(() -> {
Axis axis = keybinds.get(section, keybind);
return axis.key != null ? axis.key.toString() : axis.min + " [red]/[] " + axis.max;
}).color(Pal.accent).left().minWidth(90).fillX().padRight(20);
table.button("@settings.rebind", tstyle, () -> {
rebindAxis = true;
rebindMin = true;
openDialog(section, keybind);
}).width(130f);
}else{
table.add(bundle.get("keybind." + keybind.name() + ".name", Strings.capitalize(keybind.name())), Color.white).left().padRight(40).padLeft(8);
table.label(() -> keybinds.get(section, keybind).key.toString()).color(Pal.accent).left().minWidth(90).padRight(20);
table.button("@settings.rebind", tstyle, () -> {
rebindAxis = false;
rebindMin = false;
openDialog(section, keybind);
}).width(130f);
}
table.button("@settings.resetKey", tstyle, () -> keybinds.resetToDefault(section, keybind)).width(130f).pad(2f).padLeft(4f);
table.row();
}
table.visible(() -> this.section.equals(section));
table.button("@settings.reset", () -> keybinds.resetToDefaults()).colspan(4).padTop(4).fill();
stack.add(table);
}
cont.row();
cont.add(pane).growX().colspan(sections.length);
}
void rebind(Section section, KeyBind bind, KeyCode newKey){
if(rebindKey == null) return;
rebindDialog.hide();
boolean isAxis = bind.defaultValue(section.device.type()) instanceof Axis;
if(isAxis){
if(newKey.axis || !rebindMin){
section.binds.get(section.device.type(), OrderedMap::new).put(rebindKey, newKey.axis ? new Axis(newKey) : new Axis(minKey, newKey));
}
}else{
section.binds.get(section.device.type(), OrderedMap::new).put(rebindKey, new Axis(newKey));
}
if(rebindAxis && isAxis && rebindMin && !newKey.axis){
rebindMin = false;
minKey = newKey;
openDialog(section, rebindKey);
}else{
rebindKey = null;
rebindAxis = false;
}
}
private void openDialog(Section section, KeyBind name){
rebindDialog = new Dialog(rebindAxis ? bundle.get("keybind.press.axis") : bundle.get("keybind.press"));
rebindKey = name;
rebindDialog.titleTable.getCells().first().pad(4);
if(section.device.type() == DeviceType.keyboard){
rebindDialog.addListener(new InputListener(){
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
if(Core.app.isAndroid()) return false;
rebind(section, name, button);
return false;
}
@Override
public boolean keyDown(InputEvent event, KeyCode keycode){
rebindDialog.hide();
rebind(section, name, keycode);
return false;
}
@Override
public boolean scrolled(InputEvent event, float x, float y, float amountX, float amountY){
if(!rebindAxis) return false;
rebindDialog.hide();
rebind(section, name, KeyCode.scroll);
return false;
}
});
}
rebindDialog.show();
Time.runTask(1f, () -> getScene().setScrollFocus(rebindDialog));
}
}

View File

@@ -5,6 +5,7 @@ import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.type.*;
import mindustry.ui.*;
import java.util.*;
@@ -12,18 +13,58 @@ import java.util.*;
import static mindustry.Vars.*;
public class LanguageDialog extends BaseDialog{
private Locale lastLocale;
private ObjectMap<Locale, String> displayNames = ObjectMap.of(
Locale.TRADITIONAL_CHINESE, "正體中文",
Locale.SIMPLIFIED_CHINESE, "简体中文"
public static final ObjectMap<String, String> displayNames = ObjectMap.of(
"ca", "Català",
"id_ID", "Bahasa Indonesia",
"da", "Dansk",
"de", "Deutsch",
"et", "Eesti",
"en", "English",
"es", "Español",
"eu", "Euskara",
"fil", "Filipino",
"fr", "Français",
"it", "Italiano",
"lt", "Lietuvių",
"hu", "Magyar",
"nl", "Nederlands",
"nl_BE", "Nederlands (België)",
"pl", "Polski",
"pt_BR", "Português (Brasil)",
"pt_PT", "Português (Portugal)",
"ro", "Română",
"fi", "Suomi",
"sv", "Svenska",
"vi", "Tiếng Việt",
"tk", "Türkmen dili",
"tr", "Türkçe",
"cs", "Čeština",
"be", "Беларуская",
"bg", "Български",
"ru", "Русский",
"sr", "Српски",
"uk_UA", "Українська",
"th", "ไทย",
"zh_CN", "简体中文",
"zh_TW", "正體中文",
"ja", "日本語",
"ko", "한국어",
"router", "router"
);
private Locale lastLocale;
public LanguageDialog(){
super("@settings.language");
addCloseButton();
setup();
}
public static String getDisplayName(Locale locale){
String str = locale.toString().replace("in_ID", "id_ID");
return displayNames.get(str, str);
}
private void setup(){
Table langs = new Table();
langs.marginRight(24f).marginLeft(24f);
@@ -33,11 +74,12 @@ public class LanguageDialog extends BaseDialog{
ButtonGroup<TextButton> group = new ButtonGroup<>();
for(Locale loc : locales){
TextButton button = new TextButton(Strings.capitalize(displayNames.get(loc, loc.getDisplayName(loc))), Styles.clearTogglet);
TextButton button = new TextButton(getDisplayName(loc), Styles.flatTogglet);
button.clicked(() -> {
if(getLocale().equals(loc)) return;
Core.settings.put("locale", loc.toString());
Log.info("Setting locale: @", loc.toString());
player.locale = loc.toString();
ui.showInfo("@language.restart");
});
langs.add(button).group(group).update(t -> t.setChecked(loc.equals(getLocale()))).size(400f, 50f).row();

View File

@@ -2,10 +2,12 @@ package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.math.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import mindustry.ctype.*;
import arc.util.*;
import mindustry.content.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.type.*;
@@ -24,12 +26,14 @@ public class LaunchLoadoutDialog extends BaseDialog{
Schematic selected;
//validity of loadout items
boolean valid;
//last calculated capacity
int lastCapacity;
public LaunchLoadoutDialog(){
super("@configure");
}
public void show(CoreBlock core, Sector sector, Runnable confirm){
public void show(CoreBlock core, Sector sector, Sector destination, Runnable confirm){
cont.clear();
buttons.clear();
@@ -40,19 +44,52 @@ public class LaunchLoadoutDialog extends BaseDialog{
ItemSeq sitems = sector.items();
//hide nonsensical items
ItemSeq launch = universe.getLaunchResources();
if(sector.planet.allowLaunchLoadout){
for(var item : content.items()){
if(!item.isOnPlanet(sector.planet)){
launch.set(item, 0);
}
}
universe.updateLaunchResources(launch);
}
//updates sum requirements
Runnable update = () -> {
int cap = selected.findCore().itemCapacity;
int cap = lastCapacity = (int)(sector.planet.launchCapacityMultiplier * selected.findCore().itemCapacity);
//cap resources based on core type
ItemSeq schems = selected.requirements();
ItemSeq resources = universe.getLaunchResources();
resources.min(cap);
int capacity = lastCapacity;
if(!destination.allowLaunchLoadout()){
resources.clear();
//TODO this should be set to a proper loadout based on sector.
if(destination.preset != null){
var rules = destination.preset.generator.map.rules();
for(var stack : rules.loadout){
if(stack.item.isOnPlanet(sector.planet)){
resources.add(stack.item, stack.amount);
}
}
}
}else if(getMax()){
for(Item item : content.items()){
resources.set(item, Mathf.clamp(sitems.get(item) - schems.get(item), 0, capacity));
}
}
universe.updateLaunchResources(resources);
total.clear();
selected.requirements().each(total::add);
universe.getLaunchResources().each(total::add);
valid = sitems.has(total);
valid = sitems.has(total) || PlanetDialog.debugSelect;
};
Cons<Table> rebuild = table -> {
@@ -63,10 +100,13 @@ public class LaunchLoadoutDialog extends BaseDialog{
ItemSeq launches = universe.getLaunchResources();
for(ItemStack s : total){
table.image(s.item.icon(Cicon.small)).left().size(Cicon.small.size);
int as = schems.get(s.item), al = launches.get(s.item);
String amountStr = (al + as) + "[gray] (" + (al + " + " + as + ")");
if(as + al == 0) continue;
table.image(s.item.uiIcon).left().size(iconSmall);
String amountStr = (al + as) + (destination.allowLaunchLoadout() ? "[gray] (" + (al + " + " + as + ")") : "");
table.add(
sitems.has(s.item, s.amount) ? amountStr :
@@ -82,53 +122,95 @@ public class LaunchLoadoutDialog extends BaseDialog{
Runnable rebuildItems = () -> rebuild.get(items);
buttons.button("@resources", Icon.terrain, () -> {
ItemSeq stacks = universe.getLaunchResources();
Seq<ItemStack> out = stacks.toSeq();
loadout.show(selected.findCore().itemCapacity, out, UnlockableContent::unlocked, out::clear, () -> {}, () -> {
universe.updateLaunchResources(new ItemSeq(out));
if(destination.allowLaunchLoadout()){
buttons.button("@resources.max", Icon.add, Styles.togglet, () -> {
setMax(!getMax());
update.run();
rebuildItems.run();
});
}).width(204);
}).checked(b -> getMax());
buttons.button("@launch.text", Icon.ok, () -> {
buttons.button("@resources", Icon.edit, () -> {
ItemSeq stacks = universe.getLaunchResources();
Seq<ItemStack> out = stacks.toSeq();
ItemSeq realItems = sitems.copy();
selected.requirements().each(realItems::remove);
loadout.show(lastCapacity, realItems, out, i -> i.unlocked() && i.isOnPlanet(sector.planet), out::clear, () -> {}, () -> {
universe.updateLaunchResources(new ItemSeq(out));
update.run();
rebuildItems.run();
});
}).disabled(b -> getMax());
}
boolean rows = Core.graphics.isPortrait() && mobile;
if(rows) buttons.row();
var cell = buttons.button("@launch.text", Icon.ok, () -> {
universe.updateLoadout(core, selected);
confirm.run();
hide();
}).disabled(b -> !valid);
if(rows){
cell.colspan(2).size(160f + 160f + 4f, 64f);
}
int cols = Math.max((int)(Core.graphics.getWidth() / Scl.scl(230)), 1);
ButtonGroup<Button> group = new ButtonGroup<>();
selected = universe.getLoadout(core);
if(selected == null) selected = schematics.getLoadouts().get((CoreBlock)Blocks.coreShard).first();
cont.add(Core.bundle.format("launch.from", sector.name())).row();
cont.pane(t -> {
int i = 0;
if(destination.allowLaunchSchematics()){
cont.pane(t -> {
int[] i = {0};
for(var entry : schematics.getLoadouts()){
if(entry.key.size <= core.size){
for(Schematic s : entry.value){
Cons<Schematic> handler = s -> {
if(s.tiles.contains(tile -> !tile.block.supportsEnv(sector.planet.defaultEnv) ||
//make sure block can be built here.
!tile.block.isOnPlanet(sector.planet))){
return;
}
t.button(b -> b.add(new SchematicImage(s)), Styles.togglet, () -> {
selected = s;
update.run();
rebuildItems.run();
}).group(group).pad(4).checked(s == selected).size(200f);
t.button(b -> b.add(new SchematicImage(s)), Styles.togglet, () -> {
selected = s;
update.run();
rebuildItems.run();
}).group(group).pad(4).checked(s == selected).size(200f);
if(++i % cols == 0){
t.row();
if(++i[0] % cols == 0){
t.row();
}
};
if(destination.allowLaunchSchematics() || schematics.getDefaultLoadout(core) == null){
for(var entry : schematics.getLoadouts()){
if(entry.key.size <= core.size){
for(Schematic s : entry.value){
handler.get(s);
}
}
}
}else{
//only allow launching with the standard loadout schematic
handler.get(schematics.getDefaultLoadout(core));
}
}
}).growX().scrollX(false);
cont.row();
}).growX().get().setScrollingDisabled(true, false);
cont.label(() -> Core.bundle.format("launch.capacity", lastCapacity)).row();
cont.row();
}else if(destination.preset != null && destination.preset.description != null){
cont.pane(p -> {
p.add(destination.preset.description).grow().wrap().labelAlign(Align.center);
}).pad(10f).grow().row();
}
cont.row();
cont.pane(items);
cont.row();
cont.add("@sector.missingresources").visible(() -> !valid);
@@ -138,4 +220,12 @@ public class LaunchLoadoutDialog extends BaseDialog{
show();
}
void setMax(boolean max){
Core.settings.put("maxresources", max);
}
boolean getMax(){
return Core.settings.getBool("maxresources", true);
}
}

View File

@@ -8,7 +8,9 @@ import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.core.GameState.*;
import mindustry.game.*;
import mindustry.game.Saves.*;
import mindustry.gen.*;
import mindustry.io.*;
@@ -20,8 +22,11 @@ import java.io.*;
import static mindustry.Vars.*;
public class LoadDialog extends BaseDialog{
ScrollPane pane;
Table slots;
String searchString;
Seq<Gamemode> hidden;
TextField searchField;
ScrollPane pane;
public LoadDialog(){
this("@loadgame");
@@ -31,7 +36,10 @@ public class LoadDialog extends BaseDialog{
super(title);
setup();
shown(this::setup);
shown(() -> {
searchString = "";
setup();
});
onResize(this::setup);
addCloseButton();
@@ -42,10 +50,40 @@ public class LoadDialog extends BaseDialog{
cont.clear();
slots = new Table();
hidden = new Seq<>();
pane = new ScrollPane(slots);
rebuild();
Table search = new Table();
search.image(Icon.zoom);
searchField = search.field("", t -> {
searchString = t.length() > 0 ? t.toLowerCase() : null;
rebuild();
}).maxTextLength(50).growX().get();
searchField.setMessageText("@save.search");
for(Gamemode mode : Gamemode.all){
TextureRegionDrawable icon = Vars.ui.getIcon("mode" + Strings.capitalize(mode.name()));
boolean sandbox = mode == Gamemode.sandbox;
if(Core.atlas.isFound(icon.getRegion()) || sandbox){
search.button(sandbox ? Icon.terrain : icon, Styles.emptyTogglei, () -> {
if(!hidden.addUnique(mode)) hidden.remove(mode);
rebuild();
}).size(60f).padLeft(-12f).checked(b -> !hidden.contains(mode)).tooltip("@mode." + mode.name() + ".name");
}
}
pane.setFadeScrollBars(false);
pane.setScrollingDisabled(true, false);
cont.add(search).growX();
cont.row();
cont.add(pane).growY();
}
public void rebuild(){
slots.clear();
slots.marginRight(24).marginLeft(20f);
Time.runTask(2f, () -> Core.scene.setScrollFocus(pane));
@@ -58,11 +96,15 @@ public class LoadDialog extends BaseDialog{
boolean any = false;
for(SaveSlot slot : array){
if(slot.isHidden()) continue;
if(slot.isHidden()
|| (searchString != null && !Strings.stripColors(slot.getName()).toLowerCase().contains(searchString))
|| (!hidden.isEmpty() && hidden.contains(slot.mode()))){
continue;
}
any = true;
TextButton button = new TextButton("", Styles.cleart);
TextButton button = new TextButton("", Styles.grayt);
button.getLabel().remove();
button.clearChildren();
@@ -75,21 +117,21 @@ public class LoadDialog extends BaseDialog{
t.right();
t.defaults().size(40f);
t.button(Icon.save, Styles.emptytogglei, () -> {
t.button(Icon.save, Styles.emptyTogglei, () -> {
slot.setAutosave(!slot.isAutosave());
}).checked(slot.isAutosave()).right();
t.button(Icon.trash, Styles.emptyi, () -> {
ui.showConfirm("@confirm", "@save.delete.confirm", () -> {
slot.delete();
setup();
rebuild();
});
}).right();
t.button(Icon.pencil, Styles.emptyi, () -> {
ui.showTextInput("@save.rename", "@save.rename.text", slot.getName(), text -> {
slot.setName(text);
setup();
rebuild();
});
}).right();
@@ -141,10 +183,8 @@ public class LoadDialog extends BaseDialog{
}
if(!any){
slots.button("@save.none", () -> {}).disabled(true).fillX().margin(20f).minWidth(340f).height(80f).pad(4f);
slots.add("@save.none");
}
cont.add(pane);
}
public void addSetup(){
@@ -152,12 +192,18 @@ public class LoadDialog extends BaseDialog{
buttons.button("@save.import", Icon.add, () -> {
platform.showFileChooser(true, saveExtension, file -> {
if(SaveIO.isSaveValid(file)){
try{
control.saves.importSave(file);
setup();
}catch(IOException e){
e.printStackTrace();
ui.showException("@save.import.fail", e);
var meta = SaveIO.getMeta(file);
if(meta.rules.sector != null){
ui.showErrorMessage("@save.nocampaign");
}else{
try{
control.saves.importSave(file);
rebuild();
}catch(IOException e){
e.printStackTrace();
ui.showException("@save.import.fail", e);
}
}
}else{
ui.showErrorMessage("@save.import.invalid");
@@ -193,4 +239,15 @@ public class LoadDialog extends BaseDialog{
}
});
}
@Override
public Dialog show(){
super.show();
if(Core.app.isDesktop() && searchField != null){
Core.scene.setKeyboardFocus(searchField);
}
return this;
}
}

View File

@@ -22,6 +22,7 @@ public class LoadoutDialog extends BaseDialog{
private Boolf<Item> validator = i -> true;
private Table items;
private int capacity;
private @Nullable ItemSeq total;
public LoadoutDialog(){
super("@configure");
@@ -46,6 +47,8 @@ public class LoadoutDialog extends BaseDialog{
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
buttons.button("@max", Icon.export, this::maxItems).size(210f, 64f);
buttons.button("@settings.reset", Icon.refresh, () -> {
resetter.run();
reseed();
@@ -54,12 +57,23 @@ public class LoadoutDialog extends BaseDialog{
}).size(210f, 64f);
}
public void maxItems(){
for(ItemStack stack : stacks){
stack.amount = total == null ? capacity : Math.max(Math.min(capacity, total.get(stack.item)), 0);
}
}
public void show(int capacity, Seq<ItemStack> stacks, Boolf<Item> validator, Runnable reseter, Runnable updater, Runnable hider){
show(capacity, null, stacks, validator, reseter, updater, hider);
}
public void show(int capacity, ItemSeq total, Seq<ItemStack> stacks, Boolf<Item> validator, Runnable reseter, Runnable updater, Runnable hider){
this.originalStacks = stacks;
this.validator = validator;
this.resetter = reseter;
this.updater = updater;
this.capacity = capacity;
this.total = total;
this.hider = hider;
reseed();
show();
@@ -75,17 +89,17 @@ public class LoadoutDialog extends BaseDialog{
for(ItemStack stack : stacks){
items.table(Tex.pane, t -> {
t.margin(4).marginRight(8).left();
t.button("-", Styles.cleart, () -> {
t.button("-", Styles.flatt, () -> {
stack.amount = Math.max(stack.amount - step(stack.amount), 0);
updater.run();
}).size(bsize);
t.button("+", Styles.cleart, () -> {
t.button("+", Styles.flatt, () -> {
stack.amount = Math.min(stack.amount + step(stack.amount), capacity);
updater.run();
}).size(bsize);
t.button(Icon.pencil, Styles.cleari, () -> ui.showTextInput("@configure", stack.item.localizedName, 10, stack.amount + "", true, str -> {
t.button(Icon.pencil, Styles.flati, () -> ui.showTextInput("@configure", stack.item.localizedName, 10, stack.amount + "", true, str -> {
if(Strings.canParsePositiveInt(str)){
int amount = Strings.parseInt(str);
if(amount >= 0 && amount <= capacity){
@@ -97,7 +111,7 @@ public class LoadoutDialog extends BaseDialog{
ui.showInfo(Core.bundle.format("configure.invalid", capacity));
})).size(bsize);
t.image(stack.item.icon(Cicon.small)).size(8 * 3).padRight(4).padLeft(4);
t.image(stack.item.uiIcon).size(8 * 3).padRight(4).padLeft(4);
t.label(() -> stack.amount + "").left().width(90f);
}).pad(2).left().fillX();
@@ -110,7 +124,7 @@ public class LoadoutDialog extends BaseDialog{
private void reseed(){
this.stacks = originalStacks.map(ItemStack::copy);
this.stacks.addAll(content.items().select(i -> validator.get(i) && !stacks.contains(stack -> stack.item == i)).map(i -> new ItemStack(i, 0)));
this.stacks.addAll(content.items().select(i -> validator.get(i) && !i.isHidden() && !stacks.contains(stack -> stack.item == i)).map(i -> new ItemStack(i, 0)));
this.stacks.sort(Structs.comparingInt(s -> s.item.id));
}

View File

@@ -0,0 +1,233 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.graphics.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.maps.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public abstract class MapListDialog extends BaseDialog{
BaseDialog activeDialog;
private String searchString;
private Seq<Gamemode> modes = new Seq<>();
private Table mapTable = new Table();
private TextField searchField;
private boolean
showBuiltIn = Core.settings.getBool("editorshowbuiltinmaps", true),
showCustom = Core.settings.getBool("editorshowcustommaps", true),
searchAuthor = Core.settings.getBool("editorsearchauthor", false),
searchDescription = Core.settings.getBool("editorsearchdescription", false),
displayType;
public MapListDialog(String title, boolean displayType){
super(title);
this.displayType = displayType;
buttons.remove();
addCloseListener();
shown(this::setup);
onResize(() -> {
if(activeDialog != null){
activeDialog.hide();
}
setup();
});
}
void buildButtons(){}
abstract void showMap(Map map);
void setup(){
makeButtonOverlay();
buttons.clearChildren();
searchString = null;
if(Core.graphics.isPortrait() && displayType){
buttons.button("@back", Icon.left, this::hide).size(210f * 2f, 64f).colspan(2);
buttons.row();
}else{
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
}
buildButtons();
cont.clear();
rebuildMaps();
ScrollPane pane = new ScrollPane(mapTable);
pane.setFadeScrollBars(false);
pane.setScrollingDisabledX(true);
Table search = new Table();
search.image(Icon.zoom);
searchField = search.field("", t -> {
searchString = t.length() > 0 ? t.toLowerCase() : null;
rebuildMaps();
}).maxTextLength(50).growX().get();
searchField.setMessageText("@editor.search");
search.button(Icon.filter, Styles.emptyi, this::showMapFilters).tooltip("@editor.filters");
cont.add(search).growX();
cont.row();
cont.add(pane).padLeft(28f).uniformX().growY();
}
void rebuildMaps(){
mapTable.clear();
mapTable.marginRight(12f);
int maxwidth = Math.max((int)(Core.graphics.getWidth() / Scl.scl(230)), 1);
float mapsize = 200f;
boolean noMapsShown = true;
int i = 0;
Seq<Map> mapList = showCustom ?
showBuiltIn ? maps.all() : maps.customMaps() :
showBuiltIn ? maps.defaultMaps() : null;
if(mapList != null){
for(Map map : mapList){
boolean invalid = false;
for(Gamemode mode : modes){
invalid |= !mode.valid(map);
}
if(invalid || (searchString != null
&& !map.plainName().toLowerCase().contains(searchString)
&& (!searchAuthor || !map.plainAuthor().toLowerCase().contains(searchString))
&& (!searchDescription || !map.plainDescription().toLowerCase().contains(searchString)))){
continue;
}
noMapsShown = false;
if(i % maxwidth == 0){
mapTable.row();
}
TextButton button = mapTable.button("", Styles.grayt, () -> showMap(map)).width(mapsize).bottom().pad(8).get();
button.clearChildren();
button.margin(9);
button.bottom();
//TODO hide in editor?
button.table(t -> {
t.left();
for(Gamemode mode : Gamemode.all){
TextureRegionDrawable icon = Vars.ui.getIcon("mode" + Strings.capitalize(mode.name()) + "Small");
if(mode.valid(map) && Core.atlas.isFound(icon.getRegion())){
t.image(icon).size(16f).pad(4f);
}
}
if(t.getChildren().size == 0){
t.add().size(16f).pad(4f);
}
}).left().row();
button.add(map.name()).width(mapsize - 18f).center().get().setEllipsis(true);
button.row();
button.image().growX().pad(4).color(Pal.gray);
button.row();
button.stack(new Image(map.safeTexture()).setScaling(Scaling.fit), new BorderImage(map.safeTexture()).setScaling(Scaling.fit)).size(mapsize - 20f);
if(displayType){
button.row();
button.add(map.custom ? "@custom" : map.workshop ? "@workshop" : map.mod != null ? "[lightgray]" + map.mod.meta.displayName : "@builtin").color(Color.gray).padTop(3);
}
i++;
}
}
if(noMapsShown){
mapTable.add("@maps.none");
}
}
void showMapFilters(){
activeDialog = new BaseDialog("@editor.filters");
activeDialog.addCloseButton();
activeDialog.cont.table(menu -> {
menu.add("@editor.filters.mode").width(150f).left();
menu.table(t -> {
for(Gamemode mode : Gamemode.all){
TextureRegionDrawable icon = Vars.ui.getIcon("mode" + Strings.capitalize(mode.name()));
if(Core.atlas.isFound(icon.getRegion())){
t.button(icon, Styles.emptyTogglei, () -> {
if(modes.contains(mode)){
modes.remove(mode);
}else{
modes.add(mode);
}
rebuildMaps();
}).size(60f).checked(modes.contains(mode)).tooltip("@mode." + mode.name() + ".name");
}
}
}).padBottom(10f);
menu.row();
menu.add("@editor.filters.type").width(150f).left();
menu.table(Tex.button, t -> {
t.button("@custom", Styles.flatTogglet, () -> {
showCustom = !showCustom;
Core.settings.put("editorshowcustommaps", showCustom);
rebuildMaps();
}).size(150f, 60f).checked(showCustom);
t.button("@builtin", Styles.flatTogglet, () -> {
showBuiltIn = !showBuiltIn;
Core.settings.put("editorshowbuiltinmaps", showBuiltIn);
rebuildMaps();
}).size(150f, 60f).checked(showBuiltIn);
}).padBottom(10f);
menu.row();
menu.add("@editor.filters.search").width(150f).left();
menu.table(Tex.button, t -> {
t.button("@editor.filters.author", Styles.flatTogglet, () -> {
searchAuthor = !searchAuthor;
Core.settings.put("editorsearchauthor", searchAuthor);
rebuildMaps();
}).size(150f, 60f).checked(searchAuthor);
t.button("@editor.filters.description", Styles.flatTogglet, () -> {
searchDescription = !searchDescription;
Core.settings.put("editorsearchdescription", searchDescription);
rebuildMaps();
}).size(150f, 60f).checked(searchDescription);
});
});
activeDialog.show();
}
@Override
public Dialog show(){
super.show();
if(Core.app.isDesktop() && searchField != null){
Core.scene.setKeyboardFocus(searchField);
}
return this;
}
}

View File

@@ -12,7 +12,9 @@ import mindustry.ui.*;
import static mindustry.Vars.*;
public class MapPlayDialog extends BaseDialog{
CustomRulesDialog dialog = new CustomRulesDialog();
public @Nullable Runnable playListener;
CustomRulesDialog dialog = new CustomRulesDialog(true);
Rules rules;
Gamemode selectedGamemode = Gamemode.survival;
Map lastMap;
@@ -31,6 +33,10 @@ public class MapPlayDialog extends BaseDialog{
}
public void show(Map map){
show(map, false);
}
public void show(Map map, boolean playtesting){
this.lastMap = map;
title.setText(map.name());
cont.clearChildren();
@@ -46,27 +52,27 @@ public class MapPlayDialog extends BaseDialog{
rules = map.applyRules(selectedGamemode);
Table selmode = new Table();
selmode.add("@level.mode").colspan(4);
selmode.add("@level.mode").colspan(2);
selmode.row();
int i = 0;
Table modes = new Table();
selmode.table(Tex.button, modes -> {
int i = 0;
for(Gamemode mode : Gamemode.all){
if(mode.hidden) continue;
for(Gamemode mode : Gamemode.values()){
if(mode.hidden) continue;
modes.button(mode.toString(), Styles.flatToggleMenut, () -> {
selectedGamemode = mode;
rules = map.applyRules(mode);
}).update(b -> b.setChecked(selectedGamemode == mode)).size(140f, mobile ? 44f : 54f).disabled(!mode.valid(map));
if(i++ % 2 == 1) modes.row();
}
});
modes.button(mode.toString(), Styles.togglet, () -> {
selectedGamemode = mode;
rules = map.applyRules(mode);
}).update(b -> b.setChecked(selectedGamemode == mode)).size(140f, 54f).disabled(!mode.valid(map));
if(i++ % 2 == 1) modes.row();
}
selmode.add(modes);
selmode.button("?", this::displayGameModeHelp).width(50f).fillY().padLeft(18f);
cont.add(selmode);
cont.row();
cont.button("@customize", Icon.settings, () -> dialog.show(rules, () -> rules = map.applyRules(selectedGamemode))).width(230);
cont.button("@customize", Icon.settings, () -> dialog.show(rules, () -> rules = map.applyRules(selectedGamemode))).height(50f).width(230);
cont.row();
cont.add(new BorderImage(map.safeTexture(), 3f)).size(mobile && !Core.graphics.isPortrait() ? 150f : 250f).get().setScaling(Scaling.fit);
//only maps with survival are valid for high scores
@@ -79,7 +85,8 @@ public class MapPlayDialog extends BaseDialog{
addCloseButton();
buttons.button("@play", Icon.play, () -> {
control.playMap(map, rules);
if(playListener != null) playListener.run();
control.playMap(map, rules, playtesting);
hide();
ui.custom.hide();
}).size(210f, 64f);
@@ -95,9 +102,9 @@ public class MapPlayDialog extends BaseDialog{
ScrollPane pane = new ScrollPane(table);
pane.setFadeScrollBars(false);
table.row();
for(Gamemode mode : Gamemode.values()){
for(Gamemode mode : Gamemode.all){
if(mode.hidden) continue;
table.labelWrap("[accent]" + mode.toString() + ":[] [lightgray]" + mode.description()).width(400f);
table.labelWrap("[accent]" + mode + ":[] [lightgray]" + mode.description()).width(400f);
table.row();
}

View File

@@ -1,70 +0,0 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.input.*;
import arc.scene.event.*;
import arc.scene.ui.layout.*;
import mindustry.gen.*;
import static mindustry.Vars.*;
public class MinimapDialog extends BaseDialog{
public MinimapDialog(){
super("@minimap");
setFillParent(true);
shown(this::setup);
addCloseButton();
shouldPause = true;
titleTable.remove();
onResize(this::setup);
}
void setup(){
cont.clear();
cont.table(Tex.pane,t -> {
t.rect((x, y, width, height) -> {
if(renderer.minimap.getRegion() == null) return;
Draw.color(Color.white);
Draw.alpha(parentAlpha);
Draw.rect(renderer.minimap.getRegion(), x + width / 2f, y + height / 2f, width, height);
if(renderer.minimap.getTexture() != null){
renderer.minimap.drawEntities(x, y, width, height);
}
}).grow();
}).size(Math.min(Core.graphics.getWidth() / 1.1f, Core.graphics.getHeight() / 1.3f) / Scl.scl(1f)).padTop(-20f);
cont.addListener(new InputListener(){
@Override
public boolean scrolled(InputEvent event, float x, float y, float amountx, float amounty){
renderer.minimap.zoomBy(amounty);
return true;
}
});
cont.addListener(new ElementGestureListener(){
float lzoom = -1f;
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
lzoom = renderer.minimap.getZoom();
}
@Override
public void zoom(InputEvent event, float initialDistance, float distance){
if(lzoom < 0){
lzoom = renderer.minimap.getZoom();
}
renderer.minimap.setZoom(initialDistance / distance * lzoom);
}
});
Core.app.post(() -> Core.scene.setScrollFocus(cont));
}
}

View File

@@ -1,112 +1,183 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.Net.*;
import arc.files.*;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.Texture.*;
import arc.graphics.g2d.*;
import arc.input.*;
import arc.scene.style.*;
import arc.scene.ui.TextButton.*;
import arc.scene.ui.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import arc.util.Http.*;
import arc.util.io.*;
import arc.util.serialization.*;
import arc.util.serialization.Jval.*;
import mindustry.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.io.*;
import mindustry.mod.*;
import mindustry.mod.Mods.*;
import mindustry.ui.*;
import java.io.*;
import java.text.*;
import java.util.*;
import static mindustry.Vars.*;
public class ModsDialog extends BaseDialog{
private ObjectMap<String, TextureRegion> textureCache = new ObjectMap<>();
private float modImportProgress;
private String searchtxt = "";
private @Nullable Seq<ModListing> modList;
private boolean orderDate = true;
private BaseDialog currentContent;
private BaseDialog browser;
private Table browserTable;
private float scroll = 0f;
public ModsDialog(){
super("@mods");
addCloseButton();
browser = new BaseDialog("@mods.browser");
browser.cont.table(table -> {
table.left();
table.image(Icon.zoom);
table.field(searchtxt, res -> {
searchtxt = res;
rebuildBrowser();
}).growX().get();
table.button(Icon.list, Styles.emptyi, 32f, () -> {
orderDate = !orderDate;
rebuildBrowser();
}).update(b -> b.getStyle().imageUp = (orderDate ? Icon.list : Icon.star)).size(40f).get()
.addListener(new Tooltip(tip -> tip.label(() -> orderDate ? "@mods.browser.sortdate" : "@mods.browser.sortstars").left()));
}).fillX().padBottom(4);
browser.cont.row();
browser.cont.pane(tablebrow -> {
tablebrow.margin(10f).top();
browserTable = tablebrow;
}).scrollX(false);
browser.addCloseButton();
browser.makeButtonOverlay();
browser.onResize(this::rebuildBrowser);
buttons.button("@mods.guide", Icon.link, () -> Core.app.openURI(modGuideURL)).size(210, 64f);
shown(this::setup);
if(mobile){
onResize(this::setup);
if(!mobile){
buttons.button("@mods.openfolder", Icon.link, () -> Core.app.openFolder(modDirectory.absolutePath()));
}
shown(this::setup);
onResize(this::setup);
Events.on(ResizeEvent.class, event -> {
if(currentContent != null){
currentContent.hide();
currentContent = null;
}
});
hidden(() -> {
if(mods.requiresReload()){
reload();
}
});
shown(() -> Core.app.post(() -> {
Core.settings.getBoolOnce("modsalpha", () -> {
ui.showText("@mods", "@mods.alphainfo");
});
}));
}
void modError(Throwable error){
ui.loadfrag.hide();
if(Strings.getCauses(error).contains(t -> t.getMessage() != null && (t.getMessage().contains("trust anchor") || t.getMessage().contains("SSL") || t.getMessage().contains("protocol")))){
if(error instanceof NoSuchMethodError || Strings.getCauses(error).contains(t -> t.getMessage() != null && (t.getMessage().contains("trust anchor") || t.getMessage().contains("SSL") || t.getMessage().contains("protocol")))){
ui.showErrorMessage("@feature.unsupported");
}else if(error instanceof HttpStatusException st){
ui.showErrorMessage(Core.bundle.format("connectfail", Strings.capitalize(st.status.toString().toLowerCase())));
}else if(error.getMessage() != null && error.getMessage().toLowerCase(Locale.ROOT).contains("writable dex")){
ui.showException("@error.moddex", error);
}else{
ui.showException(error);
}
}
void getModList(Cons<Seq<ModListing>> listener){
if(modList == null){
Core.net.httpGet("https://raw.githubusercontent.com/Anuken/MindustryMods/master/mods.json", response -> {
String strResult = response.getResultAsString();
var status = response.getStatus();
void getModList(int index, Cons<Seq<ModListing>> listener){
if(index >= modJsonURLs.length) return;
if(modList != null){
listener.get(modList);
return;
}
Http.get(modJsonURLs[index], response -> {
String strResult = response.getResultAsString();
Core.app.post(() -> {
try{
modList = JsonIO.json.fromJson(Seq.class, ModListing.class, strResult);
var d = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
Func<String, Date> parser = text -> {
try{
return d.parse(text);
}catch(Exception e){
return new Date();
}
};
modList.sortComparing(m -> parser.get(m.lastUpdated)).reverse();
listener.get(modList);
}catch(Exception e){
Log.err(e);
ui.showException(e);
}
});
}, error -> {
if(index < modJsonURLs.length - 1){
getModList(index + 1, listener);
}else{
Core.app.post(() -> {
if(status != HttpStatus.OK){
ui.showErrorMessage(Core.bundle.format("connectfail", status));
}else{
modList = new Json().fromJson(Seq.class, ModListing.class, strResult);
//potentially sort mods by game version compatibility, or other criteria
//modList.sort(Structs.comparingBool(m -> !Version.isAtLeast(m.minGameVersion)));
listener.get(modList);
modError(error);
if(browser != null){
browser.hide();
}
});
}, error -> Core.app.post(() -> ui.showException(error)));
}else{
listener.get(modList);
}
}
});
}
void setup(){
float h = 110f;
float w = mobile ? 440f : 524f;
float w = Math.min(Core.graphics.getWidth() / Scl.scl(1.05f), 520f);
cont.clear();
cont.defaults().width(mobile ? 500 : 560f).pad(4);
cont.defaults().width(Math.min(Core.graphics.getWidth() / Scl.scl(1.05f), 556f)).pad(4);
cont.add("@mod.reloadrequired").visible(mods::requiresReload).center().get().setAlignment(Align.center);
cont.row();
cont.table(buttons -> {
buttons.left().defaults().growX().height(60f).uniformX();
TextButtonStyle style = Styles.clearPartialt;
TextButtonStyle style = Styles.flatBordert;
float margin = 12f;
buttons.button("@mod.import", Icon.add, style, () -> {
BaseDialog dialog = new BaseDialog("@mod.import");
TextButtonStyle bstyle = Styles.cleart;
TextButtonStyle bstyle = Styles.flatt;
dialog.cont.table(Tex.button, t -> {
t.defaults().size(300f, 70f);
@@ -116,21 +187,12 @@ public class ModsDialog extends BaseDialog{
dialog.hide();
platform.showMultiFileChooser(file -> {
Runnable go = () -> {
try{
mods.importMod(file);
setup();
}catch(IOException e){
ui.showException(e);
e.printStackTrace();
}
};
//show unsafe jar file warning
if(file.extEquals("jar")){
ui.showConfirm("@warning", "@mod.jarwarn", go);
}else{
go.run();
try{
mods.importMod(file);
setup();
}catch(Exception e){
ui.showException(e.getMessage() != null && e.getMessage().toLowerCase(Locale.ROOT).contains("writable dex") ? "@error.moddex" : "", e);
Log.err(e);
}
}, "zip", "jar");
}).margin(12f);
@@ -141,81 +203,15 @@ public class ModsDialog extends BaseDialog{
dialog.hide();
ui.showTextInput("@mod.import.github", "", 64, Core.settings.getString("lastmod", ""), text -> {
//clean up the text in case somebody inputs a URL or adds random spaces
text = text.trim().replace(" ", "");
if(text.startsWith("https://github.com/")) text = text.substring("https://github.com/".length());
Core.settings.put("lastmod", text);
ui.loadfrag.show();
githubImportMod(text);
//there's no good way to know if it's a java mod here, so assume it's not
githubImportMod(text, false, null);
});
}).margin(12f);
t.row();
t.button("@mod.featured.dialog.title", Icon.star, bstyle, () -> {
Runnable[] rebuildBrowser = {null};
dialog.hide();
BaseDialog browser = new BaseDialog("$mod.featured.dialog.title");
browser.cont.table(table -> {
table.left();
table.image(Icon.zoom);
table.field(searchtxt, res -> {
searchtxt = res;
rebuildBrowser[0].run();
}).growX().get();
}).fillX().padBottom(4);
browser.cont.row();
browser.cont.pane(tablebrow -> {
tablebrow.margin(10f).top();
rebuildBrowser[0] = () -> {
tablebrow.clear();
tablebrow.add("@loading");
getModList(listings -> {
tablebrow.clear();
for(ModListing mod : listings){
if(mod.hasJava || !searchtxt.isEmpty() && !mod.repo.contains(searchtxt) || (Vars.ios && mod.hasScripts)) continue;
tablebrow.button(btn -> {
btn.top().left();
btn.margin(12f);
btn.table(con -> {
con.left();
con.add("[accent]" + mod.name + "[white]\n[lightgray]Author:[] " + mod.author + "\n[lightgray]\uE809 " + mod.stars +
(Version.isAtLeast(mod.minGameVersion) ? "" : "\n" + Core.bundle.format("mod.requiresversion", mod.minGameVersion)))
.width(388f).wrap().growX().pad(0f, 6f, 0f, 6f).left().labelAlign(Align.left);
con.add().growX().pad(0f, 6f, 0f, 6f);
}).fillY().growX().pad(0f, 6f, 0f, 6f);
}, Styles.modsb, () -> {
var sel = new BaseDialog(mod.name);
sel.cont.add(mod.description).width(mobile ? 400f : 500f).wrap().pad(4f).labelAlign(Align.center, Align.left);
sel.buttons.defaults().size(150f, 54f).pad(2f);
sel.setFillParent(false);
sel.buttons.button("@back", Icon.left, () -> {
sel.clear();
sel.hide();
});
sel.buttons.button("@mods.browser.add", Icon.download, () -> {
sel.hide();
githubImportMod(mod.repo);
});
sel.buttons.button("@mods.github.open", Icon.link, () -> {
Core.app.openURI("https://github.com/" + mod.repo);
});
sel.keyDown(KeyCode.escape, sel::hide);
sel.keyDown(KeyCode.back, sel::hide);
sel.show();
}).width(480f).growX().left().fillY();
tablebrow.row();
}
});
};
rebuildBrowser[0].run();
});
browser.addCloseButton();
browser.show();
}).margin(12f);
});
dialog.addCloseButton();
@@ -223,96 +219,118 @@ public class ModsDialog extends BaseDialog{
}).margin(margin);
if(!mobile){
buttons.button("@mods.openfolder", Icon.link, style, () -> Core.app.openFolder(modDirectory.absolutePath())).margin(margin);
}
buttons.button("@mods.browser", Icon.menu, style, this::showModBrowser).margin(margin);
}).width(w);
cont.row();
if(!mods.list().isEmpty()){
boolean[] anyDisabled = {false};
SearchBar.add(cont, mods.list(),
mod -> mod.meta.displayName(),
(table, mod) -> {
if(!mod.enabled() && !anyDisabled[0] && mods.list().size > 0){
anyDisabled[0] = true;
table.row();
table.image().growX().height(4f).pad(6f).color(Pal.gray);
table.row();
}
Table[] pane = {null};
table.button(t -> {
t.top().left();
t.margin(12f);
Cons<String> rebuild = query -> {
pane[0].clear();
boolean any = false;
for(LoadedMod item : mods.list()){
if(Strings.matches(query, item.meta.displayName)){
any = true;
if(!item.enabled() && !anyDisabled[0] && mods.list().size > 0){
anyDisabled[0] = true;
pane[0].row();
pane[0].image().growX().height(4f).pad(6f).color(Pal.gray).row();
}
t.defaults().left().top();
t.table(title -> {
title.left();
pane[0].button(t -> {
t.top().left();
t.margin(12f);
title.add(new BorderImage(){{
if(mod.iconTexture != null){
setDrawable(new TextureRegion(mod.iconTexture));
}else{
setDrawable(Tex.nomap);
}
border(Pal.accent);
}}).size(h - 8f).padTop(-8f).padLeft(-8f).padRight(8f);
String stateDetails = getStateDetails(item);
if(stateDetails != null){
t.addListener(new Tooltip(f -> f.background(Styles.black8).margin(4f).add(stateDetails).growX().width(400f).wrap()));
}
title.table(text -> {
boolean hideDisabled = !mod.isSupported() || mod.hasUnmetDependencies() || mod.hasContentErrors();
t.defaults().left().top();
t.table(title1 -> {
title1.left();
text.add("" + mod.meta.displayName() + "\n[lightgray]v" + mod.meta.version + (mod.enabled() || hideDisabled ? "" : "\n" + Core.bundle.get("mod.disabled") + ""))
title1.add(new BorderImage(){{
if(item.iconTexture != null){
setDrawable(new TextureRegion(item.iconTexture));
}else{
setDrawable(Tex.nomap);
}
border(Pal.accent);
}}).size(h - 8f).padTop(-8f).padLeft(-8f).padRight(8f);
title1.table(text -> {
boolean hideDisabled = !item.isSupported() || item.hasUnmetDependencies() || item.hasContentErrors();
String shortDesc = item.meta.shortDescription();
text.add("[accent]" + Strings.stripColors(item.meta.displayName) + "\n" +
(shortDesc.length() > 0 ? "[lightgray]" + shortDesc + "\n" : "")
//so does anybody care about version?
//+ "[gray]v" + Strings.stripColors(trimText(item.meta.version)) + "\n"
+ (item.enabled() || hideDisabled ? "" : Core.bundle.get("mod.disabled") + ""))
.wrap().top().width(300f).growX().left();
text.row();
if(mod.isOutdated()){
text.labelWrap("@mod.outdated").growX();
text.row();
}else if(!mod.isSupported()){
text.labelWrap(Core.bundle.format("mod.requiresversion", mod.meta.minGameVersion)).growX();
text.row();
}else if(mod.hasUnmetDependencies()){
text.labelWrap(Core.bundle.format("mod.missingdependencies", mod.missingDependencies.toString(", "))).growX();
t.row();
}else if(mod.hasContentErrors()){
text.labelWrap("@mod.erroredcontent").growX();
text.row();
}
}).top().growX();
title.add().growX();
}).growX().growY().left();
String state = getStateText(item);
if(state != null){
text.labelWrap(state).growX().row();
}
}).top().growX();
t.table(right -> {
right.right();
right.button(mod.enabled() ? Icon.downOpen : Icon.upOpen, Styles.clearPartiali, () -> {
mods.setEnabled(mod, !mod.enabled());
setup();
}).size(50f).disabled(!mod.isSupported());
title1.add().growX();
}).growX().growY().left();
right.button(mod.hasSteamID() ? Icon.link : Icon.trash, Styles.clearPartiali, () -> {
if(!mod.hasSteamID()){
ui.showConfirm("@confirm", "@mod.remove.confirm", () -> {
mods.removeMod(mod);
setup();
});
}else{
platform.viewListing(mod);
}
}).size(50f);
t.table(right -> {
right.right();
right.button(item.enabled() ? Icon.downOpen : Icon.upOpen, Styles.clearNonei, () -> {
mods.setEnabled(item, !item.enabled());
setup();
}).size(50f).disabled(!item.isSupported());
if(steam && !mod.hasSteamID()){
right.row();
right.button(Icon.download, Styles.clearTransi, () -> {
platform.publish(mod);
right.button(item.hasSteamID() ? Icon.link : Icon.trash, Styles.clearNonei, () -> {
if(!item.hasSteamID()){
ui.showConfirm("@confirm", "@mod.remove.confirm", () -> {
mods.removeMod(item);
setup();
});
}else{
platform.viewListing(item);
}
}).size(50f);
}
}).growX().right().padRight(-8f).padTop(-8f);
}, Styles.clearPartialt, () -> showMod(mod)).size(w, h).growX().pad(4f);
table.row();
}, !mobile || Core.graphics.isPortrait()).margin(10f).top();
if(steam && !item.hasSteamID()){
right.row();
right.button(Icon.export, Styles.clearNonei, () -> {
platform.publish(item);
}).size(50f);
}
}).growX().right().padRight(-8f).padTop(-8f);
}, Styles.flatBordert, () -> showMod(item)).size(w, h).growX().pad(4f);
pane[0].row();
}
}
if(!any){
pane[0].add("@none.found").color(Color.lightGray).pad(4);
}
};
if(!mobile || Core.graphics.isPortrait()){
cont.table(search -> {
search.image(Icon.zoom).padRight(8f);
search.field("", rebuild).growX();
}).fillX().padBottom(4);
}
cont.row();
cont.pane(table1 -> {
pane[0] = table1.margin(10f).top();
rebuild.get("");
}).scrollX(false).update(s -> scroll = s.getScrollY()).get().setScrollYForce(scroll);
}else{
cont.table(Styles.black6, t -> t.add("@mods.none")).height(80f);
}
@@ -320,12 +338,55 @@ public class ModsDialog extends BaseDialog{
cont.row();
}
private @Nullable String getStateText(LoadedMod item){
if(item.isOutdated()){
return "@mod.incompatiblemod";
}else if(item.isBlacklisted()){
return "@mod.blacklisted";
}else if(!item.isSupported()){
return "@mod.incompatiblegame";
}else if(item.state == ModState.circularDependencies){
return "@mod.circulardependencies";
}else if(item.state == ModState.incompleteDependencies){
return "@mod.incompletedependencies";
}else if(item.hasUnmetDependencies()){
return "@mod.unmetdependencies";
}else if(item.hasContentErrors()){
return "@mod.erroredcontent";
}else if(item.meta.hidden){
return "@mod.multiplayer.compatible";
}
return null;
}
private @Nullable String getStateDetails(LoadedMod item){
if(item.isOutdated()){
return "@mod.outdatedv7.details";
}else if(item.isBlacklisted()){
return "@mod.blacklisted.details";
}else if(!item.isSupported()){
return Core.bundle.format("mod.requiresversion.details", item.meta.minGameVersion);
}else if(item.state == ModState.circularDependencies){
return "@mod.circulardependencies.details";
}else if(item.state == ModState.incompleteDependencies){
return Core.bundle.format("mod.incompletedependencies.details", item.missingDependencies.toString(", "));
}else if(item.hasUnmetDependencies()){
return Core.bundle.format("mod.missingdependencies.details", item.missingDependencies.toString(", "));
}else if(item.hasContentErrors()){
return "@mod.erroredcontent.details";
}
return null;
}
private void reload(){
ui.showInfo("@mods.reloadexit", () -> Core.app.exit());
ui.showInfoOnHidden("@mods.reloadexit", () -> {
Log.info("Exiting to reload mods.");
Core.app.exit();
});
}
private void showMod(LoadedMod mod){
BaseDialog dialog = new BaseDialog(mod.meta.displayName());
BaseDialog dialog = new BaseDialog(mod.meta.displayName);
dialog.addCloseButton();
@@ -333,14 +394,20 @@ public class ModsDialog extends BaseDialog{
dialog.buttons.button("@mods.openfolder", Icon.link, () -> Core.app.openFolder(mod.file.absolutePath()));
}
//TODO improve this menu later
if(mod.getRepo() != null){
boolean showImport = !mod.hasSteamID();
dialog.buttons.button("@mods.github.open", Icon.link, () -> Core.app.openURI("https://github.com/" + mod.getRepo()));
if(mobile && showImport) dialog.buttons.row();
if(showImport) dialog.buttons.button("@mods.browser.reinstall", Icon.download, () -> githubImportMod(mod.getRepo(), mod.isJava(), null));
}
dialog.cont.pane(desc -> {
desc.center();
desc.defaults().padTop(10).left();
desc.add("@editor.name").padRight(10).color(Color.gray).padTop(0);
desc.row();
desc.add(mod.meta.displayName()).growX().wrap().padTop(2);
desc.add(mod.meta.displayName).growX().wrap().padTop(2);
desc.row();
if(mod.meta.author != null){
desc.add("@editor.author").padRight(10).color(Color.gray);
@@ -348,6 +415,12 @@ public class ModsDialog extends BaseDialog{
desc.add(mod.meta.author).growX().wrap().padTop(2);
desc.row();
}
if(mod.meta.version != null){
desc.add("@mod.version").padRight(10).color(Color.gray).top();
desc.row();
desc.add(mod.meta.version).growX().wrap().padTop(2);
desc.row();
}
if(mod.meta.description != null){
desc.add("@editor.description").padRight(10).color(Color.gray).top();
desc.row();
@@ -355,41 +428,230 @@ public class ModsDialog extends BaseDialog{
desc.row();
}
String state = getStateDetails(mod);
if(state != null){
desc.add("@mod.disabled").padTop(13f).padBottom(-6f).row();
desc.add(state).growX().wrap().row();
}
}).width(400f);
//TODO maybe enable later
if(false){
Seq<UnlockableContent> all = Seq.with(content.getContentMap()).<Content>flatten().select(c -> c.minfo.mod == mod && c instanceof UnlockableContent).as();
if(all.any()){
dialog.cont.row();
dialog.cont.pane(cs -> {
Seq<UnlockableContent> all = Seq.with(content.getContentMap()).<Content>flatten().select(c -> c.minfo.mod == mod && c instanceof UnlockableContent u && !u.isHidden()).as();
if(all.any()){
dialog.cont.row();
dialog.cont.button("@mods.viewcontent", Icon.book, () -> {
BaseDialog d = new BaseDialog(mod.meta.displayName);
d.cont.pane(cs -> {
int i = 0;
for(UnlockableContent c : all){
cs.button(new TextureRegionDrawable(c.icon(Cicon.medium)), Styles.cleari, Cicon.medium.size, () -> {
cs.button(new TextureRegionDrawable(c.uiIcon), Styles.flati, iconMed, () -> {
ui.content.show(c);
}).size(50f).with(im -> {
var click = im.getClickListener();
im.update(() -> im.getImage().color.lerp(!click.isOver() ? Color.lightGray : Color.white, 0.4f * Time.delta));
});
if(++i % 8 == 0) cs.row();
}).tooltip(c.localizedName);
if(++i % (int)Math.min(Core.graphics.getWidth() / Scl.scl(110), 14) == 0) cs.row();
}
}).growX().minHeight(60f);
}
}).grow();
d.addCloseButton();
d.show();
currentContent = d;
}).size(300, 50).pad(4);
}
dialog.show();
}
private void showModBrowser(){
rebuildBrowser();
browser.show();
}
private void rebuildBrowser(){
ObjectSet<String> installed = mods.list().map(m -> m.getRepo()).asSet();
browserTable.clear();
browserTable.add("@loading");
int cols = (int)Math.max(Core.graphics.getWidth() / Scl.scl(480), 1);
getModList(0, rlistings -> {
browserTable.clear();
int i = 0;
var listings = rlistings;
if(!orderDate){
listings = rlistings.copy();
listings.sortComparing(m1 -> -m1.stars);
}
for(ModListing mod : listings){
if(((mod.hasJava || mod.hasScripts) && Vars.ios) ||
(!Strings.matches(searchtxt, mod.name) && !Strings.matches(searchtxt, mod.repo))
//hack, I'm basically testing if 135.10 >= modVersion, which is equivalent to modVersion >= 136
|| (Version.isAtLeast(135, 10, mod.minGameVersion))
) continue;
float s = 64f;
browserTable.button(con -> {
con.margin(0f);
con.left();
String repo = mod.repo;
con.add(new BorderImage(){
TextureRegion last;
{
border(installed.contains(repo) ? Pal.accent : Color.lightGray);
setDrawable(Tex.nomap);
pad = Scl.scl(4f);
}
@Override
public void draw(){
super.draw();
//textures are only requested when the rendering happens; this assists with culling
if(!textureCache.containsKey(repo)){
textureCache.put(repo, last = Core.atlas.find("nomap"));
Http.get("https://raw.githubusercontent.com/Anuken/MindustryMods/master/icons/" + repo.replace("/", "_"), res -> {
Pixmap pix = new Pixmap(res.getResult());
Core.app.post(() -> {
try{
var tex = new Texture(pix);
tex.setFilter(TextureFilter.linear);
textureCache.put(repo, new TextureRegion(tex));
pix.dispose();
}catch(Exception e){
Log.err(e);
}
});
}, err -> {});
}
var next = textureCache.get(repo);
if(last != next){
last = next;
setDrawable(next);
}
}
}).size(s).pad(4f * 2f);
con.add(
"[accent]" + mod.name.replace("\n", "") +
(installed.contains(mod.repo) ? "\n[lightgray]" + Core.bundle.get("mod.installed") : "") +
"\n[lightgray]\uE809 " + mod.stars +
(Version.isAtLeast(mod.minGameVersion) ? "" :
"\n" + Core.bundle.format("mod.requiresversion", mod.minGameVersion)))
.width(358f).wrap().grow().pad(4f, 2f, 4f, 6f).top().left().labelAlign(Align.topLeft);
}, Styles.flatBordert, () -> {
var sel = new BaseDialog(mod.name);
sel.cont.pane(p -> p.add(mod.description + "\n\n[accent]" + Core.bundle.get("editor.author") + "[lightgray] " + mod.author)
.width(mobile ? 400f : 500f).wrap().pad(4f).labelAlign(Align.center, Align.left)).grow();
sel.buttons.defaults().size(150f, 54f).pad(2f);
sel.buttons.button("@back", Icon.left, () -> {
sel.clear();
sel.hide();
});
var found = mods.list().find(l -> mod.repo != null && mod.repo.equals(l.getRepo()));
sel.buttons.button(found == null ? "@mods.browser.add" : "@mods.browser.reinstall", Icon.download, () -> {
sel.hide();
githubImportMod(mod.repo, mod.hasJava, null);
});
if(Core.graphics.isPortrait()){
sel.buttons.row();
}
sel.buttons.button("@mods.github.open", Icon.link, () -> {
Core.app.openURI("https://github.com/" + mod.repo);
});
sel.buttons.button("@mods.browser.view-releases", Icon.zoom, () -> {
BaseDialog load = new BaseDialog("");
load.cont.add("[accent]Fetching Releases...");
load.show();
Http.get(ghApi + "/repos/" + mod.repo + "/releases", res -> {
var json = Jval.read(res.getResultAsString());
JsonArray releases = json.asArray();
Core.app.post(() -> {
load.hide();
if(releases.size == 0){
ui.showInfo("@mods.browser.noreleases");
}else{
sel.hide();
var downloads = new BaseDialog("@mods.browser.releases");
downloads.cont.pane(p -> {
for(int j = 0; j < releases.size; j++){
var release = releases.get(j);
int index = j;
p.table(((TextureRegionDrawable)Tex.whiteui).tint(Pal.darkestGray), t -> {
t.add("[accent]" + release.getString("name") + (index == 0 ? " " + Core.bundle.get("mods.browser.latest") : "")).top().left().growX().wrap().pad(5f);
t.row();
t.add((release.getString("published_at")).substring(0, 10).replaceAll("-", "/")).top().left().growX().wrap().pad(5f).color(Color.gray);
t.row();
t.table(b -> {
b.defaults().size(150f, 54f).pad(2f);
b.button("@mods.github.open-release", Icon.link, () -> Core.app.openURI(release.getString("html_url")));
b.button("@mods.browser.add", Icon.download, () -> {
String releaseUrl = release.getString("url");
githubImportMod(mod.repo, mod.hasJava, releaseUrl.substring(releaseUrl.lastIndexOf("/") + 1));
});
}).right();
}).margin(5f).growX().pad(5f);
if(j < releases.size - 1) p.row();
}
}).width(500f).scrollX(false).fillY();
downloads.buttons.button("@back", Icon.left, () -> {
downloads.clear();
downloads.hide();
sel.show();
}).size(150f, 54f).pad(2f);
downloads.keyDown(KeyCode.escape, downloads::hide);
downloads.keyDown(KeyCode.back, downloads::hide);
downloads.hidden(sel::show);
downloads.show();
}
});
}, t -> Core.app.post(() -> {
modError(t);
load.hide();
}));
});
sel.keyDown(KeyCode.escape, sel::hide);
sel.keyDown(KeyCode.back, sel::hide);
sel.show();
}).width(438f).pad(4).growX().left().height(s + 8*2f).fillY();
if(++i % cols == 0) browserTable.row();
}
});
}
private void handleMod(String repo, HttpResponse result){
try{
Fi file = tmpDirectory.child(repo.replace("/", "") + ".zip");
Streams.copy(result.getResultAsStream(), file.write(false));
mods.importMod(file);
long len = result.getContentLength();
Floatc cons = len <= 0 ? f -> {} : p -> modImportProgress = p;
try(var stream = file.write(false)){
Streams.copyProgress(result.getResultAsStream(), stream, len, 4096, cons);
}
var mod = mods.importMod(file);
mod.setRepo(repo);
file.delete();
Core.app.post(() -> {
try{
setup();
ui.loadfrag.hide();
@@ -402,36 +664,83 @@ public class ModsDialog extends BaseDialog{
}
}
private void githubImportMod(String name){
//try several branches
//TODO use only the main branch as specified in meta
githubImportBranch("6.0", name, e1 -> {
githubImportBranch("master", name, e2 -> {
githubImportBranch("main", name, e3 -> {
ui.showErrorMessage(Core.bundle.format("connectfail", e2));
ui.loadfrag.hide();
});
});
});
private void importFail(Throwable t){
Core.app.post(() -> modError(t));
}
private void githubImportBranch(String branch, String repo, Cons<HttpStatus> err){
Core.net.httpGet("https://api.github.com/repos/" + repo + "/zipball/" + branch, loc -> {
if(loc.getStatus() == HttpStatus.OK){
if(loc.getHeader("Location") != null){
Core.net.httpGet(loc.getHeader("Location"), result -> {
if(result.getStatus() != HttpStatus.OK){
err.get(result.getStatus());
}else{
public void githubImportMod(String repo, boolean isJava){
githubImportMod(repo, isJava, null);
}
public void githubImportMod(String repo, boolean isJava, @Nullable String release){
modImportProgress = 0f;
ui.loadfrag.show("@downloading");
ui.loadfrag.setProgress(() -> modImportProgress);
if(isJava){
githubImportJavaMod(repo, release);
}else{
Http.get(ghApi + "/repos/" + repo, res -> {
var json = Jval.read(res.getResultAsString());
String mainBranch = json.getString("default_branch");
String language = json.getString("language", "<none>");
//this is a crude heuristic for class mods; only required for direct github import
//TODO make a more reliable way to distinguish java mod repos
if(language.equals("Java") || language.equals("Kotlin")){
githubImportJavaMod(repo, release);
}else{
githubImportBranch(mainBranch, repo, release);
}
}, this::importFail);
}
}
private void githubImportJavaMod(String repo, @Nullable String release){
//grab latest release
Http.get(ghApi + "/repos/" + repo + "/releases/" + (release == null ? "latest" : release), res -> {
var json = Jval.read(res.getResultAsString());
var assets = json.get("assets").asArray();
//prioritize dexed jar, as that's what Sonnicon's mod template outputs
var dexedAsset = assets.find(j -> j.getString("name").startsWith("dexed") && j.getString("name").endsWith(".jar"));
var asset = dexedAsset == null ? assets.find(j -> j.getString("name").endsWith(".jar")) : dexedAsset;
if(asset != null){
//grab actual file
var url = asset.getString("browser_download_url");
Http.get(url, result -> handleMod(repo, result), this::importFail);
}else{
throw new ArcRuntimeException("No JAR file found in releases. Make sure you have a valid jar file in the mod's latest Github Release.");
}
}, this::importFail);
}
private void githubImportBranch(String branch, String repo, @Nullable String release){
if(release != null) {
Http.get(ghApi + "/repos/" + repo + "/releases/" + release, res -> {
String zipUrl = Jval.read(res.getResultAsString()).getString("zipball_url");
Http.get(zipUrl, loc -> {
if(loc.getHeader("Location") != null){
Http.get(loc.getHeader("Location"), result -> {
handleMod(repo, result);
}
}, t2 -> Core.app.post(() -> modError(t2)));
}, this::importFail);
}else{
handleMod(repo, loc);
}
}, this::importFail);
});
}else{
Http.get(ghApi + "/repos/" + repo + "/zipball/" + branch, loc -> {
if(loc.getHeader("Location") != null){
Http.get(loc.getHeader("Location"), result -> {
handleMod(repo, result);
}, this::importFail);
}else{
handleMod(repo, loc);
}
}else{
err.get(loc.getStatus());
}
}, t2 -> Core.app.post(() -> modError(t2)));
}, this::importFail);
}
}
}

View File

@@ -21,7 +21,7 @@ public class PaletteDialog extends Dialog{
for(int i = 0; i < playerColors.length; i++){
Color color = playerColors[i];
ImageButton button = table.button(Tex.whiteui, Styles.clearTogglei, 34, () -> {
ImageButton button = table.button(Tex.whiteui, Styles.squareTogglei, 34, () -> {
cons.get(color);
hide();
}).size(48).get();

View File

@@ -1,19 +1,40 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.scene.ui.layout.*;
import mindustry.*;
import mindustry.editor.*;
import mindustry.game.*;
import mindustry.gen.*;
import static mindustry.Vars.*;
public class PausedDialog extends BaseDialog{
private MapProcessorsDialog processors = new MapProcessorsDialog();
private SaveDialog save = new SaveDialog();
private LoadDialog load = new LoadDialog();
private boolean wasClient = false;
private CustomRulesDialog rulesDialog = new CustomRulesDialog();
public PausedDialog(){
super("@menu");
shouldPause = true;
clearChildren();
add(titleTable).growX().row();
stack(cont, new Table(t -> {
t.bottom().left();
t.button(Icon.book, () -> {
Rules toEdit = Vars.state.rules.copy();
rulesDialog.show(toEdit, () -> state.rules.copy());
rulesDialog.hidden(() -> {
//apply rule changes only once it is hidden
Vars.state.rules = toEdit;
Call.setRules(toEdit);
});
}).size(70f).tooltip("@customize").visible(() -> state.rules.allowEditRules && (net.server() || !net.active()));
})).grow().row();
shown(this::rebuild);
addCloseListener();
@@ -32,6 +53,12 @@ public class PausedDialog extends BaseDialog{
float dw = 220f;
cont.defaults().width(dw).height(55).pad(5f);
cont.button("@objective", Icon.info, () -> ui.fullText.show("@objective", state.rules.sector.preset.description))
.visible(() -> state.rules.sector != null && state.rules.sector.preset != null && state.rules.sector.preset.description != null).padTop(-60f);
cont.button("@abandon", Icon.cancel, () -> ui.planet.abandonSectorConfirm(state.rules.sector, this::hide)).padTop(-60f)
.disabled(b -> net.client()).visible(() -> state.rules.sector != null).row();
cont.button("@back", Icon.left, this::hide).name("back");
cont.button("@settings", Icon.settings, ui.settings::show).name("settings");
@@ -43,21 +70,26 @@ public class PausedDialog extends BaseDialog{
cont.row();
cont.button("@hostserver", Icon.host, () -> {
//the button runs out of space when the editor button is added, so use the mobile text
cont.button(state.isEditor() ? "@hostserver.mobile" : "@hostserver", Icon.host, () -> {
if(net.server() && steam){
platform.inviteFriends();
}else{
if(steam){
ui.host.runHost();
}else{
ui.host.show();
}
ui.host.show();
}
}).disabled(b -> !((steam && net.server()) || !net.active())).colspan(2).width(dw * 2 + 20f).update(e -> e.setText(net.server() && steam ? "@invitefriends" : "@hostserver"));
}).disabled(b -> !((steam && net.server()) || !net.active())).colspan(state.isEditor() ? 1 : 2).width(state.isEditor() ? dw : dw * 2 + 10f)
.update(e -> e.setText(net.server() && steam ? "@invitefriends" : state.isEditor() ? "@hostserver.mobile" : "@hostserver"));
if(state.isEditor()){
cont.button("@editor.worldprocessors", Icon.logic, () -> {
hide();
processors.show();
});
}
cont.row();
cont.button("@quit", Icon.exit, this::showQuitConfirm).colspan(2).width(dw + 20f).update(s -> s.setText(control.saves.getCurrent() != null && control.saves.getCurrent().isAutosave() ? "@save.quit" : "@quit"));
cont.button("@quit", Icon.exit, this::showQuitConfirm).colspan(2).width(dw + 10f).update(s -> s.setText(control.saves.getCurrent() != null && control.saves.getCurrent().isAutosave() ? "@save.quit" : "@quit"));
}else{
cont.defaults().size(130f).pad(5);
@@ -93,21 +125,41 @@ public class PausedDialog extends BaseDialog{
}
void showQuitConfirm(){
ui.showConfirm("@confirm", "@quit.confirm", () -> {
wasClient = net.client();
if(net.client()) netClient.disconnectQuietly();
Runnable quit = () -> {
runExitSave();
hide();
});
};
if(confirmExit){
ui.showConfirm("@confirm", "@quit.confirm", quit);
}else{
quit.run();
}
}
public boolean checkPlaytest(){
if(state.playtestingMap != null){
//no exit save here
var testing = state.playtestingMap;
logic.reset();
ui.editor.resumeAfterPlaytest(testing);
return true;
}
return false;
}
public void runExitSave(){
boolean wasClient = net.client();
if(net.client()) netClient.disconnectQuietly();
if(state.isEditor() && !wasClient){
ui.editor.resumeEditing();
return;
}else if(checkPlaytest()){
return;
}
if(control.saves.getCurrent() == null || !control.saves.getCurrent().isAutosave() || wasClient){
if(control.saves.getCurrent() == null || !control.saves.getCurrent().isAutosave() || wasClient || state.gameOver || disableSave){
logic.reset();
return;
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ import arc.scene.actions.*;
import arc.scene.event.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.TextButton.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
@@ -30,85 +31,110 @@ import mindustry.ui.layout.TreeLayout.*;
import java.util.*;
import static mindustry.Vars.*;
import static mindustry.gen.Tex.*;
public class ResearchDialog extends BaseDialog{
public static boolean debugShowRequirements = false;
public final float nodeSize = Scl.scl(60f);
public ObjectSet<TechTreeNode> nodes = new ObjectSet<>();
public TechTreeNode root = new TechTreeNode(TechTree.root, null);
public TechTreeNode root = new TechTreeNode(TechTree.roots.first(), null);
public TechNode lastNode = root.node;
public Rect bounds = new Rect();
public ItemsDisplay itemDisplay;
public View view;
public ItemSeq items;
private boolean showTechSelect;
private boolean needsRebuild;
public ResearchDialog(){
super("");
Events.on(ResetEvent.class, e -> {
hide();
});
Events.on(UnlockEvent.class, e -> {
if(net.client() && !needsRebuild){
needsRebuild = true;
Core.app.post(() -> {
needsRebuild = false;
checkNodes(root);
view.hoverNode = null;
treeLayout();
view.rebuild();
Core.scene.act();
});
}
});
titleTable.remove();
titleTable.clear();
titleTable.top();
titleTable.button(b -> {
//TODO custom icon here.
b.imageDraw(() -> root.node.icon()).padRight(8).size(iconMed);
b.add().growX();
b.label(() -> root.node.localizedName()).color(Pal.accent);
b.add().growX();
b.add().size(iconMed);
}, () -> {
new BaseDialog("@techtree.select"){{
cont.pane(t -> {
t.table(Tex.button, in -> {
in.defaults().width(300f).height(60f);
for(TechNode node : TechTree.roots){
if(node.requiresUnlock && !node.content.unlockedHost() && node != getPrefRoot()) continue;
//TODO toggle
in.button(node.localizedName(), node.icon(), Styles.flatTogglet, iconMed, () -> {
if(node == lastNode){
return;
}
rebuildTree(node);
hide();
}).marginLeft(12f).checked(node == lastNode).row();
}
});
});
addCloseButton();
}}.show();
}).visible(() -> showTechSelect = TechTree.roots.count(node -> !(node.requiresUnlock && !node.content.unlockedHost())) > 1).minWidth(300f);
margin(0f).marginBottom(8);
cont.stack(view = new View(), itemDisplay = new ItemsDisplay()).grow();
cont.stack(titleTable, view = new View(), itemDisplay = new ItemsDisplay()).grow();
itemDisplay.visible(() -> !net.client());
titleTable.toFront();
shouldPause = true;
onResize(this::checkMargin);
shown(() -> {
checkMargin();
Core.app.post(this::checkMargin);
items = new ItemSeq(){
//store sector item amounts for modifications
ObjectMap<Sector, ItemSeq> cache = new ObjectMap<>();
Planet currPlanet = ui.planet.isShown() ?
ui.planet.state.planet :
state.isCampaign() ? state.rules.sector.planet : null;
{
//add global counts of each sector
for(Planet planet : content.planets()){
for(Sector sector : planet.sectors){
if(sector.hasSave() && sector.hasBase()){
ItemSeq cached = sector.items();
cache.put(sector, cached);
cached.each((item, amount) -> {
values[item.id] += Math.max(amount, 0);
total += Math.max(amount, 0);
});
}
}
}
}
//this is the only method that actually modifies the sequence itself.
@Override
public void add(Item item, int amount){
//only have custom removal logic for when the sequence gets items taken out of it (e.g. research)
if(amount < 0){
//remove items from each sector's storage, one by one
//negate amount since it's being *removed* - this makes it positive
amount = -amount;
//% that gets removed from each sector
double percentage = (double)amount / get(item);
int[] counter = {amount};
cache.each((sector, seq) -> {
if(counter[0] == 0) return;
//amount that will be removed
int toRemove = Math.min((int)Math.ceil(percentage * seq.get(item)), counter[0]);
//actually remove it from the sector
sector.removeItem(item, toRemove);
seq.remove(item, toRemove);
counter[0] -= toRemove;
});
//negate again to display correct number
amount = -amount;
}
super.add(item, amount);
}
};
if(currPlanet != null && currPlanet.techTree != null){
switchTree(currPlanet.techTree);
}
rebuildItems();
checkNodes(root);
treeLayout();
view.hoverNode = null;
view.infoTable.remove();
view.infoTable.clear();
});
hidden(ui.planet::setup);
@@ -172,6 +198,109 @@ public class ResearchDialog extends BaseDialog{
});
}
void checkMargin(){
if(Core.graphics.isPortrait() && showTechSelect){
itemDisplay.marginTop(60f);
}else{
itemDisplay.marginTop(0f);
}
itemDisplay.invalidate();
itemDisplay.layout();
}
public void rebuildItems(){
items = new ItemSeq(){
//store sector item amounts for modifications
ObjectMap<Sector, ItemSeq> cache = new ObjectMap<>();
{
//first, find a planet associated with the current tech tree
Planet rootPlanet = lastNode.planet != null ? lastNode.planet : content.planets().find(p -> p.techTree == lastNode);
//if there is no root, fall back to serpulo
if(rootPlanet == null) rootPlanet = Planets.serpulo;
//add global counts of each sector
for(Sector sector : rootPlanet.sectors){
if(sector.hasBase()){
ItemSeq cached = sector.items();
cache.put(sector, cached);
cached.each((item, amount) -> {
values[item.id] += Math.max(amount, 0);
total += Math.max(amount, 0);
});
}
}
}
//this is the only method that actually modifies the sequence itself.
@Override
public void add(Item item, int amount){
//only have custom removal logic for when the sequence gets items taken out of it (e.g. research)
if(amount < 0){
//remove items from each sector's storage, one by one
//negate amount since it's being *removed* - this makes it positive
amount = -amount;
//% that gets removed from each sector
double percentage = (double)amount / get(item);
int[] counter = {amount};
cache.each((sector, seq) -> {
if(counter[0] == 0) return;
//amount that will be removed
int toRemove = Math.min((int)Math.ceil(percentage * seq.get(item)), counter[0]);
//actually remove it from the sector
sector.removeItem(item, toRemove);
seq.remove(item, toRemove);
counter[0] -= toRemove;
});
//negate again to display correct number
amount = -amount;
}
super.add(item, amount);
}
};
itemDisplay.rebuild(items);
}
public @Nullable TechNode getPrefRoot(){
Planet currPlanet = ui.planet.isShown() ?
ui.planet.state.planet :
state.isCampaign() ? state.rules.sector.planet : null;
return currPlanet == null ? null : currPlanet.techTree;
}
public void switchTree(TechNode node){
if(lastNode == node || node == null) return;
nodes.clear();
root = new TechTreeNode(node, null);
lastNode = node;
view.rebuildAll();
rebuildItems();
}
public void rebuildTree(TechNode node){
switchTree(node);
view.panX = 0f;
view.panY = -200f;
view.setScale(1f);
view.hoverNode = null;
view.infoTable.remove();
view.infoTable.clear();
checkNodes(root);
treeLayout();
}
void treeLayout(){
float spacing = 20f;
LayoutNode node = new LayoutNode(root, null);
@@ -233,10 +362,10 @@ public class ResearchDialog extends BaseDialog{
void checkNodes(TechTreeNode node){
boolean locked = locked(node.node);
if(!locked) node.visible = true;
if(!locked && (node.parent == null || node.parent.visible)) node.visible = true;
node.selectable = selectable(node.node);
for(TechTreeNode l : node.children){
l.visible = !locked;
l.visible = !locked && l.parent.visible;
checkNodes(l);
}
@@ -244,11 +373,12 @@ public class ResearchDialog extends BaseDialog{
}
boolean selectable(TechNode node){
return node.content.unlocked() || !node.objectives.contains(i -> !i.complete());
//there's a desync here as far as sectors go, since the client doesn't know about that, but I'm not too concerned
return node.content.unlockedHost() || !node.objectives.contains(i -> !i.complete());
}
boolean locked(TechNode node){
return node.content.locked();
return !node.content.unlockedHost();
}
class LayoutNode extends TreeNode<LayoutNode>{
@@ -273,11 +403,9 @@ public class ResearchDialog extends BaseDialog{
this.parent = parent;
this.width = this.height = nodeSize;
nodes.add(this);
if(node.children != null){
children = new TechTreeNode[node.children.size];
for(int i = 0; i < children.length; i++){
children[i] = new TechTreeNode(node.children.get(i), this);
}
children = new TechTreeNode[node.children.size];
for(int i = 0; i < children.length; i++){
children[i] = new TechTreeNode(node.children.get(i), this);
}
}
}
@@ -289,36 +417,48 @@ public class ResearchDialog extends BaseDialog{
public Table infoTable = new Table();
{
rebuildAll();
}
public void rebuildAll(){
clear();
hoverNode = null;
infoTable.clear();
infoTable.touchable = Touchable.enabled;
for(TechTreeNode node : nodes){
ImageButton button = new ImageButton(node.node.content.icon(Cicon.medium), Styles.nodei);
ImageButton button = new ImageButton(node.node.content.uiIcon, Styles.nodei);
button.resizeImage(32f);
button.getImage().setScaling(Scaling.fit);
button.visible(() -> node.visible);
button.clicked(() -> {
if(moved) return;
if(!net.client()){
button.clicked(() -> {
if(moved) return;
if(mobile){
hoverNode = button;
rebuild();
float right = infoTable.getRight();
if(right > Core.graphics.getWidth()){
float moveBy = right - Core.graphics.getWidth();
addAction(new RelativeTemporalAction(){
{
setDuration(0.1f);
setInterpolation(Interp.fade);
}
if(mobile){
hoverNode = button;
rebuild();
float right = infoTable.getRight();
if(right > Core.graphics.getWidth()){
float moveBy = right - Core.graphics.getWidth();
addAction(new RelativeTemporalAction(){
{
setDuration(0.1f);
setInterpolation(Interp.fade);
}
@Override
protected void updateRelative(float percentDelta){
panX -= moveBy * percentDelta;
}
});
@Override
protected void updateRelative(float percentDelta){
panX -= moveBy * percentDelta;
}
});
}
}else if(canSpend(node.node) && locked(node.node)){
spend(node.node);
}
}else if(canSpend(node.node) && locked(node.node)){
spend(node.node);
}
});
});
}
button.hovered(() -> {
if(!mobile && hoverNode != button && node.visible){
hoverNode = button;
@@ -335,20 +475,21 @@ public class ResearchDialog extends BaseDialog{
button.userObject = node.node;
button.setSize(nodeSize);
button.update(() -> {
button.setDisabled(net.client() && !mobile);
float offset = (Core.graphics.getHeight() % 2) / 2f;
button.setPosition(node.x + panX + width / 2f, node.y + panY + height / 2f + offset, Align.center);
button.getStyle().up = !locked(node.node) ? Tex.buttonOver : !selectable(node.node) || !canSpend(node.node) ? Tex.buttonRed : Tex.button;
button.getStyle().up = !locked(node.node) ? Tex.buttonOver : !selectable(node.node) || (!canSpend(node.node) && !net.client()) ? Tex.buttonRed : Tex.button;
((TextureRegionDrawable)button.getStyle().imageUp).setRegion(node.selectable ? node.node.content.icon(Cicon.medium) : Icon.lock.getRegion());
((TextureRegionDrawable)button.getStyle().imageUp).setRegion(node.selectable ? node.node.content.uiIcon : Icon.lock.getRegion());
button.getImage().setColor(!locked(node.node) ? Color.white : node.selectable ? Color.gray : Pal.gray);
button.getImage().setScaling(Scaling.bounded);
button.getImage().layout();
});
addChild(button);
}
if(mobile){
tapped(() -> {
Element e = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true);
Element e = Core.scene.getHoverElement();
if(e == this){
hoverNode = null;
rebuild();
@@ -374,7 +515,7 @@ public class ResearchDialog extends BaseDialog{
}
boolean canSpend(TechNode node){
if(!selectable(node)) return false;
if(!selectable(node) || net.client()) return false;
if(node.requirements.length == 0) return true;
@@ -390,6 +531,8 @@ public class ResearchDialog extends BaseDialog{
}
void spend(TechNode node){
if(net.client()) return;
boolean complete = true;
boolean[] shine = new boolean[node.requirements.length];
@@ -425,6 +568,7 @@ public class ResearchDialog extends BaseDialog{
Core.scene.act();
rebuild(shine);
itemDisplay.rebuild(items, usedShine);
checkMargin();
}
void unlock(TechNode node){
@@ -479,97 +623,107 @@ public class ResearchDialog extends BaseDialog{
infoTable.table(b -> {
b.margin(0).left().defaults().left();
if(selectable && (node.content.description != null || node.content.stats.toMap().size > 0)){
b.button(Icon.info, Styles.cleari, () -> ui.content.show(node.content)).growY().width(50f);
if(selectable){
b.button(Icon.info, Styles.flati, () -> ui.content.show(node.content)).growY().width(50f);
}
b.add().grow();
b.table(desc -> {
desc.left().defaults().left();
desc.add(selectable ? node.content.localizedName : "[accent]???");
desc.row();
if(locked(node)){
if(locked(node) || (debugShowRequirements && !net.client())){
desc.table(t -> {
t.left();
if(selectable){
if(net.client()){
desc.add("@locked").color(Pal.remove);
}else{
desc.table(t -> {
t.left();
if(selectable){
//check if there is any progress, add research progress text
if(Structs.contains(node.finishedRequirements, s -> s.amount > 0)){
float sum = 0f, used = 0f;
boolean shiny = false;
//check if there is any progress, add research progress text
if(Structs.contains(node.finishedRequirements, s -> s.amount > 0)){
float sum = 0f, used = 0f;
boolean shiny = false;
for(int i = 0; i < node.requirements.length; i++){
sum += node.requirements[i].item.cost * node.requirements[i].amount;
used += node.finishedRequirements[i].item.cost * node.finishedRequirements[i].amount;
if(shine != null) shiny |= shine[i];
}
for(int i = 0; i < node.requirements.length; i++){
sum += node.requirements[i].item.cost * node.requirements[i].amount;
used += node.finishedRequirements[i].item.cost * node.finishedRequirements[i].amount;
if(shine != null) shiny |= shine[i];
}
Label label = t.add(Core.bundle.format("research.progress", Math.min((int)(used / sum * 100), 99))).left().get();
if(shiny){
label.setColor(Pal.accent);
label.actions(Actions.color(Color.lightGray, 0.75f, Interp.fade));
}else{
label.setColor(Color.lightGray);
}
t.row();
}
for(int i = 0; i < node.requirements.length; i++){
ItemStack req = node.requirements[i];
ItemStack completed = node.finishedRequirements[i];
//skip finished stacks
if(req.amount <= completed.amount) continue;
boolean shiny = shine != null && shine[i];
t.table(list -> {
int reqAmount = req.amount - completed.amount;
list.left();
list.image(req.item.icon(Cicon.small)).size(8 * 3).padRight(3);
list.add(req.item.localizedName).color(Color.lightGray);
Label label = list.label(() -> " " +
UI.formatAmount(Math.min(items.get(req.item), reqAmount)) + " / "
+ UI.formatAmount(reqAmount)).get();
Color targetColor = items.has(req.item) ? Color.lightGray : Color.scarlet;
Label label = t.add(Core.bundle.format("research.progress", Math.min((int)(used / sum * 100), 99))).left().get();
if(shiny){
label.setColor(Pal.accent);
label.actions(Actions.color(targetColor, 0.75f, Interp.fade));
label.actions(Actions.color(Color.lightGray, 0.75f, Interp.fade));
}else{
label.setColor(targetColor);
label.setColor(Color.lightGray);
}
}).fillX().left();
t.row();
}
for(int i = 0; i < node.requirements.length; i++){
ItemStack req = node.requirements[i];
ItemStack completed = node.finishedRequirements[i];
//skip finished stacks
if(req.amount <= completed.amount && !debugShowRequirements) continue;
boolean shiny = shine != null && shine[i];
t.table(list -> {
int reqAmount = debugShowRequirements ? req.amount : req.amount - completed.amount;
list.left();
list.image(req.item.uiIcon).size(8 * 3).padRight(3);
list.add(req.item.localizedName).color(Color.lightGray);
Label label = list.label(() -> " " +
UI.formatAmount(Math.min(items.get(req.item), reqAmount)) + " / "
+ UI.formatAmount(reqAmount)).get();
Color targetColor = items.has(req.item) ? Color.lightGray : Color.scarlet;
if(shiny){
label.setColor(Pal.accent);
label.actions(Actions.color(targetColor, 0.75f, Interp.fade));
}else{
label.setColor(targetColor);
}
}).fillX().left();
t.row();
}
}else if(node.objectives.size > 0){
t.table(r -> {
r.add("@complete").colspan(2).left();
r.row();
for(Objective o : node.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();
}
});
t.row();
}
}else if(node.objectives.size > 0){
t.table(r -> {
r.add("@complete").colspan(2).left();
r.row();
for(Objective o : node.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();
}
});
t.row();
}
});
});
}
}else{
desc.add("@completed");
}
}).pad(9);
if(mobile && locked(node)){
if(mobile && locked(node) && !net.client()){
b.row();
b.button("@research", Icon.ok, Styles.nodet, () -> spend(node))
.disabled(i -> !canSpend(node)).growX().height(44f).colspan(3);
b.button("@research", Icon.ok, new TextButtonStyle(){{
disabled = Tex.button;
font = Fonts.def;
fontColor = Color.white;
disabledFontColor = Color.gray;
up = buttonOver;
over = buttonDown;
}}, () -> spend(node)).disabled(i -> !canSpend(node)).growX().height(44f).colspan(3);
}
});
@@ -579,6 +733,10 @@ public class ResearchDialog extends BaseDialog{
}
addChild(infoTable);
checkMargin();
Core.app.post(() -> checkMargin());
infoTable.pack();
infoTable.act(Core.graphics.getDeltaTime());
}

View File

@@ -1,6 +1,7 @@
package mindustry.ui.dialogs;
import arc.*;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.Texture.*;
import arc.graphics.g2d.*;
@@ -11,8 +12,10 @@ import arc.scene.ui.*;
import arc.scene.ui.ImageButton.*;
import arc.scene.ui.TextButton.*;
import arc.scene.ui.layout.*;
import arc.scene.utils.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.ctype.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
@@ -20,28 +23,42 @@ import mindustry.input.*;
import mindustry.type.*;
import mindustry.ui.*;
import java.util.regex.*;
import static mindustry.Vars.*;
public class SchematicsDialog extends BaseDialog{
private static final float tagh = 42f;
private SchematicInfoDialog info = new SchematicInfoDialog();
private Schematic firstSchematic;
private String search = "";
private TextField searchField;
private Runnable rebuildPane = () -> {}, rebuildTags = () -> {};
private Pattern ignoreSymbols = Pattern.compile("[`~!@#$%^&*()\\-_=+{}|;:'\",<.>/?]");
private Seq<String> tags, selectedTags = new Seq<>();
private boolean checkedTags;
public SchematicsDialog(){
super("@schematics");
Core.assets.load("sprites/schematic-background.png", Texture.class).loaded = t -> ((Texture)t).setWrap(TextureWrap.repeat);
Core.assets.load("sprites/schematic-background.png", Texture.class).loaded = t -> t.setWrap(TextureWrap.repeat);
tags = Core.settings.getJson("schematic-tags", Seq.class, String.class, Seq::new);
shouldPause = true;
addCloseButton();
buttons.button("@schematic.import", Icon.download, this::showImport);
makeButtonOverlay();
shown(this::setup);
onResize(this::setup);
}
void setup(){
if(!checkedTags){
checkTags();
checkedTags = true;
}
search = "";
Runnable[] rebuildPane = {null};
cont.top();
cont.clear();
@@ -51,19 +68,56 @@ public class SchematicsDialog extends BaseDialog{
s.image(Icon.zoom);
searchField = s.field(search, res -> {
search = res;
rebuildPane[0].run();
rebuildPane.run();
}).growX().get();
searchField.setMessageText("@schematic.search");
searchField.clicked(KeyCode.mouseRight, () -> {
if(!search.isEmpty()){
search = "";
searchField.clearText();
rebuildPane.run();
}
});
}).fillX().padBottom(4);
cont.row();
cont.table(in -> {
in.left();
in.add("@schematic.tags").padRight(4);
//tags (no scroll pane visible)
in.pane(Styles.noBarPane, t -> {
rebuildTags = () -> {
t.clearChildren();
t.left();
t.defaults().pad(2).height(tagh);
for(var tag : tags){
t.button(tag, Styles.togglet, () -> {
if(selectedTags.contains(tag)){
selectedTags.remove(tag);
}else{
selectedTags.add(tag);
}
rebuildPane.run();
}).checked(selectedTags.contains(tag)).with(c -> c.getLabel().setWrap(false));
}
};
rebuildTags.run();
}).fillX().height(tagh).scrollY(false);
in.button(Icon.pencilSmall, this::showAllTags).size(tagh).pad(2).tooltip("@schematic.edittags");
}).height(tagh).fillX();
cont.row();
cont.pane(t -> {
t.top();
t.margin(20f);
t.update(() -> {
if(Core.input.keyTap(Binding.chat) && Core.scene.getKeyboardFocus() == searchField && firstSchematic != null){
if(!Vars.state.rules.schematicsAllowed){
if(!state.rules.schematicsAllowed){
ui.showInfo("@schematic.disabled");
}else{
control.input.useSchematic(firstSchematic);
@@ -72,18 +126,20 @@ public class SchematicsDialog extends BaseDialog{
}
});
rebuildPane[0] = () -> {
rebuildPane = () -> {
int cols = Math.max((int)(Core.graphics.getWidth() / Scl.scl(230)), 1);
t.clear();
int i = 0;
String regex = "[`~!@#$%^&*()-_=+{}|;:'\",<.>/?]";
String searchString = search.toLowerCase().replaceAll(regex, " ");
String searchString = ignoreSymbols.matcher(search.toLowerCase()).replaceAll("");
firstSchematic = null;
for(Schematic s : schematics.all()){
if(!search.isEmpty() && !s.name().toLowerCase().replaceAll(regex, " ").contains(searchString)) continue;
//make sure *tags* fit
if(selectedTags.any() && !s.labels.containsAll(selectedTags)) continue;
//make sure search fits
if(!search.isEmpty() && !ignoreSymbols.matcher(s.name().toLowerCase()).replaceAll("").contains(searchString)) continue;
if(firstSchematic == null) firstSchematic = s;
Button[] sel = {null};
@@ -94,64 +150,26 @@ public class SchematicsDialog extends BaseDialog{
buttons.left();
buttons.defaults().size(50f);
ImageButtonStyle style = Styles.clearPartiali;
ImageButtonStyle style = Styles.emptyi;
buttons.button(Icon.info, style, () -> {
showInfo(s);
});
buttons.button(Icon.upload, style, () -> {
showExport(s);
});
buttons.button(Icon.pencil, style, () -> {
new Dialog("@schematic.rename"){{
cont.margin(30).add("@name").padRight(6f);
TextField nameField = cont.field(s.name(), null).size(400f, 55f).addInputDialog().get();
cont.row();
cont.margin(30).add("@editor.description").padRight(6f);
TextField descripionField = cont.area(s.description(), Styles.areaField, t -> {}).size(400f, 140f).addInputDialog().get();
Runnable accept = () -> {
s.tags.put("name", nameField.getText());
s.tags.put("description", descripionField.getText());
s.save();
hide();
rebuildPane[0].run();
};
buttons.defaults().size(120, 54).pad(4);
buttons.button("@ok", accept).disabled(b -> nameField.getText().isEmpty());
buttons.button("@cancel", this::hide);
keyDown(KeyCode.enter, () -> {
if(!nameField.getText().isEmpty() && Core.scene.getKeyboardFocus() != descripionField){
accept.run();
}
});
keyDown(KeyCode.escape, this::hide);
keyDown(KeyCode.back, this::hide);
show();
}};
});
buttons.button(Icon.info, style, () -> showInfo(s)).tooltip("@info.title");
buttons.button(Icon.upload, style, () -> showExport(s)).tooltip("@editor.export");
buttons.button(Icon.pencil, style, () -> showEdit(s)).tooltip("@schematic.edit");
if(s.hasSteamID()){
buttons.button(Icon.link, style, () -> platform.viewListing(s));
buttons.button(Icon.link, style, () -> platform.viewListing(s)).tooltip("@view.workshop");
}else{
buttons.button(Icon.trash, style, () -> {
if(s.mod != null){
ui.showInfo(Core.bundle.format("mod.item.remove", s.mod.meta.displayName()));
ui.showInfo(Core.bundle.format("mod.item.remove", s.mod.meta.displayName));
}else{
ui.showConfirm("@confirm", "@schematic.delete.confirm", () -> {
schematics.remove(s);
rebuildPane[0].run();
rebuildPane.run();
});
}
});
}).tooltip("@save.delete");
}
}).growX().height(50f);
b.row();
b.stack(new SchematicImage(s).setScaling(Scaling.fit), new Table(n -> {
@@ -167,14 +185,14 @@ public class SchematicsDialog extends BaseDialog{
if(state.isMenu()){
showInfo(s);
}else{
if(!Vars.state.rules.schematicsAllowed){
if(!state.rules.schematicsAllowed){
ui.showInfo("@schematic.disabled");
}else{
control.input.useSchematic(s);
hide();
}
}
}).pad(4).style(Styles.cleari).get();
}).pad(4).style(Styles.flati).get();
sel[0].getStyle().up = Tex.pane;
@@ -184,12 +202,16 @@ public class SchematicsDialog extends BaseDialog{
}
if(firstSchematic == null){
t.add("@none");
if(!searchString.isEmpty() || selectedTags.any()){
t.add("@none.found");
}else{
t.add("@none").color(Color.lightGray);
}
}
};
rebuildPane[0].run();
}).get().setScrollingDisabled(true, false);
rebuildPane.run();
}).grow().scrollX(false);
}
public void showInfo(Schematic schematic){
@@ -197,11 +219,11 @@ public class SchematicsDialog extends BaseDialog{
}
public void showImport(){
BaseDialog dialog = new BaseDialog("@editor.export");
BaseDialog dialog = new BaseDialog("@editor.import");
dialog.cont.pane(p -> {
p.margin(10f);
p.table(Tex.button, t -> {
TextButtonStyle style = Styles.cleart;
TextButtonStyle style = Styles.flatt;
t.defaults().size(280f, 60f).left();
t.row();
t.button("@schematic.copy.import", Icon.copy, style, () -> {
@@ -212,6 +234,7 @@ public class SchematicsDialog extends BaseDialog{
schematics.add(s);
setup();
ui.showInfoFade("@schematic.saved");
checkTags(s);
showInfo(s);
}catch(Throwable e){
ui.showException(e);
@@ -227,6 +250,7 @@ public class SchematicsDialog extends BaseDialog{
schematics.add(s);
setup();
showInfo(s);
checkTags(s);
}catch(Exception e){
ui.showException(e);
}
@@ -248,9 +272,9 @@ public class SchematicsDialog extends BaseDialog{
public void showExport(Schematic s){
BaseDialog dialog = new BaseDialog("@editor.export");
dialog.cont.pane(p -> {
p.margin(10f);
p.table(Tex.button, t -> {
TextButtonStyle style = Styles.cleart;
p.margin(10f);
p.table(Tex.button, t -> {
TextButtonStyle style = Styles.flatt;
t.defaults().size(280f, 60f).left();
if(steam && !s.hasSteamID()){
t.button("@schematic.shareworkshop", Icon.book, style,
@@ -275,18 +299,384 @@ public class SchematicsDialog extends BaseDialog{
dialog.show();
}
public void focusSearchField(){
if(searchField == null) return;
public void showEdit(Schematic s){
new BaseDialog("@schematic.edit"){{
setFillParent(true);
addCloseListener();
Core.scene.setKeyboardFocus(searchField);
cont.margin(30);
cont.add("@schematic.tags").padRight(6f);
cont.table(tags -> buildTags(s, tags, false)).maxWidth(400f).fillX().left().row();
cont.margin(30).add("@name").padRight(6f);
TextField nameField = cont.field(s.name(), null).size(400f, 55f).left().get();
cont.row();
cont.margin(30).add("@editor.description").padRight(6f);
TextField descField = cont.area(s.description(), Styles.areaField, t -> {}).size(400f, 140f).left().get();
Runnable accept = () -> {
s.tags.put("name", nameField.getText());
s.tags.put("description", descField.getText());
s.save();
hide();
rebuildPane.run();
};
buttons.defaults().size(210f, 64f).pad(4);
buttons.button("@ok", Icon.ok, accept).disabled(b -> nameField.getText().isEmpty());
buttons.button("@cancel", Icon.cancel, this::hide);
keyDown(KeyCode.enter, () -> {
if(!nameField.getText().isEmpty() && Core.scene.getKeyboardFocus() != descField){
accept.run();
}
});
}}.show();
}
//adds all new tags to the global list of tags
//alternatively, unknown tags could be discarded on import?
void checkTags(){
ObjectSet<String> encountered = new ObjectSet<>();
encountered.addAll(tags);
for(Schematic s : schematics.all()){
for(var tag : s.labels){
if(encountered.add(tag)){
tags.add(tag);
}
}
}
}
//adds any new tags found to the global tag list
//TODO remove tags from it instead?
void checkTags(Schematic s){
boolean any = false;
for(var tag : s.labels){
if(!tags.contains(tag)){
tags.add(tag);
any = true;
}
}
if(any){
rebuildTags.run();
}
}
void tagsChanged(){
rebuildTags.run();
if(selectedTags.any()){
rebuildPane.run();
}
Core.settings.putJson("schematic-tags", String.class, tags);
}
void addTag(Schematic s, String tag){
s.labels.add(tag);
s.save();
tagsChanged();
}
void removeTag(Schematic s, String tag){
s.labels.remove(tag);
s.save();
tagsChanged();
}
//shows a dialog for creating a new tag
void showNewTag(Cons<String> result){
ui.showTextInput("@schematic.addtag", "", "", out -> {
if(tags.contains(out)){
ui.showInfo("@schematic.tagexists");
}else{
tags.add(out);
tagsChanged();
result.get(out);
}
});
}
void showNewIconTag(Cons<String> cons){
new Dialog(){{
closeOnBack();
setFillParent(true);
//TODO: use IconSelectDialog
cont.pane(t -> {
resized(true, () -> {
t.clearChildren();
t.marginRight(19f).marginLeft(12f);
t.defaults().size(48f);
int cols = (int)Math.min(20, Core.graphics.getWidth() / Scl.scl(52f));
int i = 0;
for(String icon : accessibleIcons){
String out = (char)Iconc.codes.get(icon) + "";
if(tags.contains(out)) continue;
t.button(Icon.icons.get(icon), Styles.flati, iconMed, () -> {
tags.add(out);
tagsChanged();
cons.get(out);
hide();
});
if(++i % cols == 0) t.row();
}
for(ContentType ctype : defaultContentIcons){
var all = content.getBy(ctype).<UnlockableContent>as().select(u -> !u.isHidden() && u.unlockedNow() && u.hasEmoji());
t.row();
if(all.count(u -> !tags.contains(u.emoji())) > 0) t.image().colspan(cols).growX().width(Float.NEGATIVE_INFINITY).height(3f).color(Pal.accent);
t.row();
i = 0;
for(UnlockableContent u : all){
if(tags.contains(u.emoji())) continue;
t.button(new TextureRegionDrawable(u.uiIcon), Styles.flati, iconMed, () -> {
String out = u.emoji() + "";
tags.add(out);
tagsChanged();
cons.get(out);
hide();
}).tooltip(u.localizedName);
if(++i % cols == 0) t.row();
}
}
});
}).scrollX(false);
buttons.button("@back", Icon.left, this::hide).size(210f, 64f);
}}.show();
}
void showAllTags(){
var dialog = new BaseDialog("@schematic.edittags");
dialog.addCloseButton();
Runnable[] rebuild = {null};
dialog.cont.pane(p -> {
rebuild[0] = () -> {
p.clearChildren();
p.margin(12f).defaults().fillX().left();
float sum = 0f;
Table current = new Table().left();
for(var tag : tags){
float si = 40f;
var next = new Table(Tex.whiteui, n -> {
n.setColor(Pal.gray);
n.margin(5f);
n.table(move -> {
//move up
move.button(Icon.upOpen, Styles.emptyi, () -> {
int idx = tags.indexOf(tag);
if(idx > 0){
tags.swap(idx, idx - 1);
tagsChanged();
rebuild[0].run();
}
}).size(si).tooltip("@editor.moveup").row();
//move down
move.button(Icon.downOpen, Styles.emptyi, () -> {
int idx = tags.indexOf(tag);
if(idx < tags.size - 1){
tags.swap(idx, idx + 1);
tagsChanged();
rebuild[0].run();
}
}).size(si).tooltip("@editor.movedown");
}).fillY();
n.table(t -> {
t.add(tag).left().row();
t.add(Core.bundle.format("schematic.tagged", schematics.all().count(s -> s.labels.contains(tag)))).left()
.update(b -> b.setColor(b.hasMouse() ? Pal.accent : Color.lightGray)).get().clicked(() -> {
dialog.hide();
selectedTags.clear().add(tag);
rebuildTags.run();
rebuildPane.run();
});
}).growX().fillY();
n.table(b -> {
b.margin(2);
//rename tag
b.button(Icon.pencil, Styles.emptyi, () -> {
ui.showTextInput("@schematic.renametag", "@name", tag, result -> {
//same tag, nothing was renamed
if(result.equals(tag)) return;
if(tags.contains(result)){
ui.showInfo("@schematic.tagexists");
}else{
for(Schematic s : schematics.all()){
if(s.labels.any()){
s.labels.replace(tag, result);
s.save();
}
}
selectedTags.replace(tag, result);
tags.replace(tag, result);
tagsChanged();
rebuild[0].run();
}
});
}).size(si).tooltip("@schematic.renametag").row();
//delete tag
b.button(Icon.trash, Styles.emptyi, () -> {
ui.showConfirm("@schematic.tagdelconfirm", () -> {
for(Schematic s : schematics.all()){
if(s.labels.any()){
s.labels.remove(tag);
s.save();
}
}
selectedTags.remove(tag);
tags.remove(tag);
tagsChanged();
rebuildPane.run();
rebuild[0].run();
});
}).size(si).tooltip("@save.delete");
}).fillY();
});
next.pack();
float w = next.getWidth() + Scl.scl(9f);
if(w + sum >= Core.graphics.getWidth() * 0.9f){
p.add(current).row();
current = new Table();
current.left();
sum = 0;
}
current.add(next).minWidth(210).pad(4);
sum += w;
}
if(sum > 0){
p.add(current).row();
}
p.table(t -> {
t.left().defaults().fillX().height(tagh).pad(2);
t.button("@schematic.texttag", Icon.add, () -> showNewTag(res -> rebuild[0].run())).wrapLabel(false).get().getLabelCell().padLeft(5);
t.button("@schematic.icontag", Icon.add, () -> showNewIconTag(res -> rebuild[0].run())).wrapLabel(false).get().getLabelCell().padLeft(5);
});
};
resized(true, rebuild[0]);
}).scrollX(false);
dialog.show();
}
void buildTags(Schematic schem, Table t){
buildTags(schem, t, true);
}
void buildTags(Schematic schem, Table t, boolean name){
t.clearChildren();
t.left();
//sort by order in the main target array. the complexity of this is probably awful
schem.labels.sort(s -> tags.indexOf(s));
if(name) t.add("@schematic.tags").padRight(4);
t.pane(s -> {
s.left();
s.defaults().pad(3).height(tagh);
for(var tag : schem.labels){
s.table(Tex.button, i -> {
i.add(tag).padRight(4).height(tagh).labelAlign(Align.center);
i.button(Icon.cancelSmall, Styles.emptyi, () -> {
removeTag(schem, tag);
buildTags(schem, t, name);
}).size(tagh).padRight(-9f).padLeft(-9f);
});
}
}).fillX().left().height(tagh).scrollY(false);
t.button(Icon.addSmall, () -> {
var dialog = new BaseDialog("@schematic.addtag");
dialog.addCloseButton();
dialog.cont.pane(p -> resized(true, () -> {
p.clearChildren();
p.defaults().fillX().left();
float sum = 0f;
Table current = new Table().left();
for(var tag : tags){
if(schem.labels.contains(tag)) continue;
var next = Elem.newButton(tag, () -> {
addTag(schem, tag);
buildTags(schem, t, name);
dialog.hide();
});
next.getLabel().setWrap(false);
next.pack();
float w = next.getPrefWidth() + Scl.scl(6f);
if(w + sum >= Core.graphics.getWidth() * (Core.graphics.isPortrait() ? 1f : 0.8f)){
p.add(current).row();
current = new Table();
current.left();
current.add(next).height(tagh).pad(2);
sum = 0;
}else{
current.add(next).height(tagh).pad(2);
}
sum += w;
}
if(sum > 0){
p.add(current).row();
}
Cons<String> handleTag = res -> {
dialog.hide();
addTag(schem, res);
buildTags(schem, t, name);
};
p.row();
p.table(v -> {
v.left().defaults().fillX().height(tagh).pad(2);
v.button("@schematic.texttag", Icon.add, () -> showNewTag(handleTag)).wrapLabel(false).get().getLabelCell().padLeft(4);
v.button("@schematic.icontag", Icon.add, () -> showNewIconTag(handleTag)).wrapLabel(false).get().getLabelCell().padLeft(4);
});
}));
dialog.show();
}).size(tagh).tooltip("@schematic.addtag");
}
@Override
public Dialog show(){
super.show();
if(Core.app.isDesktop()){
focusSearchField();
if(Core.app.isDesktop() && searchField != null){
Core.scene.setKeyboardFocus(searchField);
}
return this;
@@ -298,6 +688,7 @@ public class SchematicsDialog extends BaseDialog{
public Color borderColor = Pal.gray;
private Schematic schematic;
private Texture lastTexture;
boolean set;
public SchematicImage(Schematic s){
@@ -320,6 +711,8 @@ public class SchematicsDialog extends BaseDialog{
if(!set){
Core.app.post(this::setPreview);
set = true;
}else if(lastTexture != null && lastTexture.isDisposed()){
set = wasSet = false;
}
Texture background = Core.assets.get("sprites/schematic-background.png", Texture.class);
@@ -346,37 +739,36 @@ public class SchematicsDialog extends BaseDialog{
}
private void setPreview(){
TextureRegionDrawable draw = new TextureRegionDrawable(new TextureRegion(schematics.getPreview(schematic)));
TextureRegionDrawable draw = new TextureRegionDrawable(new TextureRegion(lastTexture = schematics.getPreview(schematic)));
setDrawable(draw);
setScaling(Scaling.fit);
}
}
public static class SchematicInfoDialog extends BaseDialog{
public class SchematicInfoDialog extends BaseDialog{
SchematicInfoDialog(){
super("");
setFillParent(true);
addCloseButton();
addCloseListener();
}
public void show(Schematic schem){
cont.clear();
title.setText("[[" + Core.bundle.get("schematic") + "] " +schem.name());
cont.add(Core.bundle.format("schematic.info", schem.width, schem.height, schem.tiles.size)).color(Color.lightGray);
cont.row();
cont.add(new SchematicImage(schem)).maxSize(800f);
cont.row();
cont.add(Core.bundle.format("schematic.info", schem.width, schem.height, schem.tiles.size)).color(Color.lightGray).row();
cont.table(tags -> buildTags(schem, tags)).fillX().left().row();
cont.add(new SchematicImage(schem)).maxSize(800f).row();
ItemSeq arr = schem.requirements();
cont.table(r -> {
int i = 0;
for(ItemStack s : arr){
r.image(s.item.icon(Cicon.small)).left();
r.image(s.item.uiIcon).left().size(iconMed);
r.label(() -> {
Building core = player.core();
if(core == null || state.rules.infiniteResources || core.items.has(s.item, s.amount)) return "[lightgray]" + s.amount + "";
if(core == null || state.isMenu() || state.rules.infiniteResources || core.items.has(s.item, s.amount)) return "[lightgray]" + s.amount + "";
return (core.items.has(s.item, s.amount) ? "[lightgray]" : "[scarlet]") + Math.min(core.items.get(s.item), s.amount) + "[lightgray]/" + s.amount;
}).padLeft(2).left().padRight(4);
@@ -406,7 +798,13 @@ public class SchematicsDialog extends BaseDialog{
});
}
buttons.clearChildren();
buttons.defaults().size(Core.graphics.isPortrait() ? 150f : 210f, 64f);
buttons.button("@back", Icon.left, this::hide);
buttons.button("@editor.export", Icon.upload, () -> showExport(schem));
buttons.button("@edit", Icon.edit, () -> showEdit(schem));
show();
}
}
}
}

View File

@@ -2,11 +2,13 @@ package mindustry.ui.dialogs;
import arc.*;
import arc.files.*;
import arc.func.*;
import arc.graphics.*;
import arc.graphics.Texture.*;
import arc.input.*;
import arc.scene.*;
import arc.scene.event.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.TextButton.*;
import arc.scene.ui.layout.*;
@@ -15,7 +17,6 @@ import arc.util.*;
import arc.util.io.*;
import mindustry.content.*;
import mindustry.content.TechTree.*;
import mindustry.core.GameState.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.game.EventType.*;
@@ -28,42 +29,37 @@ import java.io.*;
import java.util.zip.*;
import static arc.Core.*;
import static mindustry.Vars.net;
import static mindustry.Vars.*;
public class SettingsMenuDialog extends SettingsDialog{
public class SettingsMenuDialog extends BaseDialog{
public SettingsTable graphics;
public SettingsTable game;
public SettingsTable sound;
public SettingsTable main;
private Table prefs;
private Table menu;
private BaseDialog dataDialog;
private boolean wasPaused;
private Seq<SettingsCategory> categories = new Seq<>();
public SettingsMenuDialog(){
hidden(() -> {
Sounds.back.play();
if(state.isGame()){
if(!wasPaused || net.active())
state.set(State.playing);
}
});
super(bundle.get("settings", "Settings"));
addCloseButton();
cont.add(main = new SettingsTable());
shouldPause = true;
shown(() -> {
back();
if(state.isGame()){
wasPaused = state.is(State.paused);
state.set(State.paused);
}
rebuildMenu();
});
setFillParent(true);
title.setAlignment(Align.center);
titleTable.row();
titleTable.add(new Image()).growX().height(3f).pad(4f).get().setColor(Pal.accent);
onResize(() -> {
graphics.rebuild();
sound.rebuild();
game.rebuild();
updateScrollFocus();
});
cont.clearChildren();
cont.remove();
@@ -89,7 +85,7 @@ public class SettingsMenuDialog extends SettingsDialog{
dataDialog.cont.table(Tex.button, t -> {
t.defaults().size(280f, 60f).left();
TextButtonStyle style = Styles.cleart;
TextButtonStyle style = Styles.flatt;
t.button("@settings.cleardata", Icon.trash, style, () -> ui.showConfirm("@confirm", "@settings.clearall.confirm", () -> {
ObjectMap<String, Object> map = new ObjectMap<>();
@@ -146,7 +142,7 @@ public class SettingsMenuDialog extends SettingsDialog{
}
}
}
for(var slot : control.saves.getSaveSlots().copy()){
if(slot.isSector()){
slot.delete();
@@ -184,6 +180,8 @@ public class SettingsMenuDialog extends SettingsDialog{
t.button("@data.import", Icon.download, style, () -> ui.showConfirm("@confirm", "@data.import.confirm", () -> platform.showFileChooser(true, "zip", file -> {
try{
importData(file);
control.saves.resetSave();
state = new GameState();
Core.app.exit();
}catch(IllegalArgumentException e){
ui.showErrorMessage("@data.invalid");
@@ -214,37 +212,20 @@ public class SettingsMenuDialog extends SettingsDialog{
platform.shareFile(logs);
}else{
platform.showFileChooser(false, "txt", file -> {
file.writeString(getLogs());
app.post(() -> ui.showInfo("@crash.exported"));
try{
file.writeBytes(getLogs().getBytes(Strings.utf8));
app.post(() -> ui.showInfo("@crash.exported"));
}catch(Throwable e){
ui.showException(e);
}
});
}
}
}).marginLeft(4);
});
ScrollPane pane = new ScrollPane(prefs);
pane.addCaptureListener(new InputListener(){
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
Element actor = pane.hit(x, y, true);
if(actor instanceof Slider){
pane.setFlickScroll(false);
return true;
}
return super.touchDown(event, x, y, pointer, button);
}
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
pane.setFlickScroll(true);
super.touchUp(event, x, y, pointer, button);
}
});
pane.setFadeScrollBars(false);
row();
add(pane).grow().top();
pane(prefs).grow().top();
row();
add(buttons).fillX();
@@ -266,39 +247,76 @@ public class SettingsMenuDialog extends SettingsDialog{
return out.toString();
}
/** Adds a custom settings category, with the icon being the specified region. */
public void addCategory(String name, @Nullable String region, Cons<SettingsTable> builder){
categories.add(new SettingsCategory(name, region == null ? null : new TextureRegionDrawable(atlas.find(region)), builder));
}
/** Adds a custom settings category, for use in mods. The specified consumer should add all relevant mod settings to the table. */
public void addCategory(String name, @Nullable Drawable icon, Cons<SettingsTable> builder){
categories.add(new SettingsCategory(name, icon, builder));
}
/** Adds a custom settings category, for use in mods. The specified consumer should add all relevant mod settings to the table. */
public void addCategory(String name, Cons<SettingsTable> builder){
addCategory(name, (Drawable)null, builder);
}
public Seq<SettingsCategory> getCategories(){
return categories;
}
void rebuildMenu(){
menu.clearChildren();
TextButtonStyle style = Styles.cleart;
TextButtonStyle style = Styles.flatt;
float marg = 8f, isize = iconMed;
menu.defaults().size(300f, 60f);
menu.button("@settings.game", style, () -> visible(0));
menu.row();
menu.button("@settings.graphics", style, () -> visible(1));
menu.row();
menu.button("@settings.sound", style, () -> visible(2));
menu.row();
menu.button("@settings.language", style, ui.language::show);
menu.button("@settings.game", Icon.settings, style, isize, () -> visible(0)).marginLeft(marg).row();
menu.button("@settings.graphics", Icon.image, style, isize, () -> visible(1)).marginLeft(marg).row();
menu.button("@settings.sound", Icon.filters, style, isize, () -> visible(2)).marginLeft(marg).row();
menu.button("@settings.language", Icon.chat, style, isize, ui.language::show).marginLeft(marg).row();
if(!mobile || Core.settings.getBool("keyboard")){
menu.row();
menu.button("@settings.controls", style, ui.controls::show);
menu.button("@settings.controls", Icon.move, style, isize, ui.controls::show).marginLeft(marg).row();
}
menu.row();
menu.button("@settings.data", style, () -> dataDialog.show());
menu.button("@settings.data", Icon.save, style, isize, () -> dataDialog.show()).marginLeft(marg).row();
int i = 3;
for(var cat : categories){
int index = i;
if(cat.icon == null){
menu.button(cat.name, style, () -> visible(index)).marginLeft(marg).row();
}else{
menu.button(cat.name, cat.icon, style, isize, () -> visible(index)).with(b -> ((Image)b.getChildren().get(1)).setScaling(Scaling.fit)).marginLeft(marg).row();
}
i++;
}
}
void addSettings(){
sound.sliderPref("musicvol", bundle.get("setting.musicvol.name", "Music Volume"), 100, 0, 100, 1, i -> i + "%");
sound.sliderPref("sfxvol", bundle.get("setting.sfxvol.name", "SFX Volume"), 100, 0, 100, 1, i -> i + "%");
sound.sliderPref("ambientvol", bundle.get("setting.ambientvol.name", "Ambient Volume"), 100, 0, 100, 1, i -> i + "%");
sound.checkPref("alwaysmusic", false);
sound.sliderPref("musicvol", 100, 0, 100, 1, i -> i + "%");
sound.sliderPref("sfxvol", 100, 0, 100, 1, i -> i + "%");
sound.sliderPref("ambientvol", 100, 0, 100, 1, i -> i + "%");
game.sliderPref("saveinterval", 60, 10, 5 * 120, 10, i -> Core.bundle.format("setting.seconds", i));
game.screenshakePref();
if(mobile){
game.checkPref("autotarget", true);
game.checkPref("keyboard", false, val -> control.setInput(val ? new DesktopInput() : new MobileInput()));
if(Core.settings.getBool("keyboard")){
control.setInput(new DesktopInput());
if(!ios){
game.checkPref("keyboard", false, val -> {
control.setInput(val ? new DesktopInput() : new MobileInput());
input.setUseKeyboard(val);
});
if(Core.settings.getBool("keyboard")){
control.setInput(new DesktopInput());
input.setUseKeyboard(true);
}
}else{
Core.settings.put("keyboard", false);
}
}
//the issue with touchscreen support on desktop is that:
@@ -310,20 +328,35 @@ public class SettingsMenuDialog extends SettingsDialog{
control.setInput(new MobileInput());
}
}*/
game.sliderPref("saveinterval", 60, 10, 5 * 120, 10, i -> Core.bundle.format("setting.seconds", i));
if(!mobile){
game.checkPref("crashreport", true);
}
game.checkPref("communityservers", true, val -> {
defaultServers.clear();
if(val){
JoinDialog.fetchServers();
}
});
game.checkPref("savecreate", true);
game.checkPref("blockreplace", true);
game.checkPref("conveyorpathfinding", true);
game.checkPref("hints", true);
game.checkPref("logichints", true);
if(!mobile){
game.checkPref("backgroundpause", true);
game.checkPref("buildautopause", false);
game.checkPref("distinctcontrolgroups", true);
}
game.checkPref("doubletapmine", false);
game.checkPref("commandmodehold", true);
if(!ios){
game.checkPref("modcrashdisable", true);
}
if(steam){
@@ -333,19 +366,30 @@ public class SettingsMenuDialog extends SettingsDialog{
});
if(!Version.modifier.contains("beta")){
game.checkPref("publichost", false, i -> {
game.checkPref("steampublichost", false, i -> {
platform.updateLobby();
});
}
}
graphics.sliderPref("uiscale", 100, 25, 300, 25, s -> {
if(ui.settings != null){
Core.settings.put("uiscalechanged", true);
}
if(!mobile){
game.checkPref("console", false);
}
int[] lastUiScale = {settings.getInt("uiscale", 100)};
graphics.sliderPref("uiscale", 100, 25, 300, 5, s -> {
//if the user changed their UI scale, but then put it back, don't consider it 'changed'
Core.settings.put("uiscalechanged", s != lastUiScale[0]);
return s + "%";
});
graphics.sliderPref("fpscap", 240, 15, 245, 5, s -> (s > 240 ? Core.bundle.get("setting.fpscap.none") : Core.bundle.format("setting.fpscap.text", s)));
graphics.sliderPref("screenshake", 4, 0, 8, i -> (i / 4f) + "x");
graphics.sliderPref("bloomintensity", 6, 0, 16, i -> (int)(i/4f * 100f) + "%");
graphics.sliderPref("bloomblur", 2, 1, 16, i -> i + "x");
graphics.sliderPref("fpscap", 240, 10, 245, 5, s -> (s > 240 ? Core.bundle.get("setting.fpscap.none") : Core.bundle.format("setting.fpscap.text", s)));
graphics.sliderPref("chatopacity", 100, 0, 100, 5, s -> s + "%");
graphics.sliderPref("lasersopacity", 100, 0, 100, 5, s -> {
if(ui.settings != null){
@@ -353,27 +397,42 @@ public class SettingsMenuDialog extends SettingsDialog{
}
return s + "%";
});
graphics.sliderPref("unitlaseropacity", 100, 0, 100, 5, s -> s + "%");
graphics.sliderPref("bridgeopacity", 100, 0, 100, 5, s -> s + "%");
if(!mobile){
graphics.checkPref("vsync", true, b -> Core.graphics.setVSync(b));
graphics.checkPref("fullscreen", false, b -> {
if(b && settings.getBool("borderlesswindow")){
Core.graphics.setWindowedMode(Core.graphics.getWidth(), Core.graphics.getHeight());
settings.put("borderlesswindow", false);
graphics.rebuild();
}
if(b){
Core.graphics.setFullscreenMode(Core.graphics.getDisplayMode());
Core.graphics.setFullscreen();
}else{
Core.graphics.setWindowedMode(Core.graphics.getWidth(), Core.graphics.getHeight());
}
});
graphics.checkPref("borderlesswindow", false, b -> Core.graphics.setUndecorated(b));
graphics.checkPref("borderlesswindow", false, b -> {
if(b && settings.getBool("fullscreen")){
Core.graphics.setWindowedMode(Core.graphics.getWidth(), Core.graphics.getHeight());
settings.put("fullscreen", false);
graphics.rebuild();
}
Core.graphics.setBorderless(b);
});
Core.graphics.setVSync(Core.settings.getBool("vsync"));
if(Core.settings.getBool("fullscreen")){
Core.app.post(() -> Core.graphics.setFullscreenMode(Core.graphics.getDisplayMode()));
Core.app.post(() -> Core.graphics.setFullscreen());
}
if(Core.settings.getBool("borderlesswindow")){
Core.app.post(() -> Core.graphics.setUndecorated(true));
Core.app.post(() -> Core.graphics.setBorderless(true));
}
}else if(!ios){
graphics.checkPref("landscape", false, b -> {
@@ -391,6 +450,7 @@ public class SettingsMenuDialog extends SettingsDialog{
graphics.checkPref("effects", true);
graphics.checkPref("atmosphere", !mobile);
graphics.checkPref("drawlight", true);
graphics.checkPref("destroyedblocks", true);
graphics.checkPref("blockstatus", false);
graphics.checkPref("playerchat", true);
@@ -400,20 +460,20 @@ public class SettingsMenuDialog extends SettingsDialog{
graphics.checkPref("minimap", !mobile);
graphics.checkPref("smoothcamera", true);
graphics.checkPref("position", false);
if(!mobile){
graphics.checkPref("mouseposition", false);
}
graphics.checkPref("fps", false);
graphics.checkPref("playerindicators", true);
graphics.checkPref("indicators", true);
graphics.checkPref("showweather", true);
graphics.checkPref("animatedwater", true);
if(Shaders.shield != null){
graphics.checkPref("animatedshields", !mobile);
}
if(!ios){
graphics.checkPref("bloom", true, val -> renderer.toggleBloom(val));
}else{
Core.settings.put("bloom", false);
}
graphics.checkPref("bloom", true, val -> renderer.toggleBloom(val));
graphics.checkPref("pixelate", false, val -> {
if(val){
@@ -421,12 +481,17 @@ public class SettingsMenuDialog extends SettingsDialog{
}
});
graphics.checkPref("linear", !mobile, b -> {
for(Texture tex : Core.atlas.getTextures()){
TextureFilter filter = b ? TextureFilter.linear : TextureFilter.nearest;
tex.setFilter(filter, filter);
}
});
//iOS (and possibly Android) devices do not support linear filtering well, so disable it
if(!ios){
graphics.checkPref("linear", !mobile, b -> {
for(Texture tex : Core.atlas.getTextures()){
TextureFilter filter = b ? TextureFilter.linear : TextureFilter.nearest;
tex.setFilter(filter, filter);
}
});
}else{
settings.put("linear", false);
}
if(Core.settings.getBool("linear")){
for(Texture tex : Core.atlas.getTextures()){
@@ -435,11 +500,16 @@ public class SettingsMenuDialog extends SettingsDialog{
}
}
graphics.checkPref("skipcoreanimation", false);
graphics.checkPref("hidedisplays", false);
if(OS.isMac){
graphics.checkPref("macnotch", false);
}
if(!mobile){
Core.settings.put("swapdiagonal", false);
}
graphics.checkPref("flow", true);
}
public void exportData(Fi file) throws IOException{
@@ -447,16 +517,30 @@ public class SettingsMenuDialog extends SettingsDialog{
files.add(Core.settings.getSettingsFile());
files.addAll(customMapDirectory.list());
files.addAll(saveDirectory.list());
files.addAll(screenshotDirectory.list());
files.addAll(modDirectory.list());
files.addAll(schematicDirectory.list());
String base = Core.settings.getDataDirectory().path();
//add directories
for(Fi other : files.copy()){
Fi parent = other.parent();
while(!files.contains(parent) && !parent.equals(settings.getDataDirectory())){
files.add(parent);
}
}
try(OutputStream fos = file.write(false, 2048); ZipOutputStream zos = new ZipOutputStream(fos)){
for(Fi add : files){
if(add.isDirectory()) continue;
zos.putNextEntry(new ZipEntry(add.path().substring(base.length())));
Streams.copy(add.read(), zos);
String path = add.path().substring(base.length());
if(add.isDirectory()) path += "/";
//fix trailing / in path
path = path.startsWith("/") ? path.substring(1) : path;
zos.putNextEntry(new ZipEntry(path));
if(!add.isDirectory()){
try(var stream = add.read()){
Streams.copy(stream, zos);
}
}
zos.closeEntry();
}
}
@@ -495,18 +579,25 @@ public class SettingsMenuDialog extends SettingsDialog{
private void visible(int index){
prefs.clearChildren();
prefs.add(new Table[]{game, graphics, sound}[index]);
Seq<Table> tables = new Seq<>();
tables.addAll(game, graphics, sound);
for(var custom : categories){
tables.add(custom.table);
}
prefs.add(tables.get(index));
}
@Override
public void addCloseButton(){
buttons.button("@back", Icon.leftOpen, () -> {
buttons.button("@back", Icon.left, () -> {
if(prefs.getChildren().first() != menu){
back();
}else{
hide();
}
}).size(230f, 64f);
}).size(210f, 64f);
keyDown(key -> {
if(key == KeyCode.escape || key == KeyCode.back){
@@ -518,4 +609,250 @@ public class SettingsMenuDialog extends SettingsDialog{
}
});
}
public interface StringProcessor{
String get(int i);
}
public static class SettingsCategory{
public String name;
public @Nullable Drawable icon;
public Cons<SettingsTable> builder;
public SettingsTable table;
public SettingsCategory(String name, Drawable icon, Cons<SettingsTable> builder){
this.name = name;
this.icon = icon;
this.builder = builder;
table = new SettingsTable();
builder.get(table);
}
}
public static class SettingsTable extends Table{
protected Seq<Setting> list = new Seq<>();
public SettingsTable(){
left();
}
public Seq<Setting> getSettings(){
return list;
}
public void pref(Setting setting){
list.add(setting);
rebuild();
}
public SliderSetting sliderPref(String name, int def, int min, int max, StringProcessor s){
return sliderPref(name, def, min, max, 1, s);
}
public SliderSetting sliderPref(String name, int def, int min, int max, int step, StringProcessor s){
SliderSetting res;
list.add(res = new SliderSetting(name, def, min, max, step, s));
settings.defaults(name, def);
rebuild();
return res;
}
public void checkPref(String name, boolean def){
list.add(new CheckSetting(name, def, null));
settings.defaults(name, def);
rebuild();
}
public void checkPref(String name, boolean def, Boolc changed){
list.add(new CheckSetting(name, def, changed));
settings.defaults(name, def);
rebuild();
}
public void textPref(String name, String def){
list.add(new TextSetting(name, def, null));
settings.defaults(name, def);
rebuild();
}
public void textPref(String name, String def, Cons<String> changed){
list.add(new TextSetting(name, def, changed));
settings.defaults(name, def);
rebuild();
}
public void areaTextPref(String name, String def){
list.add(new AreaTextSetting(name, def, null));
settings.defaults(name, def);
rebuild();
}
public void areaTextPref(String name, String def, Cons<String> changed){
list.add(new AreaTextSetting(name, def, changed));
settings.defaults(name, def);
rebuild();
}
public void rebuild(){
clearChildren();
for(Setting setting : list){
setting.add(this);
}
button(bundle.get("settings.reset", "Reset to Defaults"), () -> {
for(Setting setting : list){
if(setting.name == null || setting.title == null) continue;
settings.remove(setting.name);
}
rebuild();
}).margin(14).width(240f).pad(6);
}
public abstract static class Setting{
public String name;
public String title;
public @Nullable String description;
public Setting(String name){
this.name = name;
String winkey = "setting." + name + ".name.windows";
title = OS.isWindows && bundle.has(winkey) ? bundle.get(winkey) : bundle.get("setting." + name + ".name", name);
description = bundle.getOrNull("setting." + name + ".description");
}
public abstract void add(SettingsTable table);
public void addDesc(Element elem){
ui.addDescTooltip(elem, description);
}
}
public static class CheckSetting extends Setting{
boolean def;
Boolc changed;
public CheckSetting(String name, boolean def, Boolc changed){
super(name);
this.def = def;
this.changed = changed;
}
@Override
public void add(SettingsTable table){
CheckBox box = new CheckBox(title);
box.update(() -> box.setChecked(settings.getBool(name)));
box.changed(() -> {
settings.put(name, box.isChecked());
if(changed != null){
changed.get(box.isChecked());
}
});
box.left();
addDesc(table.add(box).left().padTop(3f).get());
table.row();
}
}
public static class SliderSetting extends Setting{
int def, min, max, step;
StringProcessor sp;
public SliderSetting(String name, int def, int min, int max, int step, StringProcessor s){
super(name);
this.def = def;
this.min = min;
this.max = max;
this.step = step;
this.sp = s;
}
@Override
public void add(SettingsTable table){
Slider slider = new Slider(min, max, step, false);
slider.setValue(settings.getInt(name));
Label value = new Label("", Styles.outlineLabel);
Table content = new Table();
content.add(title, Styles.outlineLabel).left().growX().wrap();
content.add(value).padLeft(10f).right();
content.margin(3f, 33f, 3f, 33f);
content.touchable = Touchable.disabled;
slider.changed(() -> {
settings.put(name, (int)slider.getValue());
value.setText(sp.get((int)slider.getValue()));
});
slider.change();
addDesc(table.stack(slider, content).width(Math.min(Core.graphics.getWidth() / 1.2f, 460f)).left().padTop(4f).get());
table.row();
}
}
public static class TextSetting extends Setting{
String def;
Cons<String> changed;
public TextSetting(String name, String def, Cons<String> changed){
super(name);
this.def = def;
this.changed = changed;
}
@Override
public void add(SettingsTable table){
TextField field = new TextField();
field.update(() -> field.setText(settings.getString(name)));
field.changed(() -> {
settings.put(name, field.getText());
if(changed != null){
changed.get(field.getText());
}
});
Table prefTable = table.table().left().padTop(3f).get();
prefTable.add(field);
prefTable.label(() -> title);
addDesc(prefTable);
table.row();
}
}
public static class AreaTextSetting extends TextSetting{
public AreaTextSetting(String name, String def, Cons<String> changed){
super(name, def, changed);
}
@Override
public void add(SettingsTable table){
TextArea area = new TextArea("");
area.setPrefRows(5);
area.update(() -> {
area.setText(settings.getString(name));
area.setWidth(table.getWidth());
});
area.changed(() -> {
settings.put(name, area.getText());
if(changed != null){
changed.get(area.getText());
}
});
addDesc(table.label(() -> title).left().padTop(3f).get());
table.row().add(area).left();
table.row();
}
}
}
}

View File

@@ -4,6 +4,9 @@ import arc.*;
import arc.scene.ui.layout.*;
import mindustry.gen.*;
import mindustry.net.Administration.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class TraceDialog extends BaseDialog{
@@ -11,7 +14,6 @@ public class TraceDialog extends BaseDialog{
super("@trace");
addCloseButton();
setFillParent(false);
}
public void show(Player player, TraceInfo info){
@@ -22,16 +24,38 @@ public class TraceDialog extends BaseDialog{
table.defaults().pad(1);
table.defaults().left();
table.add(Core.bundle.format("trace.playername", player.name));
table.row();
table.add(Core.bundle.format("trace.ip", info.ip));
table.row();
table.add(Core.bundle.format("trace.id", info.uuid));
table.row();
table.add(Core.bundle.format("trace.modclient", info.modded));
table.row();
table.add(Core.bundle.format("trace.mobile", info.mobile));
table.row();
var style = Styles.emptyi;
float s = 28f;
table.table(c -> {
c.left().defaults().left();
c.button(Icon.copySmall, style, () -> copy(player.name)).size(s).padRight(4f);
c.add(Core.bundle.format("trace.playername", player.name)).row();
c.button(Icon.copySmall, style, () -> copy(info.ip)).size(s).padRight(4f);
c.add(Core.bundle.format("trace.ip", info.ip)).row();
c.button(Icon.copySmall, style, () -> copy(info.locale)).size(s).padRight(4f);
c.add(Core.bundle.format("trace.language", info.locale)).row();
c.button(Icon.copySmall, style, () -> copy(info.uuid)).size(s).padRight(4f);
c.add(Core.bundle.format("trace.id", info.uuid)).row();
}).row();
table.add(Core.bundle.format("trace.modclient", info.modded)).row();
table.add(Core.bundle.format("trace.mobile", info.mobile)).row();
table.add(Core.bundle.format("trace.times.joined", info.timesJoined)).row();
table.add(Core.bundle.format("trace.times.kicked", info.timesKicked)).row();
for(int i = 0; i < 2; i++){
table.add(i == 0 ? "@trace.ips" : "@trace.names").row();
String[] list = i == 0 ? info.ips : info.names;
table.pane(t -> {
t.left();
for(String val : list){
t.add("[lightgray]" + val).left().row();
}
}).padLeft(20f).fill().left().row();
}
table.add().pad(5);
table.row();
@@ -40,4 +64,9 @@ public class TraceDialog extends BaseDialog{
show();
}
private void copy(String content){
Core.app.setClipboardText(content);
ui.showInfoFade("@copied");
}
}

View File

@@ -12,48 +12,38 @@ import mindustry.gen.*;
import static mindustry.Vars.*;
public class BlockConfigFragment extends Fragment{
public class BlockConfigFragment{
Table table = new Table();
Building configTile;
Building selected;
@Override
public void build(Group parent){
table.visible = false;
parent.addChild(table);
//hacky way to hide block config when in menu
//TODO remove?
Core.scene.add(new Element(){
@Override
public void act(float delta){
super.act(delta);
if(state.isMenu()){
table.visible = false;
configTile = null;
}
}
});
Events.on(ResetEvent.class, e -> forceHide());
}
Events.on(ResetEvent.class, e -> {
table.visible = false;
configTile = null;
});
public void forceHide(){
table.visible = false;
selected = null;
}
public boolean isShown(){
return table.visible && configTile != null;
return table.visible && selected != null;
}
public Building getSelectedTile(){
return configTile;
public Building getSelected(){
return selected;
}
public void showConfig(Building tile){
if(selected != null) selected.onConfigureClosed();
if(tile.configTapped()){
configTile = tile;
selected = tile;
table.visible = true;
table.clear();
table.background(null);
tile.buildConfiguration(table);
table.pack();
table.setTransform(true);
@@ -61,28 +51,29 @@ public class BlockConfigFragment extends Fragment{
Actions.scaleTo(1f, 1f, 0.07f, Interp.pow3Out));
table.update(() -> {
if(configTile != null && configTile.shouldHideConfigure(player)){
if(selected != null && selected.shouldHideConfigure(player)){
hideConfig();
return;
}
table.setOrigin(Align.center);
if(configTile == null || configTile.block == Blocks.air || !configTile.isValid()){
if(selected == null || selected.block == Blocks.air || !selected.isValid()){
hideConfig();
}else{
configTile.updateTableAlign(table);
selected.updateTableAlign(table);
}
});
}
}
public boolean hasConfigMouse(){
Element e = Core.scene.hit(Core.input.mouseX(), Core.graphics.getHeight() - Core.input.mouseY(), true);
Element e = Core.scene.getHoverElement();
return e != null && (e == table || e.isDescendantOf(table));
}
public void hideConfig(){
configTile = null;
if(selected != null) selected.onConfigureClosed();
selected = null;
table.actions(Actions.scaleTo(0f, 1f, 0.06f, Interp.pow3Out), Actions.visible(false));
}
}

View File

@@ -14,23 +14,23 @@ import arc.scene.ui.layout.Stack;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.ui.*;
import java.util.*;
import static mindustry.Vars.*;
public class BlockInventoryFragment extends Fragment{
public class BlockInventoryFragment{
private static final float holdWithdraw = 20f;
private static final float holdShrink = 120f;
Table table = new Table();
Building tile;
Building build;
float holdTime = 0f, emptyTime;
boolean holding;
boolean holding, held;
float[] shrinkHoldTimes = new float[content.items().size];
Item lastItem;
@@ -38,7 +38,6 @@ public class BlockInventoryFragment extends Fragment{
Events.on(WorldLoadEvent.class, e -> hide());
}
@Override
public void build(Group parent){
table.name = "inventory";
table.setTransform(true);
@@ -47,13 +46,14 @@ public class BlockInventoryFragment extends Fragment{
}
public void showFor(Building t){
if(this.tile == t){
if(this.build == t){
hide();
return;
}
this.tile = t;
if(tile == null || !tile.block.isAccessible() || tile.items.total() == 0)
this.build = t;
if(build == null || !build.block.isAccessible() || build.items == null || build.items.total() == 0){
return;
}
rebuild(true);
}
@@ -66,7 +66,23 @@ public class BlockInventoryFragment extends Fragment{
table.update(null);
}), Actions.visible(false));
table.touchable = Touchable.disabled;
tile = null;
build = null;
}
private void takeItem(int requested){
if(!build.canWithdraw()) return;
//take everything
int amount = Math.min(requested, player.unit().maxAccepted(lastItem));
if(amount > 0){
Call.requestItem(player, build, lastItem, amount);
holding = false;
holdTime = 0f;
held = true;
if(net.client()) Events.fire(new WithdrawEvent(build, player, lastItem, amount));
}
}
private void rebuild(boolean actions){
@@ -81,35 +97,29 @@ public class BlockInventoryFragment extends Fragment{
table.touchable = Touchable.enabled;
table.update(() -> {
if(state.isMenu() || tile == null || !tile.isValid() || !tile.block.isAccessible() || emptyTime >= holdShrink){
if(state.isMenu() || build == null || !build.isValid() || !build.block.isAccessible() || emptyTime >= holdShrink){
hide();
}else{
if(tile.items.total() == 0){
if(build.items.total() == 0){
emptyTime += Time.delta;
}else{
emptyTime = 0f;
}
if(holding && lastItem != null){
holdTime += Time.delta;
if(holding && lastItem != null && (holdTime += Time.delta) >= holdWithdraw){
holdTime = 0f;
if(holdTime >= holdWithdraw){
int amount = Math.min(tile.items.get(lastItem), player.unit().maxAccepted(lastItem));
Call.requestItem(player, tile, lastItem, amount);
holding = false;
holdTime = 0f;
if(net.client()) Events.fire(new WithdrawEvent(tile, player, lastItem, amount));
}
//take one when held
takeItem(1);
}
updateTablePosition();
if(tile.block.hasItems){
if(build.block.hasItems){
boolean dirty = false;
if(shrinkHoldTimes.length != content.items().size) shrinkHoldTimes = new float[content.items().size];
for(int i = 0; i < content.items().size; i++){
boolean has = tile.items.has(content.item(i));
boolean has = build.items.has(content.item(i));
boolean had = container.contains(i);
if(has){
shrinkHoldTimes[i] = 0f;
@@ -134,44 +144,54 @@ public class BlockInventoryFragment extends Fragment{
table.margin(4f);
table.defaults().size(8 * 5).pad(4f);
if(tile.block.hasItems){
if(build.block.hasItems){
for(int i = 0; i < content.items().size; i++){
Item item = content.item(i);
if(!tile.items.has(item)) continue;
if(!build.items.has(item)) continue;
container.add(i);
Boolp canPick = () -> player.unit().acceptsItem(item) && !state.isPaused() && player.within(tile, itemTransferRange);
Boolp canPick = () -> !player.dead() && player.unit().acceptsItem(item) && !state.isPaused() && player.within(build, itemTransferRange);
HandCursorListener l = new HandCursorListener();
l.enabled = canPick;
Element image = itemImage(item.icon(Cicon.xlarge), () -> {
if(tile == null || !tile.isValid()){
Element image = itemImage(item.uiIcon, () -> {
if(build == null || !build.isValid()){
return "";
}
return round(tile.items.get(item));
return round(build.items.get(item));
});
image.addListener(l);
image.addListener(new InputListener(){
Boolp validClick = () -> !(!canPick.get() || build == null || !build.isValid() || build.items == null || !build.items.has(item));
image.addListener(new ClickListener(){
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
if(!canPick.get() || tile == null || !tile.isValid() || tile.items == null || !tile.items.has(item)) return false;
int amount = Math.min(1, player.unit().maxAccepted(item));
if(amount > 0){
Call.requestItem(player, tile, item, amount);
held = false;
if(validClick.get()){
lastItem = item;
holding = true;
holdTime = 0f;
if(net.client()) Events.fire(new WithdrawEvent(tile, player, item, amount));
}
return true;
return super.touchDown(event, x, y, pointer, button);
}
@Override
public void clicked(InputEvent event, float x, float y){
if(!validClick.get() || held) return;
//take all
takeItem(build.items.get(lastItem = item));
}
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
super.touchUp(event, x, y, pointer, button);
holding = false;
lastItem = null;
}
@@ -201,16 +221,16 @@ public class BlockInventoryFragment extends Fragment{
private String round(float f){
f = (int)f;
if(f >= 1000000){
return (int)(f / 1000000f) + "[gray]" + Core.bundle.getOrNull("unit.millions") + "[]";
return (int)(f / 1000000f) + "[gray]" + UI.millions;
}else if(f >= 1000){
return (int)(f / 1000) + Core.bundle.getOrNull("unit.thousands");
return (int)(f / 1000) + UI.thousands;
}else{
return (int)f + "";
}
}
private void updateTablePosition(){
Vec2 v = Core.input.mouseScreen(tile.x + tile.block.size * tilesize / 2f, tile.y + tile.block.size * tilesize / 2f);
Vec2 v = Core.input.mouseScreen(build.x + build.block.size * tilesize / 2f, build.y + build.block.size * tilesize / 2f);
table.pack();
table.setPosition(v.x, v.y, Align.topLeft);
}

View File

@@ -9,22 +9,22 @@ import arc.math.*;
import arc.scene.*;
import arc.scene.ui.*;
import arc.scene.ui.Label.*;
import arc.scene.ui.TextField.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.input.*;
import mindustry.type.*;
import mindustry.ui.*;
import static arc.Core.*;
import static mindustry.Vars.net;
import static mindustry.Vars.*;
public class ChatFragment extends Table{
private static final int messagesShown = 10;
private Seq<ChatMessage> messages = new Seq<>();
private Seq<String> messages = new Seq<>();
private float fadetime;
private boolean shown = false;
private TextField chatfield;
@@ -33,17 +33,11 @@ public class ChatFragment extends Table{
private Font font;
private GlyphLayout layout = new GlyphLayout();
private float offsetx = Scl.scl(4), offsety = Scl.scl(4), fontoffsetx = Scl.scl(2), chatspace = Scl.scl(50);
private Color shadowColor = new Color(0, 0, 0, 0.4f);
private Color shadowColor = new Color(0, 0, 0, 0.5f);
private float textspacing = Scl.scl(10);
private Seq<String> history = new Seq<>();
private int historyPos = 0;
private int scrollPos = 0;
private Fragment container = new Fragment(){
@Override
public void build(Group parent){
scene.add(ChatFragment.this);
}
};
public ChatFragment(){
super();
@@ -65,7 +59,7 @@ public class ChatFragment extends Table{
update(() -> {
if(net.active() && input.keyTap(Binding.chat) && (scene.getKeyboardFocus() == chatfield || scene.getKeyboardFocus() == null || ui.minimapfrag.shown()) && !ui.scriptfrag.shown()){
if(net.active() && input.keyTap(Binding.chat) && (scene.getKeyboardFocus() == chatfield || scene.getKeyboardFocus() == null || ui.minimapfrag.shown()) && !ui.consolefrag.shown()){
toggle();
}
@@ -90,8 +84,8 @@ public class ChatFragment extends Table{
setup();
}
public Fragment container(){
return container;
public void build(Group parent){
scene.add(this);
}
public void clearMessages(){
@@ -105,13 +99,14 @@ public class ChatFragment extends Table{
fieldlabel.getStyle().font = font;
fieldlabel.setStyle(fieldlabel.getStyle());
chatfield = new TextField("", new TextField.TextFieldStyle(scene.getStyle(TextField.TextFieldStyle.class)));
chatfield = new TextField("", new TextFieldStyle(scene.getStyle(TextFieldStyle.class)));
chatfield.setMaxLength(Vars.maxTextLength);
chatfield.getStyle().background = null;
chatfield.getStyle().font = Fonts.chat;
chatfield.getStyle().fontColor = Color.white;
chatfield.setStyle(chatfield.getStyle());
chatfield.typed(this::handleType);
bottom().left().marginBottom(offsety).marginLeft(offsetx * 2).add(fieldlabel).padBottom(6f);
add(chatfield).padBottom(offsety).padLeft(offsetx).growX().padRight(offsetx).height(28);
@@ -122,6 +117,27 @@ public class ChatFragment extends Table{
}
}
//no mobile support.
private void handleType(char c){
int cursor = chatfield.getCursorPosition();
if(c == ':'){
int index = chatfield.getText().lastIndexOf(':', cursor - 2);
if(index >= 0 && index < cursor){
String text = chatfield.getText().substring(index + 1, cursor - 1);
String uni = Fonts.getUnicodeStr(text);
if(uni != null && uni.length() > 0){
chatfield.setText(chatfield.getText().substring(0, index) + uni + chatfield.getText().substring(cursor));
chatfield.setCursorPosition(index + uni.length());
}
}
}
}
protected void rect(float x, float y, float w, float h){
//prevents texture bindings; the string lookup is irrelevant as it is only called <10 times per frame, and maps are very fast anyway
Draw.rect("whiteui", x + w/2f, y + h/2f, w, h);
}
@Override
public void draw(){
float opacity = Core.settings.getInt("chatopacity") / 100f;
@@ -130,7 +146,7 @@ public class ChatFragment extends Table{
Draw.color(shadowColor);
if(shown){
Fill.crect(offsetx, chatfield.y + scene.marginBottom, chatfield.getWidth() + 15f, chatfield.getHeight() - 1);
rect(offsetx, chatfield.y + scene.marginBottom, chatfield.getWidth() + 15f, chatfield.getHeight() - 1);
}
super.draw();
@@ -146,12 +162,13 @@ public class ChatFragment extends Table{
float theight = offsety + spacing + getMarginBottom() + scene.marginBottom;
for(int i = scrollPos; i < messages.size && i < messagesShown + scrollPos && (i < fadetime || shown); i++){
layout.setText(font, messages.get(i).formattedMessage, Color.white, textWidth, Align.bottomLeft, true);
layout.setText(font, messages.get(i), Color.white, textWidth, Align.bottomLeft, true);
theight += layout.height + textspacing;
if(i - scrollPos == 0) theight -= textspacing + 1;
font.getCache().clear();
font.getCache().addText(messages.get(i).formattedMessage, fontoffsetx + offsetx, offsety + theight, textWidth, Align.bottomLeft, true);
font.getCache().setColor(Color.white);
font.getCache().addText(messages.get(i), fontoffsetx + offsetx, offsety + theight, textWidth, Align.bottomLeft, true);
if(!shown && fadetime - i < 1f && fadetime - i >= 0f){
font.getCache().setAlphas((fadetime - i) * opacity);
@@ -160,7 +177,7 @@ public class ChatFragment extends Table{
font.getCache().setAlphas(opacity);
}
Fill.crect(offsetx, theight - layout.height - 2, textWidth + Scl.scl(4f), layout.height + textspacing);
rect(offsetx, theight - layout.height - 2, textWidth + Scl.scl(4f), layout.height + textspacing);
Draw.color(shadowColor);
Draw.alpha(opacity * shadowColor.a);
@@ -178,10 +195,13 @@ public class ChatFragment extends Table{
String message = chatfield.getText().trim();
clearChatInput();
if(message.isEmpty()) return;
//avoid sending prefix-empty messages
if(message.isEmpty() || (message.startsWith(mode.prefix) && message.substring(mode.prefix.length()).isEmpty())) return;
history.insert(1, message);
Events.fire(new ClientChatEvent(message));
Call.sendChatMessage(message);
}
@@ -206,7 +226,7 @@ public class ChatFragment extends Table{
}
}else{
//sending chat has a delay; workaround for issue #1943
Time.run(2f, () ->{
Time.runTask(2f, () ->{
scene.setKeyboardFocus(null);
shown = false;
scrollPos = 0;
@@ -257,32 +277,16 @@ public class ChatFragment extends Table{
return shown;
}
public void addMessage(String message, String sender){
if(sender == null && message == null) return;
messages.insert(0, new ChatMessage(message, sender));
public void addMessage(String message){
if(message == null) return;
messages.insert(0, message);
fadetime += 1f;
fadetime = Math.min(fadetime, messagesShown) + 1f;
if(scrollPos > 0) scrollPos++;
}
private static class ChatMessage{
public final String sender;
public final String message;
public final String formattedMessage;
public ChatMessage(String message, String sender){
this.message = message;
this.sender = sender;
if(sender == null){ //no sender, this is a server message?
formattedMessage = message == null ? "" : message;
}else{
formattedMessage = "[coral][[" + sender + "[coral]]:[white] " + message;
}
}
}
private enum ChatMode{
normal(""),
team("/t"),

View File

@@ -8,16 +8,18 @@ import arc.math.*;
import arc.scene.*;
import arc.scene.ui.*;
import arc.scene.ui.Label.*;
import arc.scene.ui.TextField.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.game.EventType.*;
import mindustry.input.*;
import mindustry.ui.*;
import static arc.Core.*;
import static mindustry.Vars.*;
public class ScriptConsoleFragment extends Table{
public class ConsoleFragment extends Table{
private static final int messagesShown = 30;
private Seq<String> messages = new Seq<>();
private boolean open = false, shown;
@@ -31,23 +33,23 @@ public class ScriptConsoleFragment extends Table{
private Seq<String> history = new Seq<>();
private int historyPos = 0;
private int scrollPos = 0;
private Fragment container = new Fragment(){
@Override
public void build(Group parent){
scene.add(ScriptConsoleFragment.this);
}
};
public ScriptConsoleFragment(){
public ConsoleFragment(){
setFillParent(true);
font = Fonts.def;
visible(() -> {
if(input.keyTap(Binding.console) && (scene.getKeyboardFocus() == chatfield || scene.getKeyboardFocus() == null) && !ui.chatfrag.shown()){
if(input.keyTap(Binding.console) && settings.getBool("console") && (scene.getKeyboardFocus() == chatfield || scene.getKeyboardFocus() == null) && !ui.chatfrag.shown()){
shown = !shown;
if(shown && !open && enableConsole){
if(shown && !open && settings.getBool("console")){
toggle();
}
if(shown){
chatfield.requestKeyboard();
}else if(scene.getKeyboardFocus() == chatfield){
scene.setKeyboardFocus(null);
scene.setScrollFocus(null);
}
clearChatInput();
}
@@ -55,7 +57,7 @@ public class ScriptConsoleFragment extends Table{
});
update(() -> {
if(input.keyTap(Binding.chat) && enableConsole && (scene.getKeyboardFocus() == chatfield || scene.getKeyboardFocus() == null)){
if(input.keyTap(Binding.chat) && settings.getBool("console") && (scene.getKeyboardFocus() == chatfield || scene.getKeyboardFocus() == null)){
toggle();
}
@@ -71,15 +73,15 @@ public class ScriptConsoleFragment extends Table{
}
}
scrollPos = (int)Mathf.clamp(scrollPos + input.axis(Binding.chat_scroll), 0, Math.max(0, messages.size - messagesShown));
scrollPos = (int)Mathf.clamp(scrollPos + input.axis(Binding.chat_scroll), 0, Math.max(0, messages.size));
});
history.insert(0, "");
setup();
}
public Fragment container(){
return container;
public void build(Group parent){
scene.add(this);
}
public void clearMessages(){
@@ -93,9 +95,8 @@ public class ScriptConsoleFragment extends Table{
fieldlabel.getStyle().font = font;
fieldlabel.setStyle(fieldlabel.getStyle());
chatfield = new TextField("", new TextField.TextFieldStyle(scene.getStyle(TextField.TextFieldStyle.class)));
chatfield = new TextField("", new TextFieldStyle(scene.getStyle(TextFieldStyle.class)));
chatfield.getStyle().background = null;
chatfield.getStyle().font = Fonts.chat;
chatfield.getStyle().fontColor = Color.white;
chatfield.setStyle(chatfield.getStyle());
@@ -104,6 +105,10 @@ public class ScriptConsoleFragment extends Table{
add(chatfield).padBottom(offsety).padLeft(offsetx).growX().padRight(offsetx).height(28);
}
protected void rect(float x, float y, float w, float h){
Draw.rect("whiteui", x + w/2f, y + h/2f, w, h);
}
@Override
public void draw(){
float opacity = 1f;
@@ -112,7 +117,7 @@ public class ScriptConsoleFragment extends Table{
Draw.color(shadowColor);
if(open){
Fill.crect(offsetx, chatfield.y + scene.marginBottom, chatfield.getWidth() + 15f, chatfield.getHeight() - 1);
rect(offsetx, chatfield.y + scene.marginBottom, chatfield.getWidth() + 15f, chatfield.getHeight() - 1);
}
super.draw();
@@ -133,6 +138,7 @@ public class ScriptConsoleFragment extends Table{
if(i - scrollPos == 0) theight -= textspacing + 1;
font.getCache().clear();
font.getCache().setColor(Color.white);
font.getCache().addText(messages.get(i), fontoffsetx + offsetx, offsety + theight, textWidth, Align.bottomLeft, true);
if(!open){
@@ -142,7 +148,7 @@ public class ScriptConsoleFragment extends Table{
font.getCache().setAlphas(opacity);
}
Fill.crect(offsetx, theight - layout.height - 2, textWidth + Scl.scl(4f), layout.height + textspacing);
rect(offsetx, theight - layout.height - 2, textWidth + Scl.scl(4f), layout.height + textspacing);
Draw.color(shadowColor);
Draw.alpha(opacity * shadowColor.a);
@@ -167,12 +173,24 @@ public class ScriptConsoleFragment extends Table{
history.insert(1, message);
addMessage("[lightgray]> " + message.replace("[", "[["));
addMessage(mods.getScripts().runConsole(message).replace("[", "[["));
addMessage(mods.getScripts().runConsole(injectConsoleVariables() + message).replace("[", "[["));
}
public String injectConsoleVariables(){
return
"var unit = Vars.player.unit();" +
"var team = Vars.player.team();" +
"var core = Vars.player.core();" +
"var items = Vars.player.team().items();" +
"var build = Vars.world.buildWorld(Core.input.mouseWorldX(), Core.input.mouseWorldY());" +
"var cursor = Vars.world.tileWorld(Core.input.mouseWorldX(), Core.input.mouseWorldY());" +
"\n";
}
public void toggle(){
if(!open){
Events.fire(Trigger.openConsole);
scene.setKeyboardFocus(chatfield);
open = !open;
if(mobile){

View File

@@ -7,11 +7,10 @@ import arc.scene.*;
import arc.scene.event.*;
/** Fades in a black overlay.*/
public class FadeInFragment extends Fragment{
public class FadeInFragment{
private static final float duration = 40f;
float time = 0f;
@Override
public void build(Group parent){
parent.addChild(new Element(){
{

View File

@@ -1,7 +0,0 @@
package mindustry.ui.fragments;
import arc.scene.*;
public abstract class Fragment{
public abstract void build(Group parent);
}

View File

@@ -11,22 +11,26 @@ import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.ai.types.*;
import mindustry.content.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.input.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.ConstructBlock.*;
import mindustry.world.blocks.payloads.PayloadBlock.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
public class HintsFragment extends Fragment{
public class HintsFragment{
private static final Boolp isTutorial = () -> Vars.state.rules.sector == SectorPresets.groundZero.sector;
private static final float foutTime = 0.6f;
/** All hints to be displayed in the game. */
public Seq<Hint> hints = new Seq<>().and(DefaultHint.values()).as();
public Seq<Hint> hints = new Seq<>().add(DefaultHint.values()).as();
@Nullable Hint current;
Group group = new WidgetGroup();
@@ -34,7 +38,6 @@ public class HintsFragment extends Fragment{
ObjectSet<Block> placedBlocks = new ObjectSet<>();
Table last;
@Override
public void build(Group parent){
group.setFillParent(true);
group.touchable = Touchable.childrenOnly;
@@ -52,8 +55,14 @@ public class HintsFragment extends Fragment{
Hint hint = hints.find(Hint::show);
if(hint != null && hint.complete()){
hints.remove(hint);
}else if(hint != null){
}else if(hint != null && !renderer.isCutscene() && state.isGame() && control.saves.getTotalPlaytime() > 8000){
display(hint);
}else{
//moused over a derelict structure
var build = world.buildWorld(Core.input.mouseWorldX(), Core.input.mouseWorldY());
if(build != null && build.team == Team.derelict){
events.add("derelictmouse");
}
}
}
});
@@ -76,6 +85,17 @@ public class HintsFragment extends Fragment{
events.clear();
});
Events.on(BuildingCommandEvent.class, e -> {
if(e.building instanceof PayloadBlockBuild<?>){
events.add("factorycontrol");
}
});
Events.on(BlockBuildBeginEvent.class, e -> {
if(e.unit == player.unit() && e.tile.build instanceof ConstructBuild c && c.prevBuild != null && c.prevBuild.contains(b -> b.team == Team.derelict)){
events.add("derelictbreak");
}
});
}
void checkNext(){
@@ -85,7 +105,7 @@ public class HintsFragment extends Fragment{
hints.sort(Hint::order);
Hint first = hints.find(Hint::show);
if(first != null){
if(first != null && !renderer.isCutscene() && state.isGame()){
hints.remove(first);
display(first);
}
@@ -138,42 +158,49 @@ public class HintsFragment extends Fragment{
return current != null;
}
static boolean isSerpulo(){
return !state.rules.hasEnv(Env.scorching);
}
public enum DefaultHint implements Hint{
desktopMove(visibleDesktop, () -> Core.input.axis(Binding.move_x) != 0 || Core.input.axis(Binding.move_y) != 0),
zoom(visibleDesktop, () -> Core.input.axis(KeyCode.scroll) != 0),
mine(() -> player.unit().canMine() && isTutorial.get(), () -> player.unit().mining()),
placeDrill(isTutorial, () -> ui.hints.placedBlocks.contains(Blocks.mechanicalDrill)),
placeConveyor(isTutorial, () -> ui.hints.placedBlocks.contains(Blocks.conveyor)),
placeTurret(isTutorial, () -> ui.hints.placedBlocks.contains(Blocks.duo)),
breaking(isTutorial, () -> ui.hints.events.contains("break")),
desktopShoot(visibleDesktop, () -> Vars.state.enemies > 0, () -> player.shooting),
depositItems(() -> player.unit().hasItem(), () -> !player.unit().hasItem()),
desktopPause(visibleDesktop, () -> isTutorial.get() && !Vars.net.active(), () -> Core.input.keyTap(Binding.pause)),
research(isTutorial, () -> ui.research.isShown()),
unitControl(() -> state.rules.defaultTeam.data().units.size > 2 && !net.active() && !player.dead(), () -> !player.dead() && !player.unit().spawnedByCore),
breaking(() -> isTutorial.get() && state.rules.defaultTeam.data().getCount(Blocks.conveyor) > 5, () -> ui.hints.events.contains("break")),
desktopShoot(visibleDesktop, () -> isSerpulo() && Vars.state.enemies > 0, () -> player.shooting),
depositItems(() -> !player.dead() && player.unit().hasItem(), () -> !player.dead() && !player.unit().hasItem()),
desktopPause(visibleDesktop, () -> isTutorial.get() && !Vars.net.active() && state.wave >= 2, () -> Core.input.keyTap(Binding.pause)),
unitControl(() -> isSerpulo() && state.rules.defaultTeam.data().units.size > 2 && !net.active() && !player.dead(), () -> !player.dead() && !player.unit().spawnedByCore),
unitSelectControl(() -> isSerpulo() && state.rules.defaultTeam.data().units.size > 3 && !net.active() && !player.dead(),
() -> control.input.commandMode && control.input.selectedUnits.size > 0 && control.input.selectedUnits.first().controller() instanceof CommandAI ai && ai.targetPos != null),
respawn(visibleMobile, () -> !player.dead() && !player.unit().spawnedByCore, () -> !player.dead() && player.unit().spawnedByCore),
launch(() -> isTutorial.get() && state.rules.sector.isCaptured(), () -> ui.planet.isShown()),
schematicSelect(visibleDesktop, () -> ui.hints.placedBlocks.contains(Blocks.router), () -> Core.input.keyRelease(Binding.schematic_select) || Core.input.keyTap(Binding.pick)),
launch(() -> (isTutorial.get() || Vars.state.rules.sector == SectorPresets.onset.sector) && state.rules.sector.isCaptured(), () -> ui.planet.isShown()),
schematicSelect(visibleDesktop, () -> ui.hints.placedBlocks.contains(Blocks.router) || ui.hints.placedBlocks.contains(Blocks.ductRouter), () -> Core.input.keyRelease(Binding.schematic_select) || Core.input.keyTap(Binding.pick)),
conveyorPathfind(() -> control.input.block == Blocks.titaniumConveyor, () -> Core.input.keyRelease(Binding.diagonal_placement) || (mobile && Core.settings.getBool("swapdiagonal"))),
boost(visibleDesktop, () -> !player.dead() && player.unit().type.canBoost, () -> Core.input.keyDown(Binding.boost)),
command(() -> state.rules.defaultTeam.data().units.size > 3 && !net.active(), () -> player.unit().isCommanding()),
payloadPickup(() -> !player.unit().dead && player.unit() instanceof Payloadc p && p.payloads().isEmpty(), () -> player.unit() instanceof Payloadc p && p.payloads().any()),
payloadDrop(() -> !player.unit().dead && player.unit() instanceof Payloadc p && p.payloads().any(), () -> player.unit() instanceof Payloadc p && p.payloads().isEmpty()),
waveFire(() -> Groups.fire.size() > 0 && Blocks.wave.unlockedNow(), () -> indexer.getAllied(state.rules.defaultTeam, BlockFlag.extinguisher).size() > 0),
blockInfo(() -> !(state.isCampaign() && state.rules.sector == SectorPresets.groundZero.sector && state.wave < 3), () -> ui.content.isShown()),
derelict(() -> ui.hints.events.contains("derelictmouse") && !isTutorial.get(), () -> ui.hints.events.contains("derelictbreak")),
payloadPickup(() -> isSerpulo() && !player.dead() && player.unit() instanceof Payloadc p && p.payloads().isEmpty(), () -> player.unit() instanceof Payloadc p && p.payloads().any()),
payloadDrop(() -> !player.dead() && player.unit() instanceof Payloadc p && p.payloads().any(), () -> player.unit() instanceof Payloadc p && p.payloads().isEmpty()),
waveFire(() -> Groups.fire.size() > 0 && Blocks.wave.unlockedNow(), () -> indexer.getFlagged(state.rules.defaultTeam, BlockFlag.extinguisher).size > 0),
generator(() -> control.input.block == Blocks.combustionGenerator, () -> ui.hints.placedBlocks.contains(Blocks.combustionGenerator)),
guardian(() -> state.boss() != null && state.boss().armor >= 4, () -> state.boss() == null),
coreUpgrade(() -> state.isCampaign() && Blocks.coreFoundation.unlocked()
rebuildSelect(() -> state.rules.defaultTeam.data().plans.size >= 10, () -> control.input.isRebuildSelecting()),
guardian(() -> state.boss() != null && isSerpulo() && state.boss().armor >= 4, () -> state.boss() == null),
factoryControl(() -> !(state.isCampaign() && state.rules.sector.preset == SectorPresets.onset) &&
state.rules.defaultTeam.data().getBuildings(Blocks.tankFabricator).size + state.rules.defaultTeam.data().getBuildings(Blocks.groundFactory).size > 0, () -> ui.hints.events.contains("factorycontrol")),
coreUpgrade(() -> state.isCampaign() && state.rules.sector.planet == Planets.serpulo && Blocks.coreFoundation.unlocked()
&& state.rules.defaultTeam.core() != null
&& state.rules.defaultTeam.core().block == Blocks.coreShard
&& state.rules.defaultTeam.core().items.has(Blocks.coreFoundation.requirements),
() -> ui.hints.placedBlocks.contains(Blocks.coreFoundation)),
presetLaunch(() -> state.isCampaign()
&& state.getSector().preset == null
&& SectorPresets.frozenForest.unlocked()
&& SectorPresets.frozenForest.sector.save == null,
&& state.getSector().preset == null,
() -> state.isCampaign() && state.getSector().preset == SectorPresets.frozenForest),
coreIncinerate(() -> state.isCampaign() && state.rules.defaultTeam.core() != null && state.rules.defaultTeam.core().items.get(Items.copper) >= state.rules.defaultTeam.core().storageCapacity - 10, () -> false),
coopCampaign(() -> net.client() && state.isCampaign() && SectorPresets.groundZero.sector.hasBase(), () -> false),
presetDifficulty(() -> state.isCampaign()
&& state.getSector().preset == null
&& state.getSector().threat >= 0.5f
&& !SectorPresets.tarFields.sector.isCaptured(), //appear only when the player hasn't progressed much in the game yet
() -> state.isCampaign() && state.getSector().preset != null),
coreIncinerate(() -> state.isCampaign() && state.rules.defaultTeam.core() != null && state.rules.defaultTeam.core().items.get(Items.copper) >= state.rules.defaultTeam.core().storageCapacity - 10, () -> false)
;
@Nullable

View File

@@ -14,9 +14,11 @@ import arc.scene.ui.ImageButton.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.core.GameState.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
@@ -26,17 +28,23 @@ import mindustry.input.*;
import mindustry.net.Packets.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
import static mindustry.gen.Tex.*;
public class HudFragment extends Fragment{
private static final float dsize = 65f;
public class HudFragment{
private static final float dsize = 65f, pauseHeight = 36f;
public final PlacementFragment blockfrag = new PlacementFragment();
public PlacementFragment blockfrag = new PlacementFragment();
public CoreItemsDisplay coreItems = new CoreItemsDisplay();
public boolean shown = true;
private ImageButton flip;
private CoreItemsDisplay coreItems = new CoreItemsDisplay();
private String hudText = "";
private boolean showHudText;
@@ -45,13 +53,86 @@ public class HudFragment extends Fragment{
private Table lastUnlockLayout;
private long lastToast;
@Override
private Seq<Block> blocksOut = new Seq<>();
private void addBlockSelection(Table cont){
Table blockSelection = new Table();
var pane = new ScrollPane(blockSelection, Styles.smallPane);
pane.setFadeScrollBars(false);
Planet[] last = {state.rules.planet};
pane.update(() -> {
if(pane.hasScroll()){
Element result = Core.scene.getHoverElement();
if(result == null || !result.isDescendantOf(pane)){
Core.scene.setScrollFocus(null);
}
}
if(state.rules.planet != last[0]){
last[0] = state.rules.planet;
rebuildBlockSelection(blockSelection, "");
}
});
cont.table(search -> {
search.image(Icon.zoom).padRight(8);
search.field("", text -> rebuildBlockSelection(blockSelection, text)).growX()
.name("editor/search").maxTextLength(maxNameLength).get().setMessageText("@players.search");
}).growX().pad(-2).padLeft(6f);
cont.row();
cont.add(pane).expandY().top().left();
rebuildBlockSelection(blockSelection, "");
}
private void rebuildBlockSelection(Table blockSelection, String searchText){
blockSelection.clear();
blocksOut.clear();
blocksOut.addAll(Vars.content.blocks());
blocksOut.sort((b1, b2) -> {
int synth = Boolean.compare(b1.synthetic(), b2.synthetic());
if(synth != 0) return synth;
int ore = Boolean.compare(b1 instanceof OverlayFloor && b1 != Blocks.removeOre, b2 instanceof OverlayFloor && b2 != Blocks.removeOre);
if(ore != 0) return ore;
return Integer.compare(b1.id, b2.id);
});
int i = 0;
for(Block block : blocksOut){
TextureRegion region = block.uiIcon;
if(!Core.atlas.isFound(region)
|| (!block.inEditor && !(block instanceof RemoveWall) && !(block instanceof RemoveOre))
|| !block.isOnPlanet(state.rules.planet)
|| block.buildVisibility == BuildVisibility.debugOnly
|| (!searchText.isEmpty() && !block.localizedName.toLowerCase().contains(searchText.toLowerCase()))
) continue;
ImageButton button = new ImageButton(Tex.whiteui, Styles.clearNoneTogglei);
button.getStyle().imageUp = new TextureRegionDrawable(region);
button.clicked(() -> control.input.block = block);
button.resizeImage(8 * 4f);
button.update(() -> button.setChecked(control.input.block == block));
blockSelection.add(button).size(48f).tooltip(block.localizedName);
if(++i % 6 == 0){
blockSelection.row();
}
}
if(i == 0){
blockSelection.add("@none.found").padLeft(54f).padTop(10f);
}
}
public void build(Group parent){
//warn about guardian/boss waves
Events.on(WaveEvent.class, e -> {
int max = 10;
int winWave = state.isCampaign() && state.rules.winWave > 0 ? state.rules.winWave : Integer.MAX_VALUE;
int winWave = state.rules.winWave > 0 ? state.rules.winWave : Integer.MAX_VALUE;
outer:
for(int i = state.wave - 1; i <= Math.min(state.wave + max, winWave - 2); i++){
for(SpawnGroup group : state.rules.spawns){
@@ -60,7 +141,7 @@ public class HudFragment extends Fragment{
//increments at which to warn about incoming guardian
if(diff == 1 || diff == 2 || diff == 5 || diff == 10){
showToast(Icon.warning, Core.bundle.format("wave.guardianwarn" + (diff == 1 ? ".one" : ""), diff));
showToast(Icon.warning, group.type.emoji() + " " + Core.bundle.format("wave.guardianwarn" + (diff == 1 ? ".one" : ""), diff));
}
break outer;
@@ -70,7 +151,11 @@ public class HudFragment extends Fragment{
});
Events.on(SectorCaptureEvent.class, e -> {
showToast(Core.bundle.format("sector.captured", e.sector.isBeingPlayed() ? "" : e.sector.name() + " "));
if(e.sector.isBeingPlayed()){
ui.announce("@sector.capture.current", 5f);
}else{
showToast(Core.bundle.format("sector.capture", e.sector.name()));
}
});
Events.on(SectorLoseEvent.class, e -> {
@@ -89,8 +174,17 @@ public class HudFragment extends Fragment{
//paused table
parent.fill(t -> {
t.name = "paused";
t.top().visible(() -> state.isPaused() && shown).touchable = Touchable.disabled;
t.table(Styles.black5, top -> top.label(() -> state.gameOver && state.isCampaign() ? "@sector.curlost" : "@paused").style(Styles.outlineLabel).pad(8f)).growX();
t.top().visible(() -> state.isPaused() && shown && !netServer.isWaitingForPlayers()).touchable = Touchable.disabled;
t.table(Styles.black6, top -> top.label(() -> state.gameOver && state.isCampaign() ? "@sector.curlost" : "@paused")
.style(Styles.outlineLabel).pad(8f)).height(pauseHeight).growX();
//.padLeft(dsize * 5 + 4f) to prevent alpha overlap on left
});
//"waiting for players"
parent.fill(t -> {
t.name = "waiting";
t.visible(() -> netServer.isWaitingForPlayers() && state.isPaused() && shown).touchable = Touchable.disabled;
t.table(Styles.black6, top -> top.add("@waiting.players").style(Styles.outlineLabel).pad(18f));
});
//minimap + position
@@ -101,9 +195,12 @@ public class HudFragment extends Fragment{
t.add(new Minimap()).name("minimap");
t.row();
//position
t.label(() -> player.tileX() + "," + player.tileY())
.visible(() -> Core.settings.getBool("position"))
t.label(() ->
(Core.settings.getBool("position") ? player.tileX() + "," + player.tileY() + "\n" : "") +
(Core.settings.getBool("mouseposition") ? "[lightgray]" + World.toTile(Core.input.mouseWorldX()) + "," + World.toTile(Core.input.mouseWorldY()) : ""))
.visible(() -> Core.settings.getBool("position") || Core.settings.getBool("mouseposition"))
.touchable(Touchable.disabled)
.style(Styles.outlineLabel)
.name("position");
t.top().right();
});
@@ -116,12 +213,19 @@ public class HudFragment extends Fragment{
cont.top().left();
if(mobile){
//for better inset visuals
cont.rect((x, y, w, h) -> {
if(Core.scene.marginTop > 0){
Tex.paneRight.draw(x, y, w, Core.scene.marginTop);
}
}).fillX().row();
cont.table(select -> {
select.name = "mobile buttons";
select.left();
select.defaults().size(dsize).left();
ImageButtonStyle style = Styles.clearTransi;
ImageButtonStyle style = Styles.cleari;
select.button(Icon.menu, style, ui.paused::show).name("menu");
flip = select.button(Icon.upOpen, style, this::toggleMenus).get();
@@ -134,14 +238,14 @@ public class HudFragment extends Fragment{
if(net.active()){
ui.listfrag.toggle();
}else{
state.set(state.is(State.paused) ? State.playing : State.paused);
state.set(state.isPaused() ? State.playing : State.paused);
}
}).name("pause").update(i -> {
if(net.active()){
i.getStyle().imageUp = Icon.players;
}else{
i.setDisabled(false);
i.getStyle().imageUp = state.is(State.paused) ? Icon.play : Icon.pause;
i.getStyle().imageUp = state.isPaused() ? Icon.play : Icon.pause;
}
});
@@ -176,7 +280,7 @@ public class HudFragment extends Fragment{
}
cont.update(() -> {
if(Core.input.keyTap(Binding.toggle_menus) && !ui.chatfrag.shown() && !Core.scene.hasDialog() && !(Core.scene.getKeyboardFocus() instanceof TextField)){
if(Core.input.keyTap(Binding.toggle_menus) && !ui.chatfrag.shown() && !Core.scene.hasDialog() && !Core.scene.hasField()){
Core.settings.getBoolOnce("ui-hidden", () -> {
ui.announce(Core.bundle.format("showui", Core.keybinds.get(Binding.toggle_menus).key.toString(), 11));
});
@@ -186,8 +290,19 @@ public class HudFragment extends Fragment{
Table wavesMain, editorMain;
cont.stack(wavesMain = new Table(), editorMain = new Table()).height(wavesMain.getPrefHeight())
.name("waves/editor");
cont.stack(wavesMain = new Table(), editorMain = new Table(), new Element(){
//this may seem insane, but adding an empty element of a specific height to this stack fixes layout issues on mobile.
{
visible = false;
touchable = Touchable.disabled;
}
@Override
public float getPrefHeight(){
return Scl.scl(120f);
}
}).name("waves/editor");
wavesMain.visible(() -> shown && !state.isEditor());
wavesMain.top().left().name = "waves";
@@ -196,45 +311,63 @@ public class HudFragment extends Fragment{
//wave info button with text
s.add(makeStatusTable()).grow().name("status");
var rightStyle = new ImageButtonStyle(){{
over = buttonRightOver;
down = buttonRightDown;
up = buttonRight;
disabled = buttonRightDisabled;
imageDisabledColor = Color.clear;
imageUpColor = Color.white;
}};
//table with button to skip wave
s.button(Icon.play, Styles.righti, 30f, () -> {
s.button(Icon.play, rightStyle, 30f, () -> {
if(net.client() && player.admin){
Call.adminRequest(player, AdminAction.wave);
Call.adminRequest(player, AdminAction.wave, null);
}else{
logic.skipWave();
}
}).growY().fillX().right().width(40f).disabled(b -> !canSkipWave()).name("skip");
}).width(dsize * 5 + 4f);
}).growY().fillX().right().width(40f).disabled(b -> !canSkipWave()).name("skip").get().toBack();
}).width(dsize * 5 + 4f).name("statustable");
wavesMain.row();
wavesMain.table(Tex.button, t -> t.margin(10f).add(new Bar("boss.health", Pal.health, () -> state.boss() == null ? 0f : state.boss().healthf()).blink(Color.white))
.grow()).fillX().visible(() -> state.rules.waves && state.boss() != null).height(60f).name("boss");
wavesMain.row();
addInfoTable(wavesMain.table().width(dsize * 5f + 4f).left().get());
editorMain.name = "editor";
editorMain.table(Tex.buttonEdge4, t -> {
//t.margin(0f);
t.name = "teams";
t.add("@editor.teams").growX().left();
t.row();
t.table(teams -> {
t.top().table(teams -> {
teams.left();
int i = 0;
for(Team team : Team.baseTeams){
ImageButton button = teams.button(Tex.whiteui, Styles.clearTogglePartiali, 40f, () -> Call.setPlayerTeamEditor(player, team))
.size(50f).margin(6f).get();
ImageButton button = teams.button(Tex.whiteui, Styles.clearNoneTogglei, 33f, () -> Call.setPlayerTeamEditor(player, team))
.size(45f).margin(6f).get();
button.getImageCell().grow();
button.getStyle().imageUpColor = team.color;
button.update(() -> button.setChecked(player.team() == team));
if(++i % 3 == 0){
teams.row();
}
}
}).left();
}).width(dsize * 5 + 4f);
teams.button(Icon.downOpen, Styles.emptyi, () -> Core.settings.put("editor-blocks-shown", !Core.settings.getBool("editor-blocks-shown")))
.size(45f).update(m -> m.getStyle().imageUp = (Core.settings.getBool("editor-blocks-shown") ? Icon.upOpen : Icon.downOpen));
}).top().left().row();
t.collapser(this::addBlockSelection, () -> Core.settings.getBool("editor-blocks-shown"));
}).width(dsize * 5 + 4f).top();
if(mobile){
editorMain.row().spacerY(() -> {
if(control.input instanceof MobileInput mob && Core.settings.getBool("editor-blocks-shown")){
if(Core.graphics.isPortrait()) return Core.graphics.getHeight() / 2f / Scl.scl(1f);
if(mob.hasSchematic()) return 156f;
if(mob.showCancel()) return 50f;
}
return 0f;
});
}
editorMain.row().add().growY();
editorMain.visible(() -> shown && state.isEditor());
//fps display
@@ -244,6 +377,7 @@ public class HudFragment extends Fragment{
info.top().left().margin(4).visible(() -> Core.settings.getBool("fps") && shown);
IntFormat fps = new IntFormat("fps");
IntFormat ping = new IntFormat("ping");
IntFormat tps = new IntFormat("tps");
IntFormat mem = new IntFormat("memory");
IntFormat memnative = new IntFormat("memory2");
@@ -257,68 +391,99 @@ public class HudFragment extends Fragment{
}
info.row();
info.label(() -> ping.get(netClient.getPing())).visible(net::client).left().style(Styles.outlineLabel).name("ping");
info.label(() -> ping.get(netClient.getPing())).visible(net::client).left().style(Styles.outlineLabel).name("ping").row();
info.label(() -> tps.get(state.serverTps == -1 ? 60 : state.serverTps)).visible(net::client).left().style(Styles.outlineLabel).name("tps").row();
}).top().left();
});
//core items
//core info
parent.fill(t -> {
t.name = "coreitems";
t.top().add(coreItems);
t.visible(() -> Core.settings.getBool("coreitems") && !mobile && !state.isPaused() && shown);
t.top();
if(Core.settings.getBool("macnotch") ){
t.margin(macNotchHeight);
}
t.visible(() -> shown);
t.name = "coreinfo";
t.collapser(v -> v.add().height(pauseHeight), () -> state.isPaused() && !netServer.isWaitingForPlayers()).row();
t.table(c -> {
//core items
c.top().collapser(coreItems, () -> Core.settings.getBool("coreitems") && !mobile && shown).fillX().row();
float notifDuration = 240f;
float[] coreAttackTime = {0};
Events.run(Trigger.teamCoreDamage, () -> coreAttackTime[0] = notifDuration);
//'core is under attack' table
c.collapser(top -> top.background(Styles.black6).add("@coreattack").pad(8)
.update(label -> label.color.set(Color.orange).lerp(Color.scarlet, Mathf.absin(Time.time, 2f, 1f))), true,
() -> {
if(!shown || state.isPaused()) return false;
if(state.isMenu() || !player.team().data().hasCore()){
coreAttackTime[0] = 0f;
return false;
}
return (coreAttackTime[0] -= Time.delta) > 0;
})
.touchable(Touchable.disabled)
.fillX().row();
}).row();
var bossb = new StringBuilder();
var bossText = Core.bundle.get("guardian");
int maxBosses = 6;
t.table(v -> v.margin(10f)
.add(new Bar(() -> {
bossb.setLength(0);
for(int i = 0; i < Math.min(state.teams.bosses.size, maxBosses); i++){
bossb.append(state.teams.bosses.get(i).type.emoji());
}
if(state.teams.bosses.size > maxBosses){
bossb.append("[accent]+[]");
}
bossb.append(" ");
bossb.append(bossText);
return bossb;
}, () -> Pal.health, () -> {
if(state.boss() == null) return 0f;
float max = 0f, val = 0f;
for(var boss : state.teams.bosses){
max += boss.maxHealth;
val += boss.health;
}
return max == 0f ? 0f : val / max;
}).blink(Color.white).outline(new Color(0, 0, 0, 0.6f), 7f)).grow())
.fillX().width(320f).height(60f).name("boss").visible(() -> state.rules.waves && state.boss() != null && !(mobile && Core.graphics.isPortrait())).padTop(7).row();
t.table(Styles.black3, p -> p.margin(4).label(() -> hudText).style(Styles.outlineLabel)).touchable(Touchable.disabled).with(p -> p.visible(() -> {
p.color.a = Mathf.lerpDelta(p.color.a, Mathf.num(showHudText), 0.2f);
if(state.isMenu()){
p.color.a = 0f;
showHudText = false;
}
return p.color.a >= 0.001f;
}));
});
//spawner warning
parent.fill(t -> {
t.name = "nearpoint";
t.touchable = Touchable.disabled;
t.table(Styles.black, c -> c.add("@nearpoint")
t.table(Styles.black6, c -> c.add("@nearpoint")
.update(l -> l.setColor(Tmp.c1.set(Color.white).lerp(Color.scarlet, Mathf.absin(Time.time, 10f, 1f))))
.get().setAlignment(Align.center, Align.center))
.labelAlign(Align.center, Align.center))
.margin(6).update(u -> u.color.a = Mathf.lerpDelta(u.color.a, Mathf.num(spawner.playerNear()), 0.1f)).get().color.a = 0f;
});
parent.fill(t -> {
t.name = "waiting";
t.visible(() -> netServer.isWaitingForPlayers());
t.table(Tex.button, c -> c.add("@waiting.players"));
});
//'core is under attack' table
parent.fill(t -> {
t.name = "coreattack";
t.touchable = Touchable.disabled;
float notifDuration = 240f;
float[] coreAttackTime = {0};
float[] coreAttackOpacity = {0};
Events.run(Trigger.teamCoreDamage, () -> {
coreAttackTime[0] = notifDuration;
});
t.top().visible(() -> {
if(!shown) return false;
if(state.isMenu() || !state.teams.get(player.team()).hasCore()){
coreAttackTime[0] = 0f;
return false;
}
t.color.a = coreAttackOpacity[0];
if(coreAttackTime[0] > 0){
coreAttackOpacity[0] = Mathf.lerpDelta(coreAttackOpacity[0], 1f, 0.1f);
}else{
coreAttackOpacity[0] = Mathf.lerpDelta(coreAttackOpacity[0], 0f, 0.1f);
}
coreAttackTime[0] -= Time.delta;
return coreAttackOpacity[0] > 0;
});
t.table(Tex.button, top -> top.add("@coreattack").pad(2)
.update(label -> label.color.set(Color.orange).lerp(Color.scarlet, Mathf.absin(Time.time, 2f, 1f)))).touchable(Touchable.disabled);
});
//'saving' indicator
parent.fill(t -> {
t.name = "saving";
@@ -326,55 +491,40 @@ public class HudFragment extends Fragment{
t.add("@saving").style(Styles.outlineLabel);
});
parent.fill(p -> {
p.name = "hudtext";
p.top().table(Styles.black3, t -> t.margin(4).label(() -> hudText)
.style(Styles.outlineLabel)).padTop(10).visible(p.color.a >= 0.001f);
p.update(() -> {
p.color.a = Mathf.lerpDelta(p.color.a, Mathf.num(showHudText), 0.2f);
if(state.isMenu()){
p.color.a = 0f;
showHudText = false;
}
});
p.touchable = Touchable.disabled;
});
//TODO DEBUG: rate table
if(false)
parent.fill(t -> {
t.name = "rates";
t.bottom().left();
t.table(Styles.black6, c -> {
Bits used = new Bits(content.items().size);
parent.fill(t -> {
t.bottom().left();
t.table(Styles.black6, c -> {
Bits used = new Bits(content.items().size);
Runnable rebuild = () -> {
c.clearChildren();
Runnable rebuild = () -> {
c.clearChildren();
for(Item item : content.items()){
if(state.rules.sector != null && state.rules.sector.info.getExport(item) >= 1){
c.image(item.icon(Cicon.small));
c.label(() -> (int)state.rules.sector.info.getExport(item) + " /s").color(Color.lightGray);
c.row();
for(Item item : content.items()){
if(state.rules.sector != null && state.rules.sector.info.getExport(item) >= 1){
c.image(item.uiIcon);
c.label(() -> (int)state.rules.sector.info.getExport(item) + " /s").color(Color.lightGray);
c.row();
}
}
}
};
};
c.update(() -> {
boolean wrong = false;
for(Item item : content.items()){
boolean has = state.rules.sector != null && state.rules.sector.info.getExport(item) >= 1;
if(used.get(item.id) != has){
used.set(item.id, has);
wrong = true;
c.update(() -> {
boolean wrong = false;
for(Item item : content.items()){
boolean has = state.rules.sector != null && state.rules.sector.info.getExport(item) >= 1;
if(used.get(item.id) != has){
used.set(item.id, has);
wrong = true;
}
}
}
if(wrong){
rebuild.run();
}
});
}).visible(() -> state.isCampaign() && content.items().contains(i -> state.rules.sector != null && state.rules.sector.info.getExport(i) > 0));
});
if(wrong){
rebuild.run();
}
});
}).visible(() -> state.isCampaign() && content.items().contains(i -> state.rules.sector != null && state.rules.sector.info.getExport(i) > 0));
});
blockfrag.build(parent);
}
@@ -407,11 +557,19 @@ public class HudFragment extends Fragment{
}
}
public boolean hasToast(){
return Time.timeSinceMillis(lastToast) < 3.5f * 1000f;
}
public void showToast(String text){
showToast(Icon.ok, text);
}
public void showToast(Drawable icon, String text){
showToast(icon, -1, text);
}
public void showToast(Drawable icon, float size, String text){
if(state.isMenu()) return;
scheduleToast(() -> {
@@ -424,7 +582,8 @@ public class HudFragment extends Fragment{
}
});
table.margin(12);
table.image(icon).pad(3);
var cell = table.image(icon).pad(3);
if(size > 0) cell.size(size);
table.add(text).wrap().width(280f).get().setAlignment(Align.center, Align.center);
table.pack();
@@ -462,7 +621,7 @@ public class HudFragment extends Fragment{
Table in = new Table();
//create texture stack for displaying
Image image = new Image(content.icon(Cicon.xlarge));
Image image = new Image(content.uiIcon);
image.setScaling(Scaling.fit);
in.add(image).size(8 * 6).pad(2);
@@ -516,7 +675,7 @@ public class HudFragment extends Fragment{
//if there's space, add it
if(esize < cap){
Image image = new Image(content.icon(Cicon.medium));
Image image = new Image(content.uiIcon);
image.setScaling(Scaling.fit);
lastUnlockLayout.add(image);
@@ -528,20 +687,19 @@ public class HudFragment extends Fragment{
}
}
public void showLaunchDirect(){
Image image = new Image();
image.color.a = 0f;
image.setFillParent(true);
image.actions(Actions.fadeIn(launchDuration / 60f, Interp.pow2In), Actions.delay(8f / 60f), Actions.remove());
Core.scene.add(image);
}
/** @deprecated see {@link CoreBuild#beginLaunch(CoreBlock)} */
@Deprecated
public void showLaunch(){
float margin = 30f;
Image image = new Image();
image.color.a = 0f;
image.touchable = Touchable.disabled;
image.setFillParent(true);
image.actions(Actions.fadeIn(40f / 60f));
image.actions(Actions.delay((coreLandDuration - margin) / 60f), Actions.fadeIn(margin / 60f, Interp.pow2In), Actions.delay(6f / 60f), Actions.remove());
image.update(() -> {
image.toFront();
ui.loadfrag.toFront();
if(state.isMenu()){
image.remove();
}
@@ -549,14 +707,17 @@ public class HudFragment extends Fragment{
Core.scene.add(image);
}
/** @deprecated see {@link CoreBuild#beginLaunch(CoreBlock)} */
@Deprecated
public void showLand(){
Image image = new Image();
image.color.a = 1f;
image.touchable = Touchable.disabled;
image.setFillParent(true);
image.actions(Actions.fadeOut(0.8f), Actions.remove());
image.actions(Actions.fadeOut(35f / 60f), Actions.remove());
image.update(() -> {
image.toFront();
ui.loadfrag.toFront();
if(state.isMenu()){
image.remove();
}
@@ -577,13 +738,14 @@ public class HudFragment extends Fragment{
StringBuilder ibuild = new StringBuilder();
IntFormat wavef = new IntFormat("wave");
IntFormat wavefc = new IntFormat("wave.cap");
IntFormat enemyf = new IntFormat("wave.enemy");
IntFormat enemiesf = new IntFormat("wave.enemies");
IntFormat enemycf = new IntFormat("wave.enemycore");
IntFormat enemycsf = new IntFormat("wave.enemycores");
IntFormat waitingf = new IntFormat("wave.waiting", i -> {
IntFormat
wavef = new IntFormat("wave"),
wavefc = new IntFormat("wave.cap"),
enemyf = new IntFormat("wave.enemy"),
enemiesf = new IntFormat("wave.enemies"),
enemycf = new IntFormat("wave.enemycore"),
enemycsf = new IntFormat("wave.enemycores"),
waitingf = new IntFormat("wave.waiting", i -> {
ibuild.setLength(0);
int m = i/60;
int s = i % 60;
@@ -637,20 +799,14 @@ public class HudFragment extends Fragment{
if(Float.isNaN(value) || Float.isInfinite(value)) value = 1f;
drawInner(Pal.darkishGray);
Draw.beginStencil();
Fill.crect(x, y, width, height * value);
Draw.beginStenciled();
drawInner(Tmp.c1.set(color).lerp(Color.white, blink));
Draw.endStencil();
drawInner(Pal.darkishGray, 1f);
drawInner(Tmp.c1.set(color).lerp(Color.white, blink), value);
}
void drawInner(Color color){
void drawInner(Color color, float fract){
if(fract < 0) return;
fract = Mathf.clamp(fract);
if(flip){
x += width;
width = -width;
@@ -658,21 +814,28 @@ public class HudFragment extends Fragment{
float stroke = width * 0.35f;
float bh = height/2f;
Draw.color(color);
Draw.color(color, parentAlpha);
float f1 = Math.min(fract * 2f, 1f), f2 = (fract - 0.5f) * 2f;
float bo = -(1f - f1) * (width - stroke);
Fill.quad(
x, y,
x + stroke, y,
x + width, y + bh,
x + width - stroke, y + bh
x + width + bo, y + bh * f1,
x + width - stroke + bo, y + bh * f1
);
Fill.quad(
x + width, y + bh,
x + width - stroke, y + bh,
x, y + height,
x + stroke, y + height
);
if(f2 > 0){
float bx = x + (width - stroke) * (1f - f2);
Fill.quad(
x + width, y + bh,
x + width - stroke, y + bh,
bx, y + height * fract,
bx + stroke, y + height * fract
);
}
Draw.reset();
@@ -687,10 +850,10 @@ public class HudFragment extends Fragment{
new Element(){
@Override
public void draw(){
Draw.color(Pal.darkerGray);
Draw.color(Pal.darkerGray, parentAlpha);
Fill.poly(x + width/2f, y + height/2f, 6, height / Mathf.sqrt3);
Draw.reset();
Drawf.shadow(x + width/2f, y + height/2f, height * 1.13f);
Drawf.shadow(x + width/2f, y + height/2f, height * 1.13f, parentAlpha);
}
},
new Table(t -> {
@@ -700,22 +863,67 @@ public class HudFragment extends Fragment{
t.clicked(() -> {
if(!player.dead() && mobile){
Call.unitClear(player);
control.input.recentRespawnTimer = 1f;
control.input.controlledType = null;
}
});
t.add(new SideBar(() -> player.unit().healthf(), () -> true, true)).width(bw).growY().padRight(pad);
t.add(new SideBar(() -> player.dead() ? 0f : player.unit().healthf(), () -> true, true)).width(bw).growY().padRight(pad);
t.image(() -> player.icon()).scaling(Scaling.bounded).grow().maxWidth(54f);
t.add(new SideBar(() -> player.dead() ? 0f : player.displayAmmo() ? player.unit().ammof() : player.unit().healthf(), () -> !player.displayAmmo(), false)).width(bw).growY().padLeft(pad).update(b -> {
b.color.set(player.displayAmmo() ? player.dead() || player.unit() instanceof BlockUnitc ? Pal.ammo : player.unit().type.ammoType.color : Pal.health);
b.color.set(player.displayAmmo() ? player.dead() || player.unit() instanceof BlockUnitc ? Pal.ammo : player.unit().type.ammoType.color() : Pal.health);
});
t.getChildren().get(1).toFront();
})).size(120f, 80).padRight(4);
table.labelWrap(() -> {
Cell[] lcell = {null};
boolean[] couldSkip = {true};
lcell[0] = table.labelWrap(() -> {
//update padding depend on whether the button to the right is there
boolean can = canSkipWave();
if(can != couldSkip[0]){
if(canSkipWave()){
lcell[0].padRight(8f);
}else{
lcell[0].padRight(-42f);
}
table.invalidateHierarchy();
table.pack();
couldSkip[0] = can;
}
builder.setLength(0);
//mission overrides everything
if(state.rules.mission != null && state.rules.mission.length() > 0){
builder.append(state.rules.mission);
return builder;
}
//objectives override mission?
if(state.rules.objectives.any()){
boolean first = true;
for(var obj : state.rules.objectives){
if(!obj.qualified() || obj.hidden) continue;
String text = obj.text();
if(text != null && !text.isEmpty()){
if(!first) builder.append("\n[white]");
builder.append(text);
first = false;
}
}
//TODO: display standard status when empty objective?
if(builder.length() > 0){
return builder;
}
}
if(!state.rules.waves && state.rules.attackMode){
int sum = Math.max(state.teams.present.sum(t -> t.team != player.team() ? t.cores.size : 0), 1);
builder.append(sum > 1 ? enemycsf.get(sum) : enemycf.get(sum));
@@ -730,7 +938,7 @@ public class HudFragment extends Fragment{
return builder;
}
if(state.rules.winWave > 1 && state.rules.winWave >= state.wave && state.isCampaign()){
if(state.rules.winWave > 1 && state.rules.winWave >= state.wave){
builder.append(wavefc.get(state.wave, state.rules.winWave));
}else{
builder.append(wavef.get(state.wave));
@@ -757,6 +965,37 @@ public class HudFragment extends Fragment{
table.row();
//TODO nobody reads details anyway.
/*
table.clicked(() -> {
if(state.rules.objectives.any()){
StringBuilder text = new StringBuilder();
boolean first = true;
for(var obj : state.rules.objectives){
if(!obj.qualified()) continue;
String details = obj.details();
if(details != null){
if(!first) text.append('\n');
text.append(details);
first = false;
}
}
//TODO this, as said before, could be much better.
ui.showInfo(text.toString());
}
});*/
return table;
}
private void addInfoTable(Table table){
table.name = "infotable";
table.left();
var count = new float[]{-1};
table.table().update(t -> {
if(player.unit() instanceof Payloadc payload){
@@ -765,15 +1004,39 @@ public class HudFragment extends Fragment{
count[0] = payload.payloadUsed();
}
}else{
count[0] = -1;
t.clear();
}
}).growX().visible(() -> player.unit() instanceof Payloadc p && p.payloadUsed() > 0).colspan(2);
table.row();
return table;
Bits statuses = new Bits();
table.table().update(t -> {
t.left();
Bits applied = player.dead() ? null : player.unit().statusBits();
if(!statuses.equals(applied)){
t.clear();
if(applied != null){
for(StatusEffect effect : content.statusEffects()){
if(applied.get(effect.id) && !effect.isHidden()){
t.image(effect.uiIcon).size(iconMed).get()
.addListener(new Tooltip(l -> l.label(() ->
effect.localizedName + " [lightgray]" + UI.formatTime(player.unit().getDuration(effect))).style(Styles.outlineLabel)));
}
}
statuses.set(applied);
}else{
statuses.clear();
}
}
}).left();
}
private boolean canSkipWave(){
return state.rules.waves && ((net.server() || player.admin) || !net.active()) && state.enemies == 0 && !spawner.isSpawning();
return state.rules.waves && state.rules.waveSending && ((net.server() || player.admin) || !net.active()) && state.enemies == 0 && !spawner.isSpawning();
}
}

View File

@@ -12,13 +12,13 @@ import arc.scene.ui.layout.*;
import mindustry.graphics.*;
import mindustry.ui.*;
public class LoadingFragment extends Fragment{
public class LoadingFragment{
private Table table;
private TextButton button;
private Bar bar;
private Label nameLabel;
private float progValue;
@Override
public void build(Group parent){
parent.fill(t -> {
//rect must fill screen completely.
@@ -38,19 +38,34 @@ public class LoadingFragment extends Fragment{
text("@loading");
bar = t.add(new Bar()).pad(3).size(500f, 40f).visible(false).get();
bar = t.add(new Bar()).pad(3).padTop(6).size(500f, 40f).visible(false).get();
t.row();
button = t.button("@cancel", () -> {}).pad(20).size(250f, 70f).visible(false).get();
table = t;
});
}
public void toFront(){
table.toFront();
}
public void setProgress(Floatp progress){
bar.reset(0f);
bar.visible = true;
bar.set(() -> ((int)(progress.get() * 100) + "%"), progress, Pal.accent);
}
public void snapProgress(){
bar.snap();
}
public void setProgress(float progress){
progValue = progress;
if(!bar.visible){
setProgress(() -> progValue);
}
}
public void setButton(Runnable listener){
button.visible = true;
button.getListeners().remove(button.getListeners().size - 1);
@@ -67,6 +82,7 @@ public class LoadingFragment extends Fragment{
}
public void show(String text){
button.visible = false;
nameLabel.setColor(Color.white);
bar.visible = false;
table.clearActions();

View File

@@ -9,7 +9,10 @@ import arc.scene.actions.*;
import arc.scene.event.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.ImageButton.*;
import arc.scene.ui.TextButton.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.core.*;
import mindustry.game.EventType.*;
@@ -18,19 +21,15 @@ import mindustry.graphics.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
import static mindustry.gen.Tex.*;
public class MenuFragment extends Fragment{
public class MenuFragment{
private Table container, submenu;
private Button currentMenu;
private MenuRenderer renderer;
private Seq<MenuButton> customButtons = new Seq<>();
public Seq<MenuButton> desktopButtons = null;
public MenuFragment(){
Events.on(DisposeEvent.class, event -> {
renderer.dispose();
});
}
@Override
public void build(Group parent){
renderer = new MenuRenderer();
@@ -44,22 +43,40 @@ public class MenuFragment extends Fragment{
parent.fill((x, y, w, h) -> renderer.render());
parent.fill(c -> {
container = c;
c.name = "menu container";
c.pane(Styles.noBarPane, cont -> {
container = cont;
cont.name = "menu container";
if(!mobile){
buildDesktop();
Events.on(ResizeEvent.class, event -> buildDesktop());
}else{
buildMobile();
Events.on(ResizeEvent.class, event -> buildMobile());
}
if(!mobile){
c.left();
buildDesktop();
Events.on(ResizeEvent.class, event -> buildDesktop());
}else{
buildMobile();
Events.on(ResizeEvent.class, event -> buildMobile());
}
}).with(pane -> {
pane.setOverscroll(false, false);
}).grow();
});
parent.fill(c -> c.bottom().right().button(Icon.discord, new ImageButtonStyle(){{
up = discordBanner;
}}, ui.discord::show).marginTop(9f).marginLeft(10f).tooltip("@discord").size(84, 45).name("discord"));
//info icon
if(mobile){
parent.fill(c -> c.bottom().left().button("", Styles.infot, ui.about::show).size(84, 45).name("info"));
parent.fill(c -> c.bottom().right().button("", Styles.discordt, ui.discord::show).size(84, 45).name("discord"));
parent.fill(c -> c.bottom().left().button("", new TextButtonStyle(){{
font = Fonts.def;
fontColor = Color.white;
up = infoBanner;
}}, ui.about::show).size(84, 45).name("info"));
parent.fill((x, y, w, h) -> {
if(Core.scene.marginBottom > 0){
Tex.paneTop.draw(0, 0, Core.graphics.getWidth(), Core.scene.marginBottom);
}
});
}else if(becontrol.active()){
parent.fill(c -> c.bottom().right().button("@be.check", Icon.refresh, () -> {
ui.loadfrag.show();
@@ -78,18 +95,21 @@ public class MenuFragment extends Fragment{
parent.fill((x, y, w, h) -> {
TextureRegion logo = Core.atlas.find("logo");
float width = Core.graphics.getWidth(), height = Core.graphics.getHeight() - Core.scene.marginTop;
float logoscl = Scl.scl(1);
float logoscl = Scl.scl(1) * logo.scale;
float logow = Math.min(logo.width * logoscl, Core.graphics.getWidth() - Scl.scl(20));
float logoh = logow * (float)logo.height / logo.width;
float fx = (int)(width / 2f);
float fy = (int)(height - 6 - logoh) + logoh / 2 - (Core.graphics.isPortrait() ? Scl.scl(30f) : 0f);
if(Core.settings.getBool("macnotch") ){
fy -= Scl.scl(macNotchHeight);
}
Draw.color();
Draw.rect(logo, fx, fy, logow, logoh);
Fonts.def.setColor(Color.white);
Fonts.def.draw(versionText, fx, fy - logoh/2f, Align.center);
Fonts.outline.setColor(Color.white);
Fonts.outline.draw(versionText, fx, fy - logoh/2f - Scl.scl(2f), Align.center);
}).touchable = Touchable.disabled;
}
@@ -109,7 +129,10 @@ public class MenuFragment extends Fragment{
editor = new MobileButton(Icon.terrain, "@editor", () -> checkPlay(ui.maps::show)),
tools = new MobileButton(Icon.settings, "@settings", ui.settings::show),
mods = new MobileButton(Icon.book, "@mods", ui.mods::show),
exit = new MobileButton(Icon.exit, "@quit", () -> Core.app.exit());
exit = new MobileButton(Icon.exit, "@quit", () -> Core.app.exit()),
about = new MobileButton(Icon.info, "@about.button", ui.about::show);
Seq<MobileButton> customs = customButtons.map(b -> new MobileButton(b.icon, b.text, b.runnable == null ? () -> {} : b.runnable));
if(!Core.graphics.isPortrait()){
container.marginTop(60f);
@@ -117,18 +140,20 @@ public class MenuFragment extends Fragment{
container.add(join);
container.add(custom);
container.add(maps);
// add odd custom buttons
for(int i = 1; i < customs.size; i += 2){
container.add(customs.get(i));
}
container.row();
container.table(table -> {
table.defaults().set(container.defaults());
table.add(editor);
table.add(tools);
table.add(mods);
//if(platform.canDonate()) table.add(donate);
if(!ios) table.add(exit);
}).colspan(4);
container.add(editor);
container.add(tools);
container.add(mods);
// add even custom buttons (before the exit button)
for(int i = 0; i < customs.size; i += 2){
container.add(customs.get(i));
}
container.add(ios ? about : exit);
}else{
container.marginTop(0f);
container.add(play);
@@ -140,14 +165,13 @@ public class MenuFragment extends Fragment{
container.add(editor);
container.add(tools);
container.row();
container.table(table -> {
table.defaults().set(container.defaults());
table.add(mods);
//if(platform.canDonate()) table.add(donate);
if(!ios) table.add(exit);
}).colspan(2);
container.add(mods);
// add custom buttons
for(int i = 0; i < customs.size; i++){
container.add(customs.get(i));
if(i % 2 == 0) container.row();
}
container.add(ios ? about : exit);
}
}
@@ -164,22 +188,28 @@ public class MenuFragment extends Fragment{
t.defaults().width(width).height(70f);
t.name = "buttons";
buttons(t,
new Buttoni("@play", Icon.play,
new Buttoni("@campaign", Icon.play, () -> checkPlay(ui.planet::show)),
new Buttoni("@joingame", Icon.add, () -> checkPlay(ui.join::show)),
new Buttoni("@customgame", Icon.terrain, () -> checkPlay(ui.custom::show)),
new Buttoni("@loadgame", Icon.download, () -> checkPlay(ui.load::show))
),
new Buttoni("@editor", Icon.terrain, () -> checkPlay(ui.maps::show)), steam ? new Buttoni("@workshop", Icon.book, platform::openWorkshop) : null,
new Buttoni("@mods", Icon.book, ui.mods::show),
//not enough space for this button
//new Buttoni("@schematics", Icon.paste, ui.schematics::show),
new Buttoni("@settings", Icon.settings, ui.settings::show),
new Buttoni("@about.button", Icon.info, ui.about::show),
new Buttoni("@quit", Icon.exit, Core.app::exit)
);
if(desktopButtons == null){
desktopButtons = Seq.with(
new MenuButton("@play", Icon.play,
new MenuButton("@campaign", Icon.play, () -> checkPlay(ui.planet::show)),
new MenuButton("@joingame", Icon.add, () -> checkPlay(ui.join::show)),
new MenuButton("@customgame", Icon.terrain, () -> checkPlay(ui.custom::show)),
new MenuButton("@loadgame", Icon.download, () -> checkPlay(ui.load::show))
),
new MenuButton("@database.button", Icon.menu,
new MenuButton("@schematics", Icon.paste, ui.schematics::show),
new MenuButton("@database", Icon.book, ui.database::show),
new MenuButton("@about.button", Icon.info, ui.about::show)
),
new MenuButton("@editor", Icon.terrain, () -> checkPlay(ui.maps::show)), steam ? new MenuButton("@workshop", Icon.steam, platform::openWorkshop) : null,
new MenuButton("@mods", Icon.book, ui.mods::show),
new MenuButton("@settings", Icon.settings, ui.settings::show)
);
}
buttons(t, desktopButtons.toArray(MenuButton.class));
buttons(t, customButtons.toArray(MenuButton.class));
buttons(t, new MenuButton("@quit", Icon.exit, Core.app::exit));
}).width(width).growY();
container.table(background, t -> {
@@ -194,7 +224,6 @@ public class MenuFragment extends Fragment{
}
private void checkPlay(Runnable run){
if(!mods.hasContentErrors()){
run.run();
}else{
@@ -217,23 +246,23 @@ public class MenuFragment extends Fragment{
submenu.actions(Actions.alpha(1f), Actions.alpha(0f, 0.2f, Interp.fade), Actions.run(() -> submenu.clearChildren()));
}
private void buttons(Table t, Buttoni... buttons){
for(Buttoni b : buttons){
private void buttons(Table t, MenuButton... buttons){
for(MenuButton b : buttons){
if(b == null) continue;
Button[] out = {null};
out[0] = t.button(b.text, b.icon, Styles.clearToggleMenut, () -> {
out[0] = t.button(b.text, b.icon, Styles.flatToggleMenut, () -> {
if(currentMenu == out[0]){
currentMenu = null;
fadeOutMenu();
}else{
if(b.submenu != null){
if(b.submenu != null && b.submenu.any()){
currentMenu = out[0];
submenu.clearChildren();
fadeInMenu();
//correctly offset the button
submenu.add().height((Core.graphics.getHeight() - Core.scene.marginTop - Core.scene.marginBottom - out[0].getY(Align.topLeft)) / Scl.scl(1f));
submenu.row();
buttons(submenu, b.submenu);
buttons(submenu, b.submenu.toArray());
}else{
currentMenu = null;
fadeOutMenu();
@@ -246,24 +275,56 @@ public class MenuFragment extends Fragment{
}
}
private static class Buttoni{
final Drawable icon;
final String text;
final Runnable runnable;
final Buttoni[] submenu;
/** Adds a custom button to the menu. */
public void addButton(String text, Drawable icon, Runnable callback){
addButton(new MenuButton(text, icon, callback));
}
public Buttoni(String text, Drawable icon, Runnable runnable){
/** Adds a custom button to the menu. */
public void addButton(String text, Runnable callback){
addButton(text, Styles.none, callback);
}
/**
* Adds a custom button to the menu.
* If {@link MenuButton#submenu} is null or the player is on mobile, {@link MenuButton#runnable} is invoked on click.
* Otherwise, {@link MenuButton#submenu} is shown.
*/
public void addButton(MenuButton button){
customButtons.add(button);
}
/** Represents a menu button definition. */
public static class MenuButton{
public final Drawable icon;
public final String text;
/** Runnable ran when the button is clicked. Ignored on desktop if {@link #submenu} is not null. */
public final Runnable runnable;
/** Submenu shown when this button is clicked. Used instead of {@link #runnable} on desktop. */
public final @Nullable Seq<MenuButton> submenu;
/** Constructs a simple menu button, which behaves the same way on desktop and mobile. */
public MenuButton(String text, Drawable icon, Runnable runnable){
this.icon = icon;
this.text = text;
this.runnable = runnable;
this.submenu = null;
}
public Buttoni(String text, Drawable icon, Buttoni... buttons){
/** Constructs a button that runs the runnable when clicked on mobile or shows the submenu on desktop. */
public MenuButton(String text, Drawable icon, Runnable runnable, MenuButton... submenu){
this.icon = icon;
this.text = text;
this.runnable = runnable;
this.submenu = submenu != null ? Seq.with(submenu) : null;
}
/** Comstructs a desktop-only button; used internally. */
MenuButton(String text, Drawable icon, MenuButton... submenu){
this.icon = icon;
this.text = text;
this.runnable = () -> {};
this.submenu = buttons;
this.submenu = submenu != null ? Seq.with(submenu) : null;
}
}
}

View File

@@ -5,22 +5,33 @@ import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.input.*;
import arc.math.*;
import arc.math.geom.*;
import arc.scene.*;
import arc.scene.event.*;
import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.gen.*;
import mindustry.input.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class MinimapFragment extends Fragment{
public class MinimapFragment{
private boolean shown;
float panx, pany, zoom = 1f, lastZoom = -1;
private float baseSize = Scl.scl(5f);
public Element elem;
@Override
protected Rect getRectBounds(){
float
w = Core.graphics.getWidth(),
h = Core.graphics.getHeight(),
ratio = renderer.minimap.getTexture() == null ? 1f : (float)renderer.minimap.getTexture().height / renderer.minimap.getTexture().width,
size = baseSize * zoom * world.width();
return Tmp.r1.set(w/2f + panx*zoom - size/2f, h/2f + pany*zoom - size/2f * ratio, size, size * ratio);
}
public void build(Group parent){
elem = parent.fill((x, y, w, h) -> {
w = Core.graphics.getWidth();
@@ -35,7 +46,9 @@ public class MinimapFragment extends Fragment{
float ratio = (float)renderer.minimap.getTexture().height / renderer.minimap.getTexture().width;
TextureRegion reg = Draw.wrap(renderer.minimap.getTexture());
Draw.rect(reg, w/2f + panx*zoom, h/2f + pany*zoom, size, size * ratio);
renderer.minimap.drawEntities(w/2f + panx*zoom - size/2f, h/2f + pany*zoom - size/2f * ratio, size, size * ratio, zoom, true);
Rect bounds = getRectBounds();
renderer.minimap.drawEntities(bounds.x, bounds.y, bounds.width, bounds.height, zoom, true);
}
Draw.reset();
@@ -69,13 +82,20 @@ public class MinimapFragment extends Fragment{
@Override
public void pan(InputEvent event, float x, float y, float deltaX, float deltaY){
panx += deltaX / zoom;
pany += deltaY / zoom;
if(event.keyCode != KeyCode.mouseRight){
panx += deltaX / zoom;
pany += deltaY / zoom;
}else{
panTo(x, y);
}
}
@Override
public void touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
super.touchDown(event, x, y, pointer, button);
if(button == KeyCode.mouseRight){
panTo(x, y);
}
}
@Override
@@ -106,6 +126,12 @@ public class MinimapFragment extends Fragment{
});
}
public void panTo(float relativeX, float relativeY){
Rect r = getRectBounds();
Tmp.v1.set(relativeX, relativeY).sub(r.x, r.y).scl(1f / r.width, 1f / r.height).scl(world.unitWidth(), world.unitHeight());
control.input.panCamera(Tmp.v1.clamp(-tilesize/2f, -tilesize/2f, world.unitWidth() + tilesize/2f, world.unitHeight() + tilesize/2f));
}
public boolean shown(){
return shown;
}
@@ -115,11 +141,14 @@ public class MinimapFragment extends Fragment{
}
public void toggle(){
float size = baseSize * zoom * world.width();
float ratio = (float)renderer.minimap.getTexture().height / renderer.minimap.getTexture().width;
float px = player.dead() ? Core.camera.position.x : player.x, py = player.dead() ? Core.camera.position.y : player.y;
panx = (size/2f - px / (world.width() * tilesize) * size) / zoom;
pany = (size*ratio/2f - py / (world.height() * tilesize) * size*ratio) / zoom;
if(renderer.minimap.getTexture() != null){
float size = baseSize * zoom * world.width();
float ratio = (float)renderer.minimap.getTexture().height / renderer.minimap.getTexture().width;
float px = player.dead() ? Core.camera.position.x : player.x, py = player.dead() ? Core.camera.position.y : player.y;
panx = (size/2f - px / (world.width() * tilesize) * size) / zoom;
pany = (size*ratio/2f - py / (world.height() * tilesize) * size*ratio) / zoom;
}
shown = !shown;
}
}

View File

@@ -1,32 +0,0 @@
package mindustry.ui.fragments;
import arc.*;
import arc.scene.event.*;
import arc.scene.ui.layout.*;
import mindustry.*;
/** Fragment for displaying overlays such as block inventories. */
public class OverlayFragment{
public final BlockInventoryFragment inv;
public final BlockConfigFragment config;
private WidgetGroup group = new WidgetGroup();
public OverlayFragment(){
group.touchable = Touchable.childrenOnly;
inv = new BlockInventoryFragment();
config = new BlockConfigFragment();
}
public void add(){
group.setFillParent(true);
Vars.ui.hudGroup.addChildBefore(Core.scene.find("overlaymarker"), group);
inv.build(group);
config.build(group);
}
public void remove(){
group.remove();
}
}

View File

@@ -8,25 +8,29 @@ import arc.scene.*;
import arc.scene.event.*;
import arc.scene.style.*;
import arc.scene.ui.*;
import arc.scene.ui.Tooltip.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ai.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.entities.*;
import mindustry.entities.units.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.input.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.ConstructBlock.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
public class PlacementFragment extends Fragment{
public class PlacementFragment{
final int rowWidth = 4;
public Category currentCategory = Category.distribution;
@@ -36,13 +40,17 @@ public class PlacementFragment extends Fragment{
boolean[] categoryEmpty = new boolean[Category.all.length];
ObjectMap<Category,Block> selectedBlocks = new ObjectMap<>();
ObjectFloatMap<Category> scrollPositions = new ObjectFloatMap<>();
Block menuHoverBlock;
Displayable hover;
Object lastDisplayState;
@Nullable Block menuHoverBlock;
@Nullable Displayable hover;
@Nullable Building lastFlowBuild, nextFlowBuild;
@Nullable Object lastDisplayState;
@Nullable Team lastTeam;
boolean wasHovered;
Table blockTable, toggler, topTable;
Table blockTable, toggler, topTable, blockCatTable, commandTable;
Stack mainStack;
ScrollPane blockPane;
boolean blockSelectEnd;
Runnable rebuildCommand;
boolean blockSelectEnd, wasCommandMode;
int blockSelectSeq;
long blockSelectSeqMillis;
Binding[] blockSelect = {
@@ -65,11 +73,18 @@ public class PlacementFragment extends Fragment{
public PlacementFragment(){
Events.on(WorldLoadEvent.class, event -> {
Core.app.post(() -> {
currentCategory = Category.distribution;
control.input.block = null;
rebuild();
});
});
Events.run(Trigger.unitCommandChange, () -> {
if(rebuildCommand != null){
rebuildCommand.run();
}
});
Events.on(UnlockEvent.class, event -> {
if(event.content instanceof Block){
rebuild();
@@ -79,14 +94,29 @@ public class PlacementFragment extends Fragment{
Events.on(ResetEvent.class, event -> {
selectedBlocks.clear();
});
Events.run(Trigger.update, () -> {
//disable flow updating on previous building so it doesn't waste CPU
if(lastFlowBuild != null && lastFlowBuild != nextFlowBuild){
if(lastFlowBuild.flowItems() != null) lastFlowBuild.flowItems().stopFlow();
if(lastFlowBuild.liquids != null) lastFlowBuild.liquids.stopFlow();
}
lastFlowBuild = nextFlowBuild;
if(nextFlowBuild != null){
if(nextFlowBuild.flowItems() != null) nextFlowBuild.flowItems().updateFlow();
if(nextFlowBuild.liquids != null) nextFlowBuild.liquids.updateFlow();
}
});
}
public Displayable hover(){
return hover;
}
void rebuild(){
currentCategory = Category.turret;
public void rebuild(){
//category does not change on rebuild anymore, only on new world load
Group group = toggler.parent;
int index = toggler.getZIndex();
toggler.remove();
@@ -94,13 +124,17 @@ public class PlacementFragment extends Fragment{
toggler.setZIndex(index);
}
boolean gridUpdate(InputHandler input){
scrollPositions.put(currentCategory, blockPane.getScrollY());
boolean updatePick(InputHandler input){
if(Core.input.keyTap(Binding.pick) && player.isBuilder() && !Core.scene.hasDialog()){ //mouse eyedropper select
var build = world.buildWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y);
if(Core.input.keyTap(Binding.pick) && player.isBuilder()){ //mouse eyedropper select
Building tile = world.buildWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y);
Block tryRecipe = tile == null ? null : tile.block instanceof ConstructBlock ? ((ConstructBuild)tile).cblock : tile.block;
Object tryConfig = tile == null ? null : tile.config();
//can't middle click buildings in fog
if(build != null && build.inFogTo(player.team())){
build = null;
}
Block tryRecipe = build == null ? null : build instanceof ConstructBuild c ? c.current : build.block;
Object tryConfig = build == null || !build.block.copyConfig ? null : build.config();
for(BuildPlan req : player.unit().plans()){
if(!req.breaking && req.block.bounds(req.x, req.y, Tmp.r1).contains(Core.input.mouseWorld())){
@@ -110,15 +144,36 @@ public class PlacementFragment extends Fragment{
}
}
if(tryRecipe != null && tryRecipe.isVisible() && unlocked(tryRecipe)){
if(tryRecipe == null && state.rules.editor){
var tile = world.tileWorld(Core.input.mouseWorldX(), Core.input.mouseWorldY());
if(tile != null){
tryRecipe =
tile.block() != Blocks.air ? tile.block() :
tile.overlay() != Blocks.air ? tile.overlay() :
tile.floor() != Blocks.air ? tile.floor() : null;
}
}
if(tryRecipe != null && ((tryRecipe.isVisible() && unlocked(tryRecipe)) || state.rules.editor)){
input.block = tryRecipe;
tryRecipe.lastConfig = tryConfig;
currentCategory = input.block.category;
if(tryRecipe.isVisible()){
currentCategory = input.block.category;
}
return true;
}
}
return false;
}
if(ui.chatfrag.shown() || Core.scene.hasKeyboard()) return false;
boolean gridUpdate(InputHandler input){
scrollPositions.put(currentCategory, blockPane.getScrollY());
if(updatePick(input)){
return true;
}
if(ui.chatfrag.shown() || ui.consolefrag.shown() || Core.scene.hasKeyboard()) return false;
for(int i = 0; i < blockSelect.length; i++){
if(Core.input.keyTap(blockSelect[i])){
@@ -175,25 +230,38 @@ public class PlacementFragment extends Fragment{
}
if(Core.input.keyTap(Binding.category_prev)){
int i = 0;
do{
currentCategory = currentCategory.prev();
}while(categoryEmpty[currentCategory.ordinal()]);
i ++;
}while(categoryEmpty[currentCategory.ordinal()] && i < categoryEmpty.length);
input.block = getSelectedBlock(currentCategory);
return true;
}
if(Core.input.keyTap(Binding.category_next)){
int i = 0;
do{
currentCategory = currentCategory.next();
}while(categoryEmpty[currentCategory.ordinal()]);
i ++;
}while(categoryEmpty[currentCategory.ordinal()] && i < categoryEmpty.length);
input.block = getSelectedBlock(currentCategory);
return true;
}
if(Core.input.keyTap(Binding.block_info)){
var build = world.buildWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y);
Block hovering = build == null ? null : build instanceof ConstructBuild c ? c.current : build.block;
Block displayBlock = menuHoverBlock != null ? menuHoverBlock : input.block != null ? input.block : hovering;
if(displayBlock != null && displayBlock.unlockedNow()){
ui.content.show(displayBlock);
Events.fire(new BlockInfoEvent());
}
}
return false;
}
@Override
public void build(Group parent){
parent.fill(full -> {
toggler = full;
@@ -217,9 +285,9 @@ public class PlacementFragment extends Fragment{
blockTable.row();
}
ImageButton button = blockTable.button(new TextureRegionDrawable(block.icon(Cicon.medium)), Styles.selecti, () -> {
ImageButton button = blockTable.button(new TextureRegionDrawable(block.uiIcon), Styles.selecti, () -> {
if(unlocked(block)){
if(Core.input.keyDown(KeyCode.shiftLeft) && Fonts.getUnicode(block.name) != 0){
if((Core.input.keyDown(KeyCode.shiftLeft) || Core.input.keyDown(KeyCode.controlLeft)) && Fonts.getUnicode(block.name) != 0){
Core.app.setClipboardText((char)Fonts.getUnicode(block.name) + "");
ui.showInfoFade("@copied");
}else{
@@ -228,7 +296,7 @@ public class PlacementFragment extends Fragment{
}
}
}).size(46f).group(group).name("block-" + block.name).get();
button.resizeImage(Cicon.medium.size);
button.resizeImage(iconMed);
button.update(() -> { //color unplacable things gray
Building core = player.core();
@@ -276,13 +344,14 @@ public class PlacementFragment extends Fragment{
//don't refresh unnecessarily
//refresh only when the hover state changes, or the displayed block changes
if(wasHovered == isHovered && lastDisplayState == displayState) return;
if(wasHovered == isHovered && lastDisplayState == displayState && lastTeam == player.team()) return;
topTable.clear();
topTable.top().left().margin(5);
lastDisplayState = displayState;
wasHovered = isHovered;
lastTeam = player.team();
//show details of selected block, with costs
if(displayBlock != null){
@@ -302,12 +371,12 @@ public class PlacementFragment extends Fragment{
}
final String keyComboFinal = keyCombo;
header.left();
header.add(new Image(displayBlock.icon(Cicon.medium))).size(8 * 4);
header.add(new Image(displayBlock.uiIcon)).size(8 * 4);
header.labelWrap(() -> !unlocked(displayBlock) ? Core.bundle.get("block.unknown") : displayBlock.localizedName + keyComboFinal)
.left().width(190f).padLeft(5);
header.add().growX();
if(unlocked(displayBlock)){
header.button("?", Styles.clearPartialt, () -> {
header.button("?", Styles.flatBordert, () -> {
ui.content.show(displayBlock);
Events.fire(new BlockInfoEvent());
}).size(8 * 5).padTop(-5).padRight(-5).right().grow().name("blockinfo");
@@ -321,15 +390,15 @@ public class PlacementFragment extends Fragment{
for(ItemStack stack : displayBlock.requirements){
req.table(line -> {
line.left();
line.image(stack.item.icon(Cicon.small)).size(8 * 2);
line.image(stack.item.uiIcon).size(8 * 2);
line.add(stack.item.localizedName).maxWidth(140f).fillX().color(Color.lightGray).padLeft(2).left().get().setEllipsis(true);
line.labelWrap(() -> {
Building core = player.core();
if(core == null || state.rules.infiniteResources) return "*/*";
int stackamount = Math.round(stack.amount * state.rules.buildCostMultiplier);
if(core == null || state.rules.infiniteResources) return "*/" + stackamount;
int amount = core.items.get(stack.item);
int stackamount = Math.round(stack.amount * state.rules.buildCostMultiplier);
String color = (amount < stackamount / 2f ? "[red]" : amount < stackamount ? "[accent]" : "[white]");
String color = (amount < stackamount / 2f ? "[scarlet]" : amount < stackamount ? "[accent]" : "[white]");
return color + UI.formatAmount(amount) + "[white]/" + stackamount;
}).padLeft(5);
@@ -338,11 +407,11 @@ public class PlacementFragment extends Fragment{
}
}).growX().left().margin(3);
if(!displayBlock.isPlaceable() || !player.isBuilder()){
if((!displayBlock.isPlaceable() || !player.isBuilder()) && !state.rules.editor){
topTable.row();
topTable.table(b -> {
b.image(Icon.cancel).padRight(2).color(Color.scarlet);
b.add(!player.isBuilder() ? "@unit.nobuild" : "@banned").width(190f).wrap();
b.add(!player.isBuilder() ? "@unit.nobuild" : !displayBlock.supportsEnv(state.rules.env) ? "@unsupported.environment" : "@banned").width(190f).wrap();
b.left();
}).padTop(2).left();
}
@@ -352,65 +421,295 @@ public class PlacementFragment extends Fragment{
hovered.display(topTable);
}
});
}).colspan(3).fillX().visible(this::hasInfoBox).touchable(Touchable.enabled);
frame.row();
frame.image().color(Pal.gray).colspan(3).height(4).growX();
frame.row();
frame.table(Tex.pane2, blocksSelect -> {
blocksSelect.margin(4).marginTop(0);
blockPane = blocksSelect.pane(blocks -> blockTable = blocks).height(194f).update(pane -> {
if(pane.hasScroll()){
Element result = Core.scene.hit(Core.input.mouseX(), Core.input.mouseY(), true);
if(result == null || !result.isDescendantOf(pane)){
Core.scene.setScrollFocus(null);
}
}
}).grow().get();
blockPane.setStyle(Styles.smallPane);
blocksSelect.row();
blocksSelect.table(control.input::buildPlacementUI).name("inputTable").growX();
}).fillY().bottom().touchable(Touchable.enabled);
frame.table(categories -> {
categories.bottom();
categories.add(new Image(Styles.black6){
@Override
public void draw(){
if(height <= Scl.scl(3f)) return;
getDrawable().draw(x, y, width, height - Scl.scl(3f));
}
}).colspan(2).growX().growY().padTop(-3f).row();
categories.defaults().size(50f);
}).colspan(3).fillX().visible(this::hasInfoBox).touchable(Touchable.enabled).row();
ButtonGroup<ImageButton> group = new ButtonGroup<>();
frame.image().color(Pal.gray).colspan(3).height(4).growX().row();
//update category empty values
for(Category cat : Category.all){
Seq<Block> blocks = getUnlockedByCategory(cat);
categoryEmpty[cat.ordinal()] = blocks.isEmpty();
blockCatTable = new Table();
commandTable = new Table(Tex.pane2);
mainStack = new Stack();
mainStack.update(() -> {
if(control.input.commandMode != wasCommandMode){
mainStack.clearChildren();
mainStack.addChild(control.input.commandMode ? commandTable : blockCatTable);
//hacky, but forces command table to be same width as blocks
if(control.input.commandMode){
commandTable.getCells().peek().width(blockCatTable.getWidth() / Scl.scl(1f));
}
wasCommandMode = control.input.commandMode;
}
});
int f = 0;
for(Category cat : getCategories()){
if(f++ % 2 == 0) categories.row();
frame.add(mainStack).colspan(3).fill();
if(categoryEmpty[cat.ordinal()]){
categories.image(Styles.black6);
continue;
frame.row();
//for better inset visuals at the bottom
frame.rect((x, y, w, h) -> {
if(Core.scene.marginBottom > 0){
Tex.paneLeft.draw(x, 0, w, y);
}
}).colspan(3).fillX().row();
//commandTable: commanded units
{
commandTable.touchable = Touchable.enabled;
commandTable.add(Core.bundle.get("commandmode.name")).fill().center().labelAlign(Align.center).row();
commandTable.image().color(Pal.accent).growX().pad(20f).padTop(0f).padBottom(4f).row();
commandTable.table(u -> {
u.left();
int[] curCount = {0};
UnitCommand[] currentCommand = {null};
var commands = new Seq<UnitCommand>();
UnitStance[] currentStance = {null};
var stances = new Seq<UnitStance>();
rebuildCommand = () -> {
u.clearChildren();
var units = control.input.selectedUnits;
if(units.size > 0){
int[] counts = new int[content.units().size];
for(var unit : units){
counts[unit.type.id] ++;
}
commands.clear();
stances.clear();
boolean firstCommand = false, firstStance = false;
Table unitlist = u.table().growX().left().get();
unitlist.left();
int col = 0;
for(int i = 0; i < counts.length; i++){
if(counts[i] > 0){
var type = content.unit(i);
unitlist.add(StatValues.stack(type, counts[i])).pad(4).with(b -> {
b.clearListeners();
b.addListener(Tooltips.getInstance().create(type.localizedName, false));
var listener = new ClickListener();
//left click -> select
b.clicked(KeyCode.mouseLeft, () -> {
control.input.selectedUnits.removeAll(unit -> unit.type != type);
Events.fire(Trigger.unitCommandChange);
});
//right click -> remove
b.clicked(KeyCode.mouseRight, () -> {
control.input.selectedUnits.removeAll(unit -> unit.type == type);
Events.fire(Trigger.unitCommandChange);
});
b.addListener(listener);
b.addListener(new HandCursorListener());
//gray on hover
b.update(() -> ((Group)b.getChildren().first()).getChildren().first().setColor(listener.isOver() ? Color.lightGray : Color.white));
});
if(++col % 7 == 0){
unitlist.row();
}
if(!firstCommand){
commands.add(type.commands);
firstCommand = true;
}else{
//remove commands that this next unit type doesn't have
commands.removeAll(com -> !type.commands.contains(com));
}
if(!firstStance){
stances.add(type.stances);
firstStance = true;
}else{
//remove commands that this next unit type doesn't have
stances.removeAll(st -> !type.stances.contains(st));
}
}
}
//list commands
if(commands.size > 1){
u.row();
u.table(coms -> {
coms.left();
int scol = 0;
for(var command : commands){
coms.button(Icon.icons.get(command.icon, Icon.cancel), Styles.clearNoneTogglei, () -> {
Call.setUnitCommand(player, units.mapInt(un -> un.id).toArray(), command);
}).checked(i -> currentCommand[0] == command).size(50f).tooltip(command.localized(), true);
if(++scol % 6 == 0) coms.row();
}
}).fillX().padTop(4f).left();
}
//list stances
if(stances.size > 1){
u.row();
if(commands.size > 1){
u.add(new Image(Tex.whiteui)).height(3f).color(Pal.gray).pad(7f).growX().row();
}
u.table(coms -> {
coms.left();
int scol = 0;
for(var stance : stances){
coms.button(Icon.icons.get(stance.icon, Icon.cancel), Styles.clearNoneTogglei, () -> {
Call.setUnitStance(player, units.mapInt(un -> un.id).toArray(), stance);
}).checked(i -> currentStance[0] == stance).size(50f).tooltip(stance.localized(), true);
if(++scol % 6 == 0) coms.row();
}
}).fillX().padTop(4f).left();
}
}else{
u.add(Core.bundle.get("commandmode.nounits")).color(Color.lightGray).growX().center().labelAlign(Align.center).pad(6);
}
};
u.update(() -> {
{
boolean hadCommand = false, hadStance = false;
UnitCommand shareCommand = null;
UnitStance shareStance = null;
//find the command that all units have, or null if they do not share one
for(var unit : control.input.selectedUnits){
if(unit.isCommandable()){
var nextCommand = unit.command().command;
if(hadCommand){
if(shareCommand != nextCommand){
shareCommand = null;
}
}else{
shareCommand = nextCommand;
hadCommand = true;
}
var nextStance = unit.command().stance;
if(hadStance){
if(shareStance != nextStance){
shareStance = null;
}
}else{
shareStance = nextStance;
hadStance = true;
}
}
}
currentCommand[0] = shareCommand;
currentStance[0] = shareStance;
int size = control.input.selectedUnits.size;
if(curCount[0] != size){
curCount[0] = size;
rebuildCommand.run();
}
//not a huge fan of running input logic here, but it's convenient as the stance arrays are all here...
for(UnitStance stance : stances){
//first stance must always be the stop stance
if(stance.keybind != null && Core.input.keyTap(stance.keybind)){
Call.setUnitStance(player, control.input.selectedUnits.mapInt(un -> un.id).toArray(), stance);
}
}
for(UnitCommand command : commands){
//first stance must always be the stop stance
if(command.keybind != null && Core.input.keyTap(command.keybind)){
Call.setUnitCommand(player, control.input.selectedUnits.mapInt(un -> un.id).toArray(), command);
}
}
}
});
rebuildCommand.run();
}).grow();
}
//blockCatTable: all blocks | all categories
{
blockCatTable.table(Tex.pane2, blocksSelect -> {
blocksSelect.margin(4).marginTop(0);
blockPane = blocksSelect.pane(blocks -> blockTable = blocks).height(194f).update(pane -> {
if(pane.hasScroll()){
Element result = Core.scene.getHoverElement();
if(result == null || !result.isDescendantOf(pane)){
Core.scene.setScrollFocus(null);
}
}
}).grow().get();
blockPane.setStyle(Styles.smallPane);
blocksSelect.row();
blocksSelect.table(t -> {
t.image().color(Pal.gray).height(4f).colspan(4).growX();
t.row();
control.input.buildPlacementUI(t);
}).name("inputTable").growX();
}).fillY().bottom().touchable(Touchable.enabled);
blockCatTable.table(categories -> {
categories.bottom();
categories.add(new Image(Styles.black6){
@Override
public void draw(){
if(height <= Scl.scl(3f)) return;
getDrawable().draw(x, y, width, height - Scl.scl(3f));
}
}).colspan(2).growX().growY().padTop(-3f).row();
categories.defaults().size(50f);
ButtonGroup<ImageButton> group = new ButtonGroup<>();
//update category empty values
for(Category cat : Category.all){
Seq<Block> blocks = getUnlockedByCategory(cat);
categoryEmpty[cat.ordinal()] = blocks.isEmpty();
}
categories.button(ui.getIcon(cat.name()), Styles.clearToggleTransi, () -> {
currentCategory = cat;
if(control.input.block != null){
control.input.block = getSelectedBlock(currentCategory);
boolean needsAssign = categoryEmpty[currentCategory.ordinal()];
int f = 0;
for(Category cat : getCategories()){
if(f++ % 2 == 0) categories.row();
if(categoryEmpty[cat.ordinal()]){
categories.image(Styles.black6);
continue;
}
rebuildCategory.run();
}).group(group).update(i -> i.setChecked(currentCategory == cat)).name("category-" + cat.name());
}
}).fillY().bottom().touchable(Touchable.enabled);
if(needsAssign){
currentCategory = cat;
needsAssign = false;
}
categories.button(ui.getIcon(cat.name()), Styles.clearTogglei, () -> {
currentCategory = cat;
if(control.input.block != null){
control.input.block = getSelectedBlock(currentCategory);
}
rebuildCategory.run();
}).group(group).update(i -> i.setChecked(currentCategory == cat)).name("category-" + cat.name());
}
}).fillY().bottom().touchable(Touchable.enabled);
}
mainStack.add(blockCatTable);
rebuildCategory.run();
frame.update(() -> {
if(gridUpdate(control.input)) rebuildCategory.run();
if(!control.input.commandMode && gridUpdate(control.input)){
rebuildCategory.run();
}
});
});
});
@@ -421,7 +720,7 @@ public class PlacementFragment extends Fragment{
}
Seq<Block> getByCategory(Category cat){
return returnArray.selectFrom(content.blocks(), block -> block.category == cat && block.isVisible());
return returnArray.selectFrom(content.blocks(), block -> block.category == cat && block.isVisible() && block.environmentBuildable());
}
Seq<Block> getUnlockedByCategory(Category cat){
@@ -433,7 +732,8 @@ public class PlacementFragment extends Fragment{
}
boolean unlocked(Block block){
return block.unlockedNow();
return block.unlockedNowHost() && block.placeablePlayer && block.environmentBuildable() &&
block.supportsEnv(state.rules.env);
}
boolean hasInfoBox(){
@@ -441,16 +741,15 @@ public class PlacementFragment extends Fragment{
return control.input.block != null || menuHoverBlock != null || hover != null;
}
/** Returns the thing being hovered over. */
@Nullable
Displayable hovered(){
/** @return the thing being hovered over. */
public @Nullable Displayable hovered(){
Vec2 v = topTable.stageToLocalCoordinates(Core.input.mouse());
//if the mouse intersects the table or the UI has the mouse, no hovering can occur
if(Core.scene.hasMouse() || topTable.hit(v.x, v.y, false) != null) return null;
if(Core.scene.hasMouse(Core.input.mouseX(), Core.input.mouseY()) || topTable.hit(v.x, v.y, false) != null) return null;
//check for a unit
Unit unit = Units.closestOverlap(player.team(), Core.input.mouseWorldX(), Core.input.mouseWorldY(), 5f, u -> !u.isLocal());
Unit unit = Units.closestOverlap(player.team(), Core.input.mouseWorldX(), Core.input.mouseWorldY(), 5f, u -> !u.isLocal() && u.displayable());
//if cursor has a unit, display it
if(unit != null) return unit;
@@ -458,13 +757,12 @@ public class PlacementFragment extends Fragment{
Tile hoverTile = world.tileWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y);
if(hoverTile != null){
//if the tile has a building, display it
if(hoverTile.build != null){
hoverTile.build.updateFlow = true;
return hoverTile.build;
if(hoverTile.build != null && hoverTile.build.displayable() && !hoverTile.build.inFogTo(player.team())){
return nextFlowBuild = hoverTile.build;
}
//if the tile has a drop, display the drop
if(hoverTile.drop() != null){
if((hoverTile.drop() != null && hoverTile.block() == Blocks.air) || hoverTile.wallDrop() != null || hoverTile.floor().liquidDrop != null){
return hoverTile;
}
}

View File

@@ -0,0 +1,83 @@
package mindustry.ui.fragments;
import arc.*;
import arc.math.*;
import arc.math.geom.*;
import arc.scene.*;
import arc.scene.actions.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.ctype.*;
import mindustry.entities.units.*;
import mindustry.game.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import static mindustry.Vars.*;
/**
* Displays the configuration UI for build plans before they have been placed.
* Shamelessly stolen from Foo's Client.
*/
public class PlanConfigFragment{
Table table = new Table();
BuildPlan selected;
public void build(Group parent){
table.visible = false;
parent.addChild(table);
Events.on(EventType.ResetEvent.class, e -> forceHide());
}
public void showConfig(BuildPlan plan){
if(this.selected == plan || plan.block == null){
hide();
return;
}
Block block = plan.block;
if(!block.configurable) return;
selected = plan;
table.clear();
var options = new Seq<UnlockableContent>();
block.getPlanConfigs(options);
if(options.isEmpty()) return;
ItemSelection.buildTable(
table, options,
() -> selected != null ? (selected.config instanceof UnlockableContent c ? c : null) : null,
content -> {
selected.config = content;
hide();
},
block.selectionRows, block.selectionColumns
);
table.pack();
table.setTransform(true);
table.visible = true;
table.actions(Actions.scaleTo(0f, 1f), Actions.visible(true),
Actions.scaleTo(1f, 1f, 0.07f, Interp.pow3Out));
table.update(() -> {
table.setOrigin(Align.center);
if(plan.isDone() || !(control.input.selectPlans.contains(plan) || player.dead() || player.unit().plans.contains(plan))){
this.hide();
return;
}
Vec2 pos = Core.input.mouseScreen(plan.drawx(), plan.drawy() - block.size * tilesize / 2.0F - 1);
table.setPosition(pos.x, pos.y, Align.top);
});
}
public void forceHide(){
table.visible = false;
selected = null;
}
public void hide(){
selected = null;
table.actions(Actions.scaleTo(0f, 1f, 0.06f, Interp.pow3Out), Actions.visible(false));
}
}

View File

@@ -1,29 +1,32 @@
package mindustry.ui.fragments;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.scene.*;
import arc.scene.event.*;
import arc.scene.ui.*;
import arc.scene.ui.ImageButton.*;
import arc.scene.ui.layout.*;
import arc.struct.*;
import arc.util.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.net.*;
import mindustry.net.Packets.*;
import mindustry.ui.*;
import mindustry.ui.dialogs.*;
import static mindustry.Vars.*;
public class PlayerListFragment extends Fragment{
public class PlayerListFragment{
public Table content = new Table().marginRight(13f).marginLeft(13f);
private boolean visible = false;
private Interval timer = new Interval();
private TextField sField;
private TextField search;
private Seq<Player> players = new Seq<>();
@Override
public void build(Group parent){
content.name = "players";
parent.fill(cont -> {
@@ -47,15 +50,12 @@ public class PlayerListFragment extends Fragment{
cont.table(Tex.buttonTrans, pane -> {
pane.label(() -> Core.bundle.format(Groups.player.size() == 1 ? "players.single" : "players", Groups.player.size()));
pane.row();
sField = pane.field(null, text -> {
rebuild();
}).grow().pad(8).get();
sField.name = "search";
sField.setMaxLength(maxNameLength);
sField.setMessageText(Core.bundle.format("players.search"));
search = pane.field(null, text -> rebuild()).grow().pad(8).name("search").maxTextLength(maxNameLength).get();
search.setMessageText(Core.bundle.get("players.search"));
pane.row();
pane.pane(content).grow().get().setScrollingDisabled(true, false);
pane.pane(content).grow().scrollX(false);
pane.row();
pane.table(menu -> {
@@ -67,102 +67,191 @@ public class PlayerListFragment extends Fragment{
menu.button("@close", this::toggle);
}).margin(0f).pad(10f).growX();
}).touchable(Touchable.enabled).margin(14f);
}).touchable(Touchable.enabled).margin(14f).minWidth(360f);
});
rebuild();
}
public void rebuild(){
boolean allowTeamSwitch = !state.isCampaign() && (state.rules.pvp || state.rules.infiniteResources);
content.clear();
float h = 74f;
float h = 50f;
boolean found = false;
players.clear();
Groups.player.copy(players);
players.sort(Structs.comps(Structs.comparing(Player::team), Structs.comparingBool(p -> !p.admin)));
if(search.getText().length() > 0){
players.retainAll(p -> Strings.stripColors(p.name().toLowerCase()).contains(search.getText().toLowerCase()));
}
for(var user : players){
found = true;
NetConnection connection = user.con;
if(connection == null && net.server() && !user.isLocal()) return;
if(sField.getText().length() > 0 && !user.name().toLowerCase().contains(sField.getText().toLowerCase()) && !Strings.stripColors(user.name().toLowerCase()).contains(sField.getText().toLowerCase())) return;
Table button = new Table();
button.left();
button.margin(5).marginBottom(10);
Table table = new Table(){
ClickListener listener = new ClickListener();
Table iconTable = new Table(){
@Override
public void draw(){
super.draw();
Draw.color(Pal.gray);
Draw.colorMul(user.team().color, listener.isOver() ? 1.3f : 1f);
Draw.alpha(parentAlpha);
Lines.stroke(Scl.scl(4f));
Lines.rect(x, y, width, height);
Draw.reset();
}
};
table.margin(8);
table.add(new Image(user.icon()).setScaling(Scaling.bounded)).grow();
table.name = user.name();
button.add(table).size(h);
button.labelWrap("[#" + user.color().toString().toUpperCase() + "]" + user.name()).width(170f).pad(10);
boolean clickable = !(state.rules.fog && state.rules.pvp && user.team() != player.team());
if(clickable){
iconTable.addListener(listener);
iconTable.addListener(new HandCursorListener());
}
iconTable.margin(8);
iconTable.add(new Image(user.icon()).setScaling(Scaling.bounded)).grow();
iconTable.name = user.name();
iconTable.touchable = Touchable.enabled;
iconTable.tapped(() -> {
if(!user.dead() && clickable){
control.input.spectate(user.unit());
ui.showInfoFade(Core.bundle.format("viewplayer", user.name), 1f);
}
});
button.add(iconTable).size(h);
button.labelWrap("[#" + user.color().toString().toUpperCase() + "]" + user.name()).style(Styles.outlineLabel).width(170f).pad(10);
button.add().grow();
button.background(Tex.underline);
button.image(Icon.admin).visible(() -> user.admin && !(!user.isLocal() && net.server())).padRight(5).get().updateVisibility();
if((net.server() || player.admin) && !user.isLocal() && (!user.admin || net.server())){
var ustyle = new ImageButtonStyle(){{
down = Styles.none;
up = Styles.none;
imageDownColor = Pal.accent;
imageUpColor = Color.white;
imageOverColor = Color.lightGray;
}};
if(net.server() || (player.admin && (!user.admin || user == player))){
button.add().growY();
float bs = (h) / 2f;
if(allowTeamSwitch || user != player){
button.button(Icon.menu, ustyle, () -> {
var dialog = new BaseDialog(user.coloredName());
button.table(t -> {
t.defaults().size(bs);
dialog.title.setColor(Color.white);
dialog.titleTable.remove();
t.button(Icon.hammer, Styles.clearPartiali,
() -> ui.showConfirm("@confirm", Core.bundle.format("confirmban", user.name()), () -> Call.adminRequest(user, AdminAction.ban)));
t.button(Icon.cancel, Styles.clearPartiali,
() -> ui.showConfirm("@confirm", Core.bundle.format("confirmkick", user.name()), () -> Call.adminRequest(user, AdminAction.kick)));
dialog.closeOnBack();
t.row();
var bstyle = Styles.defaultt;
t.button(Icon.admin, Styles.clearTogglePartiali, () -> {
if(net.client()) return;
dialog.cont.add(user.coloredName()).row();
dialog.cont.image(Tex.whiteui, Pal.accent).fillX().height(3f).pad(4f).row();
String id = user.uuid();
dialog.cont.pane(t -> {
t.defaults().size(220f, 55f).pad(3f);
if(netServer.admins.isAdmin(id, connection.address)){
ui.showConfirm("@confirm", Core.bundle.format("confirmunadmin", user.name()), () -> netServer.admins.unAdminPlayer(id));
}else{
ui.showConfirm("@confirm", Core.bundle.format("confirmadmin", user.name()), () -> netServer.admins.adminPlayer(id, user.usid()));
}
}).update(b -> b.setChecked(user.admin))
.disabled(b -> net.client())
.touchable(() -> net.client() ? Touchable.disabled : Touchable.enabled)
.checked(user.admin);
if(user != player){
t.button("@player.ban", Icon.hammer, bstyle, () -> {
ui.showConfirm("@confirm", Core.bundle.format("confirmban", user.name()), () -> Call.adminRequest(user, AdminAction.ban, null));
dialog.hide();
}).row();
t.button(Icon.zoom, Styles.clearPartiali, () -> Call.adminRequest(user, AdminAction.trace));
t.button("@player.kick", Icon.cancel, bstyle, () -> {
ui.showConfirm("@confirm", Core.bundle.format("confirmkick", user.name()), () -> Call.adminRequest(user, AdminAction.kick, null));
dialog.hide();
}).row();
}).padRight(12).size(bs + 10f, bs);
t.button("@player.trace", Icon.zoom, bstyle, () -> {
Call.adminRequest(user, AdminAction.trace, null);
dialog.hide();
}).row();
}
//there's generally no reason to team switch outside PvP or sandbox, and it's basically an easy way to cheat
if(allowTeamSwitch){
t.button("@player.team", Icon.redo, bstyle, () -> {
var teamSelect = new BaseDialog(Core.bundle.get("player.team") + ": " + user.name);
teamSelect.setFillParent(false);
var group = new ButtonGroup<>();
int i = 0;
for(Team team : Team.baseTeams){
var b = new ImageButton(Tex.whiteui, Styles.clearNoneTogglei);
b.margin(4f);
b.getImageCell().grow();
b.getStyle().imageUpColor = team.color;
b.clicked(() -> {
Call.adminRequest(user, AdminAction.switchTeam, team);
teamSelect.hide();
});
teamSelect.cont.add(b).size(50f).checked(a -> user.team() == team).group(group);
if(i++ % 3 == 2) teamSelect.cont.row();
}
teamSelect.addCloseButton();
teamSelect.show();
dialog.hide();
}).row();
}
if(!net.client() && !user.isLocal()){
t.button("@player.admin", Icon.admin, Styles.togglet, () -> {
dialog.hide();
String id = user.uuid();
if(user.admin){
ui.showConfirm("@confirm", Core.bundle.format("confirmunadmin", user.name()), () -> {
netServer.admins.unAdminPlayer(id);
user.admin = false;
});
}else{
ui.showConfirm("@confirm", Core.bundle.format("confirmadmin", user.name()), () -> {
netServer.admins.adminPlayer(id, user.usid());
user.admin = true;
});
}
}).checked(b -> user.admin).row();
}
}).row();
dialog.cont.button("@back", Icon.left, dialog::hide).padTop(-1f).size(220f, 55f);
dialog.show();
}).size(h);
}
}else if(!user.isLocal() && !user.admin && net.client() && Groups.player.size() >= 3 && player.team() == user.team()){ //votekick
button.add().growY();
button.button(Icon.hammer, Styles.clearPartiali,
() -> {
ui.showConfirm("@confirm", Core.bundle.format("confirmvotekick", user.name()), () -> {
Call.sendChatMessage("/votekick " + user.name());
});
}).size(h);
button.button(Icon.hammer, ustyle,
() -> ui.showTextInput("@votekick.reason", Core.bundle.format("votekick.reason.message", user.name()), "",
reason -> Call.sendChatMessage("/votekick #" + user.id + " " + reason)))
.size(h);
}
content.add(button).padBottom(-6).width(350f).maxHeight(h + 14);
content.row();
content.image().height(4f).color(state.rules.pvp ? user.team().color : Pal.gray).growX();
content.add(button).width(350f).height(h + 14);
content.row();
}
@@ -179,7 +268,7 @@ public class PlayerListFragment extends Fragment{
rebuild();
}else{
Core.scene.setKeyboardFocus(null);
sField.clearText();
search.clearText();
}
}

View File

@@ -26,7 +26,7 @@ public class BranchTreeLayout implements TreeLayout{
}
private float getWidthOrHeightOfNode(TreeNode treeNode, boolean returnWidth){
return returnWidth ? treeNode.width : treeNode.height;
return returnWidth ? treeNode.calcWidth() : treeNode.height;
}
private float getNodeThickness(TreeNode treeNode){

View File

@@ -11,12 +11,23 @@ public interface TreeLayout{
public T parent;
//internal stuff
public float mode, prelim, change, shift;
public float mode, prelim, change, shift, cachedWidth = -1f;
public int number = -1, leaves;
public TreeNode thread, ancestor;
public boolean isLeaf(){
return children == null || children.length == 0;
}
public float calcWidth(){
if(children == null) return width;
if(cachedWidth > 0) return cachedWidth;
float cWidth = 0;
for(T node : children){
cWidth += node.calcWidth();
}
return cachedWidth = Math.max(width, cWidth);
}
}
}