diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 7d4d731df1..9af4f50c90 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -17,7 +17,8 @@ android:usesCleartextTraffic="true" android:appCategory="game" android:label="@string/app_name" - android:theme="@style/ArcTheme" android:fullBackupContent="@xml/backup_rules"> + android:theme="@style/ArcTheme" + android:fullBackupContent="@xml/backup_rules"> copy{ @@ -131,25 +155,7 @@ task copyAndroidNatives(){ } task run(type: Exec){ - def path - def localProperties = project.file("../local.properties") - if(localProperties.exists()){ - Properties properties = new Properties() - localProperties.withInputStream{ instr -> - properties.load(instr) - } - def sdkDir = properties.getProperty('sdk.dir') - if(sdkDir){ - path = sdkDir - }else{ - path = "$System.env.ANDROID_HOME" - } - }else{ - path = "$System.env.ANDROID_HOME" - } - - def adb = path + "/platform-tools/adb" - commandLine "$adb", 'shell', 'am', 'start', '-n', 'io.anuke.mindustry/mindustry.android.AndroidLauncher' + commandLine "${findSdkDir()}/platform-tools/adb", 'shell', 'am', 'start', '-n', 'io.anuke.mindustry/mindustry.android.AndroidLauncher' } if(!project.ext.hasSprites()){ diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro new file mode 100644 index 0000000000..f25e5e79c5 --- /dev/null +++ b/android/proguard-rules.pro @@ -0,0 +1,8 @@ +-dontobfuscate + +#these are essential packages that should not be "optimized" in any way +#the main purpose of d8 here is to shrink the absurdly-large google play games libraries +-keep class mindustry.** { *; } +-keep class arc.** { *; } +-keep class net.jpountz.** { *; } +-keep class rhino.** { *; } diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml index d40cf3accd..d42ffe6d4a 100644 --- a/android/res/values/strings.xml +++ b/android/res/values/strings.xml @@ -1,6 +1,4 @@ - Mindustry - diff --git a/android/res/values/styles.xml b/android/res/values/styles.xml index 3fe4d7f6a8..c37d2c6ed9 100644 --- a/android/res/values/styles.xml +++ b/android/res/values/styles.xml @@ -1,5 +1,4 @@ - - diff --git a/android/src/mindustry/android/AndroidLauncher.java b/android/src/mindustry/android/AndroidLauncher.java index b46d09c9c0..69543be82e 100644 --- a/android/src/mindustry/android/AndroidLauncher.java +++ b/android/src/mindustry/android/AndroidLauncher.java @@ -15,9 +15,11 @@ import arc.func.*; import arc.scene.ui.layout.*; import arc.util.*; import dalvik.system.*; +import io.anuke.mindustry.*; import mindustry.*; import mindustry.game.Saves.*; import mindustry.io.*; +import mindustry.mod.*; import mindustry.net.*; import mindustry.ui.dialogs.*; @@ -33,6 +35,9 @@ public class AndroidLauncher extends AndroidApplication{ FileChooser chooser; Runnable permCallback; + Object gpService; + Class serviceClass; + @Override protected void onCreate(Bundle savedInstanceState){ UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler(); @@ -50,7 +55,7 @@ public class AndroidLauncher extends AndroidApplication{ }); super.onCreate(savedInstanceState); - if(doubleScaleTablets && isTablet(this.getContext())){ + if(doubleScaleTablets && isTablet(this)){ Scl.setAddition(0.5f); } @@ -63,7 +68,9 @@ public class AndroidLauncher extends AndroidApplication{ @Override public rhino.Context getScriptContext(){ - return AndroidRhinoContext.enter(getContext().getCacheDir()); + rhino.Context result = AndroidRhinoContext.enter(((Context)AndroidLauncher.this).getCacheDir()); + result.setClassShutter(Scripts::allowClass); + return result; } @Override @@ -71,8 +78,8 @@ public class AndroidLauncher extends AndroidApplication{ } @Override - public ClassLoader loadJar(Fi jar, String mainClass) throws Exception{ - return new DexClassLoader(jar.file().getPath(), getFilesDir().getPath(), null, getClassLoader()); + public ClassLoader loadJar(Fi jar, ClassLoader parent) throws Exception{ + return new DexClassLoader(jar.file().getPath(), getFilesDir().getPath(), null, parent); } @Override @@ -165,9 +172,20 @@ public class AndroidLauncher extends AndroidApplication{ try{ //new external folder - Fi data = Core.files.absolute(getContext().getExternalFilesDir(null).getAbsolutePath()); + Fi data = Core.files.absolute(((Context)this).getExternalFilesDir(null).getAbsolutePath()); Core.settings.setDataDirectory(data); + //delete unused cache folder to free up space + try{ + Fi cache = Core.settings.getDataDirectory().child("cache"); + if(cache.exists()){ + cache.deleteDirectory(); + } + }catch(Throwable t){ + Log.err("Failed to delete cached folder", t); + } + + //move to internal storage if there's no file indicating that it moved if(!Core.files.local("files_moved").exists()){ Log.info("Moving files to external storage..."); @@ -209,6 +227,24 @@ public class AndroidLauncher extends AndroidApplication{ } } + @Override + public void onResume(){ + super.onResume(); + + //TODO enable once GPGS is set up on the GP console + if(false && BuildConfig.FLAVOR.equals("gp")){ + try{ + if(gpService == null){ + serviceClass = Class.forName("mindustry.android.GPGameService"); + gpService = serviceClass.getConstructor().newInstance(); + } + serviceClass.getMethod("onResume", Context.class).invoke(gpService, this); + }catch(Exception e){ + Log.err("Failed to update Google Play Services", e); + } + } + } + private void checkFiles(Intent intent){ try{ Uri uri = intent.getData(); diff --git a/android/src/mindustry/android/AndroidRhinoContext.java b/android/src/mindustry/android/AndroidRhinoContext.java index d3e80c7427..0147f118b8 100644 --- a/android/src/mindustry/android/AndroidRhinoContext.java +++ b/android/src/mindustry/android/AndroidRhinoContext.java @@ -175,7 +175,7 @@ public class AndroidRhinoContext{ }catch(IOException e){ e.printStackTrace(); } - android.content.Context context = ((AndroidApplication) Core.app).getContext(); + android.content.Context context = (android.content.Context)((AndroidApplication)Core.app); return new DexClassLoader(dexFile.getPath(), VERSION.SDK_INT >= 21 ? context.getCodeCacheDir().getPath() : context.getCacheDir().getAbsolutePath(), null, getParent()).loadClass(name); } diff --git a/android/srcgp/mindustry/android/GPGameService.java b/android/srcgp/mindustry/android/GPGameService.java new file mode 100644 index 0000000000..9a64b6f3d4 --- /dev/null +++ b/android/srcgp/mindustry/android/GPGameService.java @@ -0,0 +1,40 @@ +package mindustry.android; + +import android.content.*; +import arc.util.*; +import com.google.android.gms.auth.api.signin.*; +import com.google.android.gms.games.*; +import mindustry.service.*; + +public class GPGameService extends GameService{ + private GoogleSignInAccount account; + + public void onResume(Context context){ + Log.info("[GooglePlayService] Resuming."); + + GoogleSignInAccount current = GoogleSignIn.getLastSignedInAccount(context); + + GoogleSignInOptions options = + new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN) + .requestScopes(Games.SCOPE_GAMES_SNAPSHOTS) + .build(); + + if(GoogleSignIn.hasPermissions(current, options.getScopeArray())){ + this.account = current; + Log.info("Already signed in to Google Play Games."); + }else{ + GoogleSignIn.getClient(context, options).silentSignIn().addOnCompleteListener(complete -> { + if(!complete.isSuccessful()){ + if(complete.getException() != null){ + Log.err("Failed to sign in to Google Play Games.", complete.getException()); + }else{ + Log.warn("Failed to sign in to Google Play Games."); + } + }else{ + this.account = complete.getResult(); + Log.info("Signed in to Google Play Games."); + } + }); + } + } +} diff --git a/annotations/src/main/java/mindustry/annotations/Annotations.java b/annotations/src/main/java/mindustry/annotations/Annotations.java index 88bd457e05..9960f673eb 100644 --- a/annotations/src/main/java/mindustry/annotations/Annotations.java +++ b/annotations/src/main/java/mindustry/annotations/Annotations.java @@ -118,7 +118,7 @@ public class Annotations{ /** * The region name to load. Variables can be used: * "@" -> block name - * "$size" -> block size + * "@size" -> block size * "#" "#1" "#2" -> index number, for arrays * */ String value(); @@ -177,12 +177,12 @@ public class Annotations{ //region remote public enum PacketPriority{ + /** Does not get handled unless client is connected. */ + low, /** Gets put in a queue and processed if not connected. */ normal, /** Gets handled immediately, regardless of connection status. */ high, - /** Does not get handled unless client is connected. */ - low } /** A set of two booleans, one specifying server and one specifying client. */ diff --git a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java index 74cd4e8bde..310f8e2906 100644 --- a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java +++ b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java @@ -2,15 +2,10 @@ package mindustry.annotations; import arc.files.*; import arc.struct.*; -import arc.util.Log; -import arc.util.Log.*; import arc.util.*; +import arc.util.Log.*; import com.squareup.javapoet.*; import com.sun.source.util.*; -import com.sun.tools.javac.model.*; -import com.sun.tools.javac.processing.*; -import com.sun.tools.javac.tree.*; -import com.sun.tools.javac.util.*; import mindustry.annotations.util.*; import javax.annotation.processing.*; @@ -22,7 +17,6 @@ import javax.tools.Diagnostic.*; import javax.tools.*; import java.io.*; import java.lang.annotation.*; -import java.util.List; import java.util.*; @SupportedSourceVersion(SourceVersion.RELEASE_8) @@ -31,19 +25,16 @@ public abstract class BaseProcessor extends AbstractProcessor{ public static final String packageName = "mindustry.gen"; public static Types typeu; - public static JavacElements elementu; + public static Elements elementu; public static Filer filer; public static Messager messager; public static Trees trees; - public static TreeMaker maker; protected int round; protected int rounds = 1; protected RoundEnvironment env; protected Fi rootDirectory; - protected Context context; - public static String getMethodName(Element element){ return ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString() + "." + element.getSimpleName(); } @@ -186,7 +177,7 @@ public abstract class BaseProcessor extends AbstractProcessor{ Log.err("[CODEGEN ERROR] " + message + ": " + elem); } - public void err(String message, Selement elem){ + public static void err(String message, Selement elem){ err(message, elem.e); } @@ -194,15 +185,11 @@ public abstract class BaseProcessor extends AbstractProcessor{ public synchronized void init(ProcessingEnvironment env){ super.init(env); - JavacProcessingEnvironment javacProcessingEnv = (JavacProcessingEnvironment)env; - trees = Trees.instance(env); typeu = env.getTypeUtils(); - elementu = javacProcessingEnv.getElementUtils(); + elementu = env.getElementUtils(); filer = env.getFiler(); messager = env.getMessager(); - context = ((JavacProcessingEnvironment)env).getContext(); - maker = TreeMaker.instance(javacProcessingEnv.getContext()); Log.level = LogLevel.info; diff --git a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java index 6067b986b5..9612018e7b 100644 --- a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java +++ b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java @@ -132,6 +132,7 @@ public class EntityProcess extends BaseProcessor{ .build())).addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT).build()); } + //generate interface getters and setters for all "standard" fields for(Svar field : component.fields().select(e -> !e.is(Modifier.STATIC) && !e.is(Modifier.PRIVATE) && !e.has(Import.class))){ String cname = field.name(); @@ -674,11 +675,28 @@ public class EntityProcess extends BaseProcessor{ //build mapping class for sync IDs TypeSpec.Builder idBuilder = TypeSpec.classBuilder("EntityMapping").addModifiers(Modifier.PUBLIC) .addField(FieldSpec.builder(TypeName.get(Prov[].class), "idMap", Modifier.PUBLIC, Modifier.STATIC).initializer("new Prov[256]").build()) + .addField(FieldSpec.builder(ParameterizedTypeName.get(ClassName.get(ObjectMap.class), tname(String.class), tname(Prov.class)), "nameMap", Modifier.PUBLIC, Modifier.STATIC).initializer("new ObjectMap<>()").build()) + + .addField(FieldSpec.builder(ParameterizedTypeName.get(ClassName.get(IntMap.class), tname(String.class)), + "customIdMap", Modifier.PUBLIC, Modifier.STATIC).initializer("new IntMap<>()").build()) + + .addMethod(MethodSpec.methodBuilder("register").addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(TypeName.get(int.class)) + .addParameter(String.class, "name").addParameter(Prov.class, "constructor") + .addStatement("int next = arc.util.Structs.indexOf(idMap, v -> v == null)") + .addStatement("idMap[next] = constructor") + .addStatement("nameMap.put(name, constructor)") + .addStatement("customIdMap.put(next, name)") + .addStatement("return next") + .addJavadoc("Use this method for obtaining a classId for custom modded unit types. Only call this once for each type. Modded types should return this id in their overridden classId method.") + .build()) + .addMethod(MethodSpec.methodBuilder("map").addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(TypeName.get(Prov.class)).addParameter(int.class, "id").addStatement("return idMap[id]").build()) + .addMethod(MethodSpec.methodBuilder("map").addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(TypeName.get(Prov.class)).addParameter(String.class, "name").addStatement("return nameMap.get(name)").build()); @@ -707,11 +725,6 @@ public class EntityProcess extends BaseProcessor{ }else{ //round 3: generate actual classes and implement interfaces - //write base classes - for(TypeSpec.Builder b : baseClasses){ - write(b, imports.asArray()); - } - //implement each definition for(EntityDefinition def : definitions){ @@ -736,6 +749,12 @@ public class EntityProcess extends BaseProcessor{ if(def.legacy) continue; + @Nullable TypeSpec.Builder superclass = null; + + if(def.extend != null){ + superclass = baseClasses.find(b -> (packageName + "." + Reflect.get(b, "name")).equals(def.extend.toString())); + } + //generate getter/setter for each method for(Smethod method : inter.methods()){ String var = method.name(); @@ -743,14 +762,36 @@ public class EntityProcess extends BaseProcessor{ //make sure it's a real variable AND that the component doesn't already implement it somewhere with custom logic if(field == null || methodNames.contains(method.simpleString())) continue; + MethodSpec result = null; + //getter if(!method.isVoid()){ - def.builder.addMethod(MethodSpec.overriding(method.e).addStatement("return " + var).build()); + result = MethodSpec.overriding(method.e).addStatement("return " + var).build(); } //setter if(method.isVoid() && !Seq.with(field.annotations).contains(f -> f.type.toString().equals("@mindustry.annotations.Annotations.ReadOnly"))){ - def.builder.addMethod(MethodSpec.overriding(method.e).addStatement("this." + var + " = " + var).build()); + result = MethodSpec.overriding(method.e).addStatement("this." + var + " = " + var).build(); + } + + //add getter/setter to parent class, if possible. when this happens, skip adding getters setters *here* because they are defined in the superclass. + if(result != null && superclass != null){ + FieldSpec superField = Seq.with(superclass.fieldSpecs).find(f -> f.name.equals(var)); + + //found the right field, try to check for the method already existing now + if(superField != null){ + MethodSpec fr = result; + MethodSpec targetMethod = Seq.with(superclass.methodSpecs).find(m -> m.name.equals(var) && m.returnType.equals(fr.returnType)); + //if the method isn't added yet, add it. in any case, skip. + if(targetMethod == null){ + superclass.addMethod(result); + } + continue; + } + } + + if(result != null){ + def.builder.addMethod(result); } } } @@ -758,9 +799,16 @@ public class EntityProcess extends BaseProcessor{ write(def.builder, imports.asArray()); } + //write base classes last + for(TypeSpec.Builder b : baseClasses){ + write(b, imports.asArray()); + } + + //TODO nulls were an awful idea //store nulls TypeSpec.Builder nullsBuilder = TypeSpec.classBuilder("Nulls").addModifiers(Modifier.PUBLIC).addModifiers(Modifier.FINAL); - ObjectSet nullList = ObjectSet.with("unit", "blockUnit"); + //TODO should be dynamic + ObjectSet nullList = ObjectSet.with("unit"); //create mock types of all components for(Stype interf : allInterfaces){ @@ -918,7 +966,7 @@ public class EntityProcess extends BaseProcessor{ } String createName(Selement elem){ - Seq comps = types(elem.annotation(EntityDef.class), EntityDef::value).map(this::interfaceToComp);; + Seq comps = types(elem.annotation(EntityDef.class), EntityDef::value).map(this::interfaceToComp); comps.sortComparing(Selement::name); return comps.toString("", s -> s.name().replace("Comp", "")); } diff --git a/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java b/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java index 0b5620c11d..c144abc534 100644 --- a/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java +++ b/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java @@ -43,7 +43,7 @@ public class AssetsProcess extends BaseProcessor{ texIcons.each((key, val) -> { String[] split = val.split("\\|"); - String name = Strings.kebabToCamel(split[1]).replace("Medium", "").replace("Icon", ""); + String name = Strings.kebabToCamel(split[1]).replace("Medium", "").replace("Icon", "").replace("Ui", ""); if(SourceVersion.isKeyword(name) || name.equals("char")) name += "i"; ichtype.addField(FieldSpec.builder(char.class, name, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).addJavadoc(String.format("\\u%04x", Integer.parseInt(key))).initializer("'" + ((char)Integer.parseInt(key)) + "'").build()); diff --git a/annotations/src/main/java/mindustry/annotations/impl/CallSuperProcess.java b/annotations/src/main/java/mindustry/annotations/impl/CallSuperProcess.java deleted file mode 100644 index e28ecf13a0..0000000000 --- a/annotations/src/main/java/mindustry/annotations/impl/CallSuperProcess.java +++ /dev/null @@ -1,154 +0,0 @@ -package mindustry.annotations.impl; - -import com.sun.source.tree.*; -import com.sun.source.util.*; -import com.sun.tools.javac.code.Scope; -import com.sun.tools.javac.code.*; -import com.sun.tools.javac.code.Symbol.*; -import com.sun.tools.javac.code.Type.*; -import com.sun.tools.javac.tree.*; -import com.sun.tools.javac.tree.JCTree.*; -import mindustry.annotations.Annotations.*; - -import javax.annotation.processing.*; -import javax.lang.model.*; -import javax.lang.model.element.*; -import javax.tools.Diagnostic.*; -import java.lang.annotation.*; -import java.util.*; - -@SupportedAnnotationTypes({"java.lang.Override"}) -public class CallSuperProcess extends AbstractProcessor{ - private Trees trees; - - @Override - public void init(ProcessingEnvironment pe){ - super.init(pe); - trees = Trees.instance(pe); - } - - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv){ - for(Element e : roundEnv.getElementsAnnotatedWith(Override.class)){ - if(e.getAnnotation(OverrideCallSuper.class) != null) return false; - - CodeAnalyzerTreeScanner codeScanner = new CodeAnalyzerTreeScanner(); - codeScanner.methodName = e.getSimpleName().toString(); - - TreePath tp = trees.getPath(e.getEnclosingElement()); - codeScanner.scan(tp, trees); - - if(codeScanner.callSuperUsed){ - List list = codeScanner.method.getBody().getStatements(); - - if(!doesCallSuper(list, codeScanner.methodName)){ - processingEnv.getMessager().printMessage(Kind.ERROR, "Overriding method '" + codeScanner.methodName + "' must explicitly call super method from its parent class.", e); - } - } - } - - return false; - } - - private boolean doesCallSuper(List list, String methodName){ - for(Object object : list){ - if(object instanceof JCTree.JCExpressionStatement){ - JCTree.JCExpressionStatement expr = (JCExpressionStatement)object; - String exprString = expr.toString(); - if(exprString.startsWith("super." + methodName) && exprString.endsWith(");")) return true; - } - } - - return false; - } - - @Override - public SourceVersion getSupportedSourceVersion(){ - return SourceVersion.RELEASE_8; - } - - static class CodeAnalyzerTreeScanner extends TreePathScanner{ - String methodName; - MethodTree method; - boolean callSuperUsed; - - @Override - public Object visitClass(ClassTree classTree, Trees trees){ - Tree extendTree = classTree.getExtendsClause(); - - if(extendTree instanceof JCTypeApply){ //generic classes case - JCTypeApply generic = (JCTypeApply)extendTree; - extendTree = generic.clazz; - } - - if(extendTree instanceof JCIdent){ - JCIdent tree = (JCIdent)extendTree; - - if(tree == null || tree.sym == null) return super.visitClass(classTree, trees); - - com.sun.tools.javac.code.Scope members = tree.sym.members(); - - if(checkScope(members)) - return super.visitClass(classTree, trees); - - if(checkSuperTypes((ClassType)tree.type)) - return super.visitClass(classTree, trees); - - } - callSuperUsed = false; - - return super.visitClass(classTree, trees); - } - - public boolean checkSuperTypes(ClassType type){ - if(type.supertype_field != null && type.supertype_field.tsym != null){ - if(checkScope(type.supertype_field.tsym.members())) - return true; - else - return checkSuperTypes((ClassType)type.supertype_field); - } - - return false; - } - - @SuppressWarnings("unchecked") - public boolean checkScope(Scope members){ - Iterable it; - try{ - it = (Iterable)members.getClass().getMethod("getElements").invoke(members); - }catch(Throwable t){ - try{ - it = (Iterable)members.getClass().getMethod("getSymbols").invoke(members); - }catch(Exception e){ - throw new RuntimeException(e); - } - } - - for(Symbol s : it){ - - if(s instanceof MethodSymbol){ - MethodSymbol ms = (MethodSymbol)s; - - if(ms.getSimpleName().toString().equals(methodName)){ - Annotation annotation = ms.getAnnotation(CallSuper.class); - if(annotation != null){ - callSuperUsed = true; - return true; - } - } - } - } - - return false; - } - - @Override - public Object visitMethod(MethodTree methodTree, Trees trees){ - if(methodTree.getName().toString().equals(methodName)) - method = methodTree; - - return super.visitMethod(methodTree, trees); - } - - } -} diff --git a/annotations/src/main/java/mindustry/annotations/misc/LoadRegionProcessor.java b/annotations/src/main/java/mindustry/annotations/misc/LoadRegionProcessor.java index d2e3e99c17..7002972345 100644 --- a/annotations/src/main/java/mindustry/annotations/misc/LoadRegionProcessor.java +++ b/annotations/src/main/java/mindustry/annotations/misc/LoadRegionProcessor.java @@ -18,6 +18,7 @@ public class LoadRegionProcessor extends BaseProcessor{ @Override public void process(RoundEnvironment env) throws Exception{ TypeSpec.Builder regionClass = TypeSpec.classBuilder("ContentRegions") + .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class).addMember("value", "\"deprecation\"").build()) .addModifiers(Modifier.PUBLIC); MethodSpec.Builder method = MethodSpec.methodBuilder("loadRegions") .addParameter(tname("mindustry.ctype.MappableContent"), "content") @@ -34,7 +35,7 @@ public class LoadRegionProcessor extends BaseProcessor{ } for(Entry> entry : fieldMap){ - method.beginControlFlow("if(content instanceof $T)", entry.key.tname()); + method.beginControlFlow("if(content instanceof $L)", entry.key.fullName()); for(Svar field : entry.value){ Load an = field.annotation(Load.class); @@ -45,7 +46,7 @@ public class LoadRegionProcessor extends BaseProcessor{ //not an array if(dims == 0){ - method.addStatement("(($T)content).$L = $T.atlas.find($L$L)", entry.key.tname(), field.name(), Core.class, parse(an.value()), fallbackString); + method.addStatement("(($L)content).$L = $T.atlas.find($L$L)", entry.key.fullName(), field.name(), Core.class, parse(an.value()), fallbackString); }else{ //is an array, create length string int[] lengths = an.lengths(); diff --git a/annotations/src/main/java/mindustry/annotations/remote/CallGenerator.java b/annotations/src/main/java/mindustry/annotations/remote/CallGenerator.java new file mode 100644 index 0000000000..00ac5fabac --- /dev/null +++ b/annotations/src/main/java/mindustry/annotations/remote/CallGenerator.java @@ -0,0 +1,363 @@ +package mindustry.annotations.remote; + +import arc.struct.*; +import arc.util.io.*; +import com.squareup.javapoet.*; +import mindustry.annotations.Annotations.*; +import mindustry.annotations.*; +import mindustry.annotations.util.*; +import mindustry.annotations.util.TypeIOResolver.*; + +import javax.lang.model.element.*; +import java.io.*; + +import static mindustry.annotations.BaseProcessor.*; + +/** Generates code for writing remote invoke packets on the client and server. */ +public class CallGenerator{ + + /** Generates all classes in this list. */ + public static void generate(ClassSerializer serializer, Seq methods) throws IOException{ + //create builder + TypeSpec.Builder callBuilder = TypeSpec.classBuilder(RemoteProcess.callLocation).addModifiers(Modifier.PUBLIC); + + MethodSpec.Builder register = MethodSpec.methodBuilder("registerPackets") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC); + + //go through each method entry in this class + for(MethodEntry ent : methods){ + //builder for the packet type + TypeSpec.Builder packet = TypeSpec.classBuilder(ent.packetClassName) + .addModifiers(Modifier.PUBLIC); + + packet.superclass(tname("mindustry.net.Packet")); + + //return the correct priority + if(ent.priority != PacketPriority.normal){ + packet.addMethod(MethodSpec.methodBuilder("getPriority") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class).returns(int.class).addStatement("return $L", ent.priority.ordinal()) + .build()); + } + + //implement read & write methods + packet.addMethod(makeWriter(ent, serializer)); + packet.addMethod(makeReader(ent, serializer)); + + //generate handlers + if(ent.where.isClient){ + packet.addMethod(writeHandleMethod(ent, false)); + } + + if(ent.where.isServer){ + packet.addMethod(writeHandleMethod(ent, true)); + } + + //register packet + register.addStatement("mindustry.net.Net.registerPacket($L.$L::new)", packageName, ent.packetClassName); + + //add fields to the type + for(Svar param : ent.element.params()){ + packet.addField(param.tname(), param.name(), Modifier.PUBLIC); + } + + //write the 'send event to all players' variant: always happens for clients, but only happens if 'all' is enabled on the server method + if(ent.where.isClient || ent.target.isAll){ + writeCallMethod(callBuilder, ent, true, false); + } + + //write the 'send event to one player' variant, which is only applicable on the server + if(ent.where.isServer && ent.target.isOne){ + writeCallMethod(callBuilder, ent, false, false); + } + + //write the forwarded method version + if(ent.where.isServer && ent.forward){ + writeCallMethod(callBuilder, ent, true, true); + } + + //write the completed packet class + JavaFile.builder(packageName, packet.build()).build().writeTo(BaseProcessor.filer); + } + + callBuilder.addMethod(register.build()); + + //build and write resulting class + TypeSpec spec = callBuilder.build(); + JavaFile.builder(packageName, spec).build().writeTo(BaseProcessor.filer); + } + + private static MethodSpec makeWriter(MethodEntry ent, ClassSerializer serializer){ + MethodSpec.Builder builder = MethodSpec.methodBuilder("write") + .addParameter(Writes.class, "WRITE") + .addModifiers(Modifier.PUBLIC).addAnnotation(Override.class); + Seq params = ent.element.params(); + + for(int i = 0; i < params.size; i++){ + //first argument is skipped as it is always the player caller + if(!ent.where.isServer && i == 0){ + continue; + } + + Svar var = params.get(i); + + //name of parameter + String varName = var.name(); + //name of parameter type + String typeName = var.mirror().toString(); + //special case: method can be called from anywhere to anywhere + //thus, only write the player when the SERVER is writing data, since the client is the only one who reads the player anyway + boolean writePlayerSkipCheck = ent.where == Loc.both && i == 0; + + if(writePlayerSkipCheck){ //write begin check + builder.beginControlFlow("if(mindustry.Vars.net.server())"); + } + + if(BaseProcessor.isPrimitive(typeName)){ //check if it's a primitive, and if so write it + builder.addStatement("WRITE.$L($L)", typeName.equals("boolean") ? "bool" : typeName.charAt(0) + "", varName); + }else{ + //else, try and find a serializer + String ser = serializer.writers.get(typeName.replace("mindustry.gen.", ""), SerializerResolver.locate(ent.element.e, var.mirror(), true)); + + if(ser == null){ //make sure a serializer exists! + BaseProcessor.err("No method to write class type: '" + typeName + "'", var); + } + + //add statement for writing it + builder.addStatement(ser + "(WRITE, " + varName + ")"); + } + + if(writePlayerSkipCheck){ //write end check + builder.endControlFlow(); + } + } + + return builder.build(); + } + + private static MethodSpec makeReader(MethodEntry ent, ClassSerializer serializer){ + MethodSpec.Builder builder = MethodSpec.methodBuilder("read") + .addParameter(Reads.class, "READ") + .addModifiers(Modifier.PUBLIC).addAnnotation(Override.class); + Seq params = ent.element.params(); + + //go through each parameter + for(int i = 0; i < params.size; i++){ + Svar var = params.get(i); + + //first argument is skipped as it is always the player caller + if(!ent.where.isServer && i == 0){ + continue; + } + + //special case: method can be called from anywhere to anywhere + //thus, only read the player when the CLIENT is receiving data, since the client is the only one who cares about the player anyway + boolean writePlayerSkipCheck = ent.where == Loc.both && i == 0; + + if(writePlayerSkipCheck){ //write begin check + builder.beginControlFlow("if(mindustry.Vars.net.client())"); + } + + //full type name of parameter + String typeName = var.mirror().toString(); + //name of parameter + String varName = var.name(); + //capitalized version of type name for reading primitives + String pname = typeName.equals("boolean") ? "bool" : typeName.charAt(0) + ""; + + //write primitives automatically + if(BaseProcessor.isPrimitive(typeName)){ + builder.addStatement("$L = READ.$L()", varName, pname); + }else{ + //else, try and find a serializer + String ser = serializer.readers.get(typeName.replace("mindustry.gen.", ""), SerializerResolver.locate(ent.element.e, var.mirror(), false)); + + if(ser == null){ //make sure a serializer exists! + BaseProcessor.err("No read method to read class type '" + typeName + "' in method " + ent.targetMethod + "; " + serializer.readers, var); + } + + //add statement for reading it + builder.addStatement("$L = $L(READ)", varName, ser); + } + + if(writePlayerSkipCheck){ //write end check + builder.endControlFlow(); + } + } + + return builder.build(); + } + + /** Creates a specific variant for a method entry. */ + private static void writeCallMethod(TypeSpec.Builder classBuilder, MethodEntry ent, boolean toAll, boolean forwarded){ + Smethod elem = ent.element; + Seq params = elem.params(); + + //create builder + MethodSpec.Builder method = MethodSpec.methodBuilder(elem.name() + (forwarded ? "__forward" : "")) //add except suffix when forwarding + .addModifiers(Modifier.STATIC) + .returns(void.class); + + //forwarded methods aren't intended for use, and are not public + if(!forwarded){ + method.addModifiers(Modifier.PUBLIC); + } + + //validate client methods to make sure + if(ent.where.isClient){ + if(params.isEmpty()){ + BaseProcessor.err("Client invoke methods must have a first parameter of type Player", elem); + return; + } + + if(!params.get(0).mirror().toString().contains("Player")){ + BaseProcessor.err("Client invoke methods should have a first parameter of type Player", elem); + return; + } + } + + //if toAll is false, it's a 'send to one player' variant, so add the player as a parameter + if(!toAll){ + method.addParameter(ClassName.bestGuess("mindustry.net.NetConnection"), "playerConnection"); + } + + //add sender to ignore + if(forwarded){ + method.addParameter(ClassName.bestGuess("mindustry.net.NetConnection"), "exceptConnection"); + } + + //call local method if applicable, shouldn't happen when forwarding method as that already happens by default + if(!forwarded && ent.local != Loc.none){ + //add in local checks + if(ent.local != Loc.both){ + method.beginControlFlow("if(" + getCheckString(ent.local) + " || !mindustry.Vars.net.active())"); + } + + //concatenate parameters + int index = 0; + StringBuilder results = new StringBuilder(); + for(Svar var : params){ + //special case: calling local-only methods uses the local player + if(index == 0 && ent.where == Loc.client){ + results.append("mindustry.Vars.player"); + }else{ + results.append(var.name()); + } + if(index != params.size - 1) results.append(", "); + index++; + } + + //add the statement to call it + method.addStatement("$N." + elem.name() + "(" + results + ")", + ((TypeElement)elem.up()).getQualifiedName().toString()); + + if(ent.local != Loc.both){ + method.endControlFlow(); + } + } + + //start control flow to check if it's actually client/server so no netcode is called + method.beginControlFlow("if(" + getCheckString(ent.where) + ")"); + + //add statement to create packet from pool + method.addStatement("$1T packet = new $1T()", tname("mindustry.gen." + ent.packetClassName)); + + method.addTypeVariables(Seq.with(elem.e.getTypeParameters()).map(BaseProcessor::getTVN)); + + for(int i = 0; i < params.size; i++){ + //first argument is skipped as it is always the player caller + if((!ent.where.isServer) && i == 0){ + continue; + } + + Svar var = params.get(i); + + method.addParameter(var.tname(), var.name()); + + //name of parameter + String varName = var.name(); + //special case: method can be called from anywhere to anywhere + //thus, only write the player when the SERVER is writing data, since the client is the only one who reads it + boolean writePlayerSkipCheck = ent.where == Loc.both && i == 0; + + if(writePlayerSkipCheck){ //write begin check + method.beginControlFlow("if(mindustry.Vars.net.server())"); + } + + method.addStatement("packet.$L = $L", varName, varName); + + if(writePlayerSkipCheck){ //write end check + method.endControlFlow(); + } + } + + String sendString; + + if(forwarded){ //forward packet + if(!ent.local.isClient){ //if the client doesn't get it called locally, forward it back after validation + sendString = "mindustry.Vars.net.send("; + }else{ + sendString = "mindustry.Vars.net.sendExcept(exceptConnection, "; + } + }else if(toAll){ //send to all players / to server + sendString = "mindustry.Vars.net.send("; + }else{ //send to specific client from server + sendString = "playerConnection.send("; + } + + //send the actual packet + method.addStatement(sendString + "packet, " + (!ent.unreliable) + ")"); + + + //end check for server/client + method.endControlFlow(); + + //add method to class, finally + classBuilder.addMethod(method.build()); + } + + private static String getCheckString(Loc loc){ + return + loc.isClient && loc.isServer ? "mindustry.Vars.net.server() || mindustry.Vars.net.client()" : + loc.isClient ? "mindustry.Vars.net.client()" : + loc.isServer ? "mindustry.Vars.net.server()" : "false"; + } + + /** Generates handleServer / handleClient methods. */ + public static MethodSpec writeHandleMethod(MethodEntry ent, boolean isClient){ + + //create main method builder + MethodSpec.Builder builder = MethodSpec.methodBuilder(isClient ? "handleClient" : "handleServer") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(void.class); + + Smethod elem = ent.element; + Seq params = elem.params(); + + if(!isClient){ + //add player parameter + builder.addParameter(ClassName.get("mindustry.net", "NetConnection"), "con"); + + //skip if player is invalid + builder.beginControlFlow("if(con.player == null || con.kicked)"); + builder.addStatement("return"); + builder.endControlFlow(); + + //make sure to use the actual player who sent the packet + builder.addStatement("mindustry.gen.Player player = con.player"); + } + + //execute the relevant method before the forward + //if it throws a ValidateException, the method won't be forwarded + builder.addStatement("$N." + elem.name() + "(" + params.toString(", ", s -> s.name()) + ")", ((TypeElement)elem.up()).getQualifiedName().toString()); + + //call forwarded method, don't forward on the client reader + if(ent.forward && ent.where.isServer && !isClient){ + //call forwarded method + builder.addStatement("$L.$L.$L__forward(con, $L)", packageName, ent.className, elem.name(), params.toString(", ", s -> s.name())); + } + + return builder.build(); + } +} diff --git a/annotations/src/main/java/mindustry/annotations/remote/ClassEntry.java b/annotations/src/main/java/mindustry/annotations/remote/ClassEntry.java deleted file mode 100644 index 3474eff468..0000000000 --- a/annotations/src/main/java/mindustry/annotations/remote/ClassEntry.java +++ /dev/null @@ -1,15 +0,0 @@ -package mindustry.annotations.remote; - -import java.util.ArrayList; - -/** Represents a class witha list method entries to include in it. */ -public class ClassEntry{ - /** All methods in this generated class. */ - public final ArrayList methods = new ArrayList<>(); - /** Simple class name. */ - public final String name; - - public ClassEntry(String name){ - this.name = name; - } -} diff --git a/annotations/src/main/java/mindustry/annotations/remote/MethodEntry.java b/annotations/src/main/java/mindustry/annotations/remote/MethodEntry.java index 68ea81dec0..768b373387 100644 --- a/annotations/src/main/java/mindustry/annotations/remote/MethodEntry.java +++ b/annotations/src/main/java/mindustry/annotations/remote/MethodEntry.java @@ -1,8 +1,7 @@ package mindustry.annotations.remote; import mindustry.annotations.Annotations.*; - -import javax.lang.model.element.ExecutableElement; +import mindustry.annotations.util.*; /** Class that repesents a remote method to be constructed and put into a class. */ public class MethodEntry{ @@ -10,6 +9,8 @@ public class MethodEntry{ public final String className; /** Fully qualified target method to call. */ public final String targetMethod; + /** Simple name of the generated packet class. */ + public final String packetClassName; /** Whether this method can be called on a client/server. */ public final Loc where; /** @@ -26,12 +27,13 @@ public class MethodEntry{ /** Unique method ID. */ public final int id; /** The element method associated with this entry. */ - public final ExecutableElement element; + public final Smethod element; /** The assigned packet priority. Only used in clients. */ public final PacketPriority priority; - public MethodEntry(String className, String targetMethod, Loc where, Variant target, - Loc local, boolean unreliable, boolean forward, int id, ExecutableElement element, PacketPriority priority){ + public MethodEntry(String className, String targetMethod, String packetClassName, Loc where, Variant target, + Loc local, boolean unreliable, boolean forward, int id, Smethod element, PacketPriority priority){ + this.packetClassName = packetClassName; this.className = className; this.forward = forward; this.targetMethod = targetMethod; diff --git a/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java b/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java index 916bb3f278..ceceddb8a8 100644 --- a/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java +++ b/annotations/src/main/java/mindustry/annotations/remote/RemoteProcess.java @@ -1,7 +1,7 @@ package mindustry.annotations.remote; import arc.struct.*; -import com.squareup.javapoet.*; +import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.annotations.*; import mindustry.annotations.util.*; @@ -9,7 +9,6 @@ import mindustry.annotations.util.TypeIOResolver.*; import javax.annotation.processing.*; import javax.lang.model.element.*; -import java.util.*; /** The annotation processor for generating remote method call code. */ @@ -18,106 +17,58 @@ import java.util.*; "mindustry.annotations.Annotations.TypeIOHandler" }) public class RemoteProcess extends BaseProcessor{ - /** Maximum size of each event packet. */ - public static final int maxPacketSize = 8192; - /** Warning on top of each autogenerated file. */ - public static final String autogenWarning = "Autogenerated file. Do not modify!\n"; - - /** Name of class that handles reading and invoking packets on the server. */ - private static final String readServerName = "RemoteReadServer"; - /** Name of class that handles reading and invoking packets on the client. */ - private static final String readClientName = "RemoteReadClient"; /** Simple class name of generated class name. */ - private static final String callLocation = "Call"; - - //class serializers - private ClassSerializer serializer; - //all elements with the Remote annotation - private Seq elements; - //map of all classes to generate by name - private HashMap classMap; - //list of all method entries - private Seq methods; - //list of all method entries - private Seq classes; - - { - rounds = 2; - } + public static final String callLocation = "Call"; @Override public void process(RoundEnvironment roundEnv) throws Exception{ - //round 1: find all annotations, generate *writers* - if(round == 1){ - //get serializers - serializer = TypeIOResolver.resolve(this); - //last method ID used - int lastMethodID = 0; - //find all elements with the Remote annotation - elements = methods(Remote.class); - //map of all classes to generate by name - classMap = new HashMap<>(); - //list of all method entries - methods = new Seq<>(); - //list of all method entries - classes = new Seq<>(); + //get serializers + //class serializers + ClassSerializer serializer = TypeIOResolver.resolve(this); + //last method ID used + int lastMethodID = 0; + //find all elements with the Remote annotation + //all elements with the Remote annotation + Seq elements = methods(Remote.class); + //list of all method entries + Seq methods = new Seq<>(); - Seq orderedElements = elements.copy(); - orderedElements.sort((a, b) -> -a.toString().compareTo(b.toString())); + Seq orderedElements = elements.copy(); + orderedElements.sortComparing(Selement::toString); - //create methods - for(Smethod element : orderedElements){ - Remote annotation = element.annotation(Remote.class); + //create methods + for(Smethod element : orderedElements){ + Remote annotation = element.annotation(Remote.class); - //check for static - if(!element.is(Modifier.STATIC) || !element.is(Modifier.PUBLIC)){ - err("All @Remote methods must be public and static", element); - } - - //can't generate none methods - if(annotation.targets() == Loc.none){ - err("A @Remote method's targets() cannot be equal to 'none'", element); - } - - //get and create class entry if needed - if(!classMap.containsKey(callLocation)){ - ClassEntry clas = new ClassEntry(callLocation); - classMap.put(callLocation, clas); - classes.add(clas); - } - - ClassEntry entry = classMap.get(callLocation); - - //create and add entry - MethodEntry method = new MethodEntry(entry.name, BaseProcessor.getMethodName(element.e), annotation.targets(), annotation.variants(), - annotation.called(), annotation.unreliable(), annotation.forward(), lastMethodID++, element.e, annotation.priority()); - - entry.methods.add(method); - methods.add(method); + //check for static + if(!element.is(Modifier.STATIC) || !element.is(Modifier.PUBLIC)){ + err("All @Remote methods must be public and static", element); } - //create read/write generators - RemoteWriteGenerator writegen = new RemoteWriteGenerator(serializer); + //can't generate none methods + if(annotation.targets() == Loc.none){ + err("A @Remote method's targets() cannot be equal to 'none'", element); + } - //generate the methods to invoke (write) - writegen.generateFor(classes, packageName); - }else if(round == 2){ //round 2: generate all *readers* - RemoteReadGenerator readgen = new RemoteReadGenerator(serializer); + String packetName = Strings.capitalize(element.name()) + "CallPacket"; + int[] index = {1}; - //generate server readers - readgen.generateFor(methods.select(method -> method.where.isClient), readServerName, packageName, true); - //generate client readers - readgen.generateFor(methods.select(method -> method.where.isServer), readClientName, packageName, false); + while(methods.contains(m -> m.packetClassName.equals(packetName + (index[0] == 1 ? "" : index[0])))){ + index[0] ++; + } - //create class for storing unique method hash - TypeSpec.Builder hashBuilder = TypeSpec.classBuilder("MethodHash").addModifiers(Modifier.PUBLIC); - hashBuilder.addJavadoc(autogenWarning); - hashBuilder.addField(FieldSpec.builder(int.class, "HASH", Modifier.STATIC, Modifier.PUBLIC, Modifier.FINAL) - .initializer("$1L", Arrays.hashCode(methods.map(m -> m.element).toArray())).build()); + //create and add entry + MethodEntry method = new MethodEntry( + callLocation, BaseProcessor.getMethodName(element.e), packetName + (index[0] == 1 ? "" : index[0]), + annotation.targets(), annotation.variants(), + annotation.called(), annotation.unreliable(), annotation.forward(), lastMethodID++, + element, annotation.priority() + ); - //build and write resulting hash class - TypeSpec spec = hashBuilder.build(); - JavaFile.builder(packageName, spec).build().writeTo(BaseProcessor.filer); + methods.add(method); } + + //generate the methods to invoke, as well as the packet classes + CallGenerator.generate(serializer, methods); } } diff --git a/annotations/src/main/java/mindustry/annotations/remote/RemoteReadGenerator.java b/annotations/src/main/java/mindustry/annotations/remote/RemoteReadGenerator.java deleted file mode 100644 index 3442843669..0000000000 --- a/annotations/src/main/java/mindustry/annotations/remote/RemoteReadGenerator.java +++ /dev/null @@ -1,129 +0,0 @@ -package mindustry.annotations.remote; - -import arc.struct.*; -import arc.util.io.*; -import com.squareup.javapoet.*; -import mindustry.annotations.*; -import mindustry.annotations.util.TypeIOResolver.*; - -import javax.lang.model.element.*; - -/** Generates code for reading remote invoke packets on the client and server. */ -public class RemoteReadGenerator{ - private final ClassSerializer serializers; - - /** Creates a read generator that uses the supplied serializer setup. */ - public RemoteReadGenerator(ClassSerializer serializers){ - this.serializers = serializers; - } - - /** - * Generates a class for reading remote invoke packets. - * @param entries List of methods to use. - * @param className Simple target class name. - * @param packageName Full target package name. - * @param needsPlayer Whether this read method requires a reference to the player sender. - */ - public void generateFor(Seq entries, String className, String packageName, boolean needsPlayer) throws Exception{ - - TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC); - classBuilder.addJavadoc(RemoteProcess.autogenWarning); - - //create main method builder - MethodSpec.Builder readMethod = MethodSpec.methodBuilder("readPacket") - .addModifiers(Modifier.PUBLIC, Modifier.STATIC) - .addParameter(Reads.class, "read") //buffer to read form - .addParameter(int.class, "id") //ID of method type to read - .returns(void.class); - - if(needsPlayer){ - //add player parameter - readMethod.addParameter(ClassName.get(packageName, "Player"), "player"); - } - - CodeBlock.Builder readBlock = CodeBlock.builder(); //start building block of code inside read method - boolean started = false; //whether an if() statement has been written yet - - for(MethodEntry entry : entries){ - //write if check for this entry ID - if(!started){ - started = true; - readBlock.beginControlFlow("if(id == " + entry.id + ")"); - }else{ - readBlock.nextControlFlow("else if(id == " + entry.id + ")"); - } - - readBlock.beginControlFlow("try"); - - //concatenated list of variable names for method invocation - StringBuilder varResult = new StringBuilder(); - - //go through each parameter - for(int i = 0; i < entry.element.getParameters().size(); i++){ - VariableElement var = entry.element.getParameters().get(i); - - if(!needsPlayer || i != 0){ //if client, skip first parameter since it's always of type player and doesn't need to be read - //full type name of parameter - String typeName = var.asType().toString(); - //name of parameter - String varName = var.getSimpleName().toString(); - //captialized version of type name for reading primitives - String pname = typeName.equals("boolean") ? "bool" : typeName.charAt(0) + ""; - - //write primitives automatically - if(BaseProcessor.isPrimitive(typeName)){ - readBlock.addStatement("$L $L = read.$L()", typeName, varName, pname); - }else{ - //else, try and find a serializer - String ser = serializers.readers.get(typeName.replace("mindustry.gen.", ""), SerializerResolver.locate(entry.element, var.asType(), false)); - - if(ser == null){ //make sure a serializer exists! - BaseProcessor.err("No read method to read class type '" + typeName + "' in method " + entry.targetMethod + "; " + serializers.readers, var); - return; - } - - //add statement for reading it - readBlock.addStatement(typeName + " " + varName + " = " + ser + "(read)"); - } - - //append variable name to string builder - varResult.append(var.getSimpleName()); - if(i != entry.element.getParameters().size() - 1) varResult.append(", "); - }else{ - varResult.append("player"); - if(i != entry.element.getParameters().size() - 1) varResult.append(", "); - } - } - - //execute the relevant method before the forward - //if it throws a ValidateException, the method won't be forwarded - readBlock.addStatement("$N." + entry.element.getSimpleName() + "(" + varResult.toString() + ")", ((TypeElement)entry.element.getEnclosingElement()).getQualifiedName().toString()); - - //call forwarded method, don't forward on the client reader - if(entry.forward && entry.where.isServer && needsPlayer){ - //call forwarded method - readBlock.addStatement(packageName + "." + entry.className + "." + entry.element.getSimpleName() + - "__forward(player.con" + (varResult.length() == 0 ? "" : ", ") + varResult.toString() + ")"); - } - - readBlock.nextControlFlow("catch (java.lang.Exception e)"); - readBlock.addStatement("throw new java.lang.RuntimeException(\"Failed to read remote method '" + entry.element.getSimpleName() + "'!\", e)"); - readBlock.endControlFlow(); - } - - //end control flow if necessary - if(started){ - readBlock.nextControlFlow("else"); - readBlock.addStatement("throw new $1N(\"Invalid read method ID: \" + id + \"\")", RuntimeException.class.getName()); //handle invalid method IDs - readBlock.endControlFlow(); - } - - //add block and method to class - readMethod.addCode(readBlock.build()); - classBuilder.addMethod(readMethod.build()); - - //build and write resulting class - TypeSpec spec = classBuilder.build(); - JavaFile.builder(packageName, spec).build().writeTo(BaseProcessor.filer); - } -} diff --git a/annotations/src/main/java/mindustry/annotations/remote/RemoteWriteGenerator.java b/annotations/src/main/java/mindustry/annotations/remote/RemoteWriteGenerator.java deleted file mode 100644 index 606fe513bd..0000000000 --- a/annotations/src/main/java/mindustry/annotations/remote/RemoteWriteGenerator.java +++ /dev/null @@ -1,228 +0,0 @@ -package mindustry.annotations.remote; - -import arc.struct.*; -import arc.util.io.*; -import com.squareup.javapoet.*; -import mindustry.annotations.Annotations.*; -import mindustry.annotations.*; -import mindustry.annotations.util.TypeIOResolver.*; - -import javax.lang.model.element.*; -import java.io.*; - -/** Generates code for writing remote invoke packets on the client and server. */ -public class RemoteWriteGenerator{ - private final ClassSerializer serializers; - - /** Creates a write generator that uses the supplied serializer setup. */ - public RemoteWriteGenerator(ClassSerializer serializers){ - this.serializers = serializers; - } - - /** Generates all classes in this list. */ - public void generateFor(Seq entries, String packageName) throws IOException{ - - for(ClassEntry entry : entries){ - //create builder - TypeSpec.Builder classBuilder = TypeSpec.classBuilder(entry.name).addModifiers(Modifier.PUBLIC); - classBuilder.addJavadoc(RemoteProcess.autogenWarning); - - //add temporary write buffer - classBuilder.addField(FieldSpec.builder(ReusableByteOutStream.class, "OUT", Modifier.STATIC, Modifier.PRIVATE, Modifier.FINAL) - .initializer("new ReusableByteOutStream($L)", RemoteProcess.maxPacketSize).build()); - - //add writer for that buffer - classBuilder.addField(FieldSpec.builder(Writes.class, "WRITE", Modifier.STATIC, Modifier.PRIVATE, Modifier.FINAL) - .initializer("new Writes(new $T(OUT))", DataOutputStream.class).build()); - - //go through each method entry in this class - for(MethodEntry methodEntry : entry.methods){ - //write the 'send event to all players' variant: always happens for clients, but only happens if 'all' is enabled on the server method - if(methodEntry.where.isClient || methodEntry.target.isAll){ - writeMethodVariant(classBuilder, methodEntry, true, false); - } - - //write the 'send event to one player' variant, which is only applicable on the server - if(methodEntry.where.isServer && methodEntry.target.isOne){ - writeMethodVariant(classBuilder, methodEntry, false, false); - } - - //write the forwarded method version - if(methodEntry.where.isServer && methodEntry.forward){ - writeMethodVariant(classBuilder, methodEntry, true, true); - } - } - - //build and write resulting class - TypeSpec spec = classBuilder.build(); - JavaFile.builder(packageName, spec).build().writeTo(BaseProcessor.filer); - } - } - - /** Creates a specific variant for a method entry. */ - private void writeMethodVariant(TypeSpec.Builder classBuilder, MethodEntry methodEntry, boolean toAll, boolean forwarded){ - ExecutableElement elem = methodEntry.element; - - //create builder - MethodSpec.Builder method = MethodSpec.methodBuilder(elem.getSimpleName().toString() + (forwarded ? "__forward" : "")) //add except suffix when forwarding - .addModifiers(Modifier.STATIC) - .returns(void.class); - - //forwarded methods aren't intended for use, and are not public - if(!forwarded){ - method.addModifiers(Modifier.PUBLIC); - } - - //validate client methods to make sure - if(methodEntry.where.isClient){ - if(elem.getParameters().isEmpty()){ - BaseProcessor.err("Client invoke methods must have a first parameter of type Player", elem); - return; - } - - if(!elem.getParameters().get(0).asType().toString().contains("Player")){ - BaseProcessor.err("Client invoke methods should have a first parameter of type Player", elem); - return; - } - } - - //if toAll is false, it's a 'send to one player' variant, so add the player as a parameter - if(!toAll){ - method.addParameter(ClassName.bestGuess("mindustry.net.NetConnection"), "playerConnection"); - } - - //add sender to ignore - if(forwarded){ - method.addParameter(ClassName.bestGuess("mindustry.net.NetConnection"), "exceptConnection"); - } - - //call local method if applicable, shouldn't happen when forwarding method as that already happens by default - if(!forwarded && methodEntry.local != Loc.none){ - //add in local checks - if(methodEntry.local != Loc.both){ - method.beginControlFlow("if(" + getCheckString(methodEntry.local) + " || !mindustry.Vars.net.active())"); - } - - //concatenate parameters - int index = 0; - StringBuilder results = new StringBuilder(); - for(VariableElement var : elem.getParameters()){ - //special case: calling local-only methods uses the local player - if(index == 0 && methodEntry.where == Loc.client){ - results.append("mindustry.Vars.player"); - }else{ - results.append(var.getSimpleName()); - } - if(index != elem.getParameters().size() - 1) results.append(", "); - index++; - } - - //add the statement to call it - method.addStatement("$N." + elem.getSimpleName() + "(" + results.toString() + ")", - ((TypeElement)elem.getEnclosingElement()).getQualifiedName().toString()); - - if(methodEntry.local != Loc.both){ - method.endControlFlow(); - } - } - - //start control flow to check if it's actually client/server so no netcode is called - method.beginControlFlow("if(" + getCheckString(methodEntry.where) + ")"); - - //add statement to create packet from pool - method.addStatement("$1N packet = $2N.obtain($1N.class, $1N::new)", "mindustry.net.Packets.InvokePacket", "arc.util.pooling.Pools"); - //assign priority - method.addStatement("packet.priority = (byte)" + methodEntry.priority.ordinal()); - //assign method ID - method.addStatement("packet.type = (byte)" + methodEntry.id); - //reset stream - method.addStatement("OUT.reset()"); - - method.addTypeVariables(Seq.with(elem.getTypeParameters()).map(BaseProcessor::getTVN)); - - for(int i = 0; i < elem.getParameters().size(); i++){ - //first argument is skipped as it is always the player caller - if((!methodEntry.where.isServer/* || methodEntry.mode == Loc.both*/) && i == 0){ - continue; - } - - VariableElement var = elem.getParameters().get(i); - - try{ - //add parameter to method - method.addParameter(TypeName.get(var.asType()), var.getSimpleName().toString()); - }catch(Throwable t){ - throw new RuntimeException("Error parsing method " + methodEntry.targetMethod); - } - - //name of parameter - String varName = var.getSimpleName().toString(); - //name of parameter type - String typeName = var.asType().toString(); - //captialized version of type name for writing primitives - String capName = typeName.equals("byte") ? "" : Character.toUpperCase(typeName.charAt(0)) + typeName.substring(1); - //special case: method can be called from anywhere to anywhere - //thus, only write the player when the SERVER is writing data, since the client is the only one who reads it - boolean writePlayerSkipCheck = methodEntry.where == Loc.both && i == 0; - - if(writePlayerSkipCheck){ //write begin check - method.beginControlFlow("if(mindustry.Vars.net.server())"); - } - - if(BaseProcessor.isPrimitive(typeName)){ //check if it's a primitive, and if so write it - method.addStatement("WRITE.$L($L)", typeName.equals("boolean") ? "bool" : typeName.charAt(0) + "", varName); - }else{ - //else, try and find a serializer - String ser = serializers.writers.get(typeName.replace("mindustry.gen.", ""), SerializerResolver.locate(elem, var.asType(), true)); - - if(ser == null){ //make sure a serializer exists! - BaseProcessor.err("No @WriteClass method to write class type: '" + typeName + "'", var); - return; - } - - //add statement for writing it - method.addStatement(ser + "(WRITE, " + varName + ")"); - } - - if(writePlayerSkipCheck){ //write end check - method.endControlFlow(); - } - } - - //assign packet bytes - method.addStatement("packet.bytes = OUT.getBytes()"); - //assign packet length - method.addStatement("packet.length = OUT.size()"); - - String sendString; - - if(forwarded){ //forward packet - if(!methodEntry.local.isClient){ //if the client doesn't get it called locally, forward it back after validation - sendString = "mindustry.Vars.net.send("; - }else{ - sendString = "mindustry.Vars.net.sendExcept(exceptConnection, "; - } - }else if(toAll){ //send to all players / to server - sendString = "mindustry.Vars.net.send("; - }else{ //send to specific client from server - sendString = "playerConnection.send("; - } - - //send the actual packet - method.addStatement(sendString + "packet, " + - (methodEntry.unreliable ? "mindustry.net.Net.SendMode.udp" : "mindustry.net.Net.SendMode.tcp") + ")"); - - - //end check for server/client - method.endControlFlow(); - - //add method to class, finally - classBuilder.addMethod(method.build()); - } - - private String getCheckString(Loc loc){ - return loc.isClient && loc.isServer ? "mindustry.Vars.net.server() || mindustry.Vars.net.client()" : - loc.isClient ? "mindustry.Vars.net.client()" : - loc.isServer ? "mindustry.Vars.net.server()" : "false"; - } -} diff --git a/annotations/src/main/java/mindustry/annotations/util/Svar.java b/annotations/src/main/java/mindustry/annotations/util/Svar.java index 2b74ced930..b160ec6ca0 100644 --- a/annotations/src/main/java/mindustry/annotations/util/Svar.java +++ b/annotations/src/main/java/mindustry/annotations/util/Svar.java @@ -1,7 +1,6 @@ package mindustry.annotations.util; import com.sun.source.tree.*; -import com.sun.tools.javac.tree.JCTree.*; import mindustry.annotations.*; import javax.lang.model.element.*; @@ -16,10 +15,6 @@ public class Svar extends Selement{ return up().asType().toString() + "#" + super.toString().replace("mindustry.gen.", ""); } - public JCVariableDecl jtree(){ - return (JCVariableDecl)BaseProcessor.elementu.getTree(e); - } - public Stype enclosingType(){ return new Stype((TypeElement)up()); } diff --git a/annotations/src/main/resources/classids.properties b/annotations/src/main/resources/classids.properties index 293373a9df..2dc5b73284 100644 --- a/annotations/src/main/resources/classids.properties +++ b/annotations/src/main/resources/classids.properties @@ -22,6 +22,7 @@ mindustry.entities.comp.PosTeamDef=28 mindustry.entities.comp.PuddleComp=13 mindustry.type.Weather.WeatherStateComp=14 mindustry.world.blocks.campaign.LaunchPad.LaunchPayloadComp=15 +mindustry.world.blocks.campaign.PayloadLaunchPad.LargeLaunchPayloadComp=34 mindustry.world.blocks.defense.ForceProjector.ForceDrawComp=22 mono=16 nova=17 diff --git a/annotations/src/main/resources/revisions/LargeLaunchPayloadComp/0.json b/annotations/src/main/resources/revisions/LargeLaunchPayloadComp/0.json new file mode 100644 index 0000000000..677d3f93da --- /dev/null +++ b/annotations/src/main/resources/revisions/LargeLaunchPayloadComp/0.json @@ -0,0 +1 @@ +{fields:[{name:lifetime,type:float},{name:payload,type:mindustry.world.blocks.payloads.Payload},{name:team,type:mindustry.game.Team},{name:time,type:float},{name:x,type:float},{name:y,type:float}]} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0017d58727..b3e911106a 100644 --- a/build.gradle +++ b/build.gradle @@ -29,18 +29,18 @@ plugins{ } allprojects{ - apply plugin: 'maven' + apply plugin: 'maven-publish' version = 'release' group = 'com.github.Anuken' ext{ - versionNumber = '6' - if(!project.hasProperty("versionModifier")) versionModifier = 'release' + versionNumber = '7' + if(!project.hasProperty("versionModifier")) versionModifier = 'pre-alpha' if(!project.hasProperty("versionType")) versionType = 'official' appName = 'Mindustry' - steamworksVersion = '891ed912791e01fe9ee6237a6497e5212b85c256' - rhinoVersion = '378626d8abc552bba57864358358045d2f2dbe9b' + steamworksVersion = '0b86023401880bb5e586bc404bedbaae9b1f1c94' + rhinoVersion = '099aed6c82f8094b3ba39a273b8d2ba7bdcc6443' loadVersionProps = { return new Properties().with{p -> p.load(file('../core/assets/version.properties').newReader()); return p } @@ -88,13 +88,11 @@ allprojects{ } hasSprites = { - return new File(rootDir, "core/assets/sprites/sprites.atlas").exists() + return new File(rootDir, "core/assets/sprites/sprites.aatls").exists() } getModifierString = { - if(versionModifier != "release"){ - return "[${versionModifier.toUpperCase()}]" - } + if(versionModifier != "release") return "[${versionModifier.toUpperCase()}]" return "" } @@ -112,8 +110,7 @@ allprojects{ def v = System.getenv("ANDROID_HOME") if(v != null) return v //rootDir is null here, amazing. brilliant. - def file = new File("local.properties") - if(!file.exists()) file = new File("../local.properties") + def file = new File(rootDir, "local.properties") def props = new Properties().with{p -> p.load(file.newReader()); return p } return props.get("sdk.dir") } @@ -200,10 +197,20 @@ allprojects{ tasks.withType(JavaCompile){ targetCompatibility = 8 - sourceCompatibility = 14 + //TODO fix dynamically, this is a hack + if(System.getProperty("user.name") == "anuke"){ + sourceCompatibility = JavaVersion.VERSION_15 + }else{ + sourceCompatibility = JavaVersion.VERSION_14 + } options.encoding = "UTF-8" options.compilerArgs += ["-Xlint:deprecation"] dependsOn clearCache + + options.forkOptions.jvmArgs.addAll([ + '--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED' + ]) } } @@ -211,15 +218,14 @@ configure(project(":annotations")){ tasks.withType(JavaCompile){ targetCompatibility = 8 sourceCompatibility = 8 + options.fork = true } } //compile with java 8 compatibility for everything except the annotation project configure(subprojects - project(":annotations")){ tasks.withType(JavaCompile){ - if(JavaVersion.current() != JavaVersion.VERSION_1_8){ - options.compilerArgs.addAll(['--release', '8', '--enable-preview']) - } + options.compilerArgs.addAll(['--release', '8', '--enable-preview']) doFirst{ options.compilerArgs = options.compilerArgs.findAll{it != '--enable-preview' } @@ -242,9 +248,9 @@ project(":desktop"){ dependencies{ implementation project(":core") + implementation arcModule("extensions:discord") implementation arcModule("natives:natives-desktop") implementation arcModule("natives:natives-freetype-desktop") - implementation 'com.github.MinnDevelopment:java-discord-rpc:v2.0.1' if(debugged()) implementation project(":debug") @@ -360,14 +366,16 @@ project(":core"){ dependencies{ compileJava.dependsOn(preGen) - api "org.lz4:lz4-java:1.4.1" + api "org.lz4:lz4-java:1.7.1" api arcModule("arc-core") + api arcModule("extensions:flabel") api arcModule("extensions:freetype") api arcModule("extensions:g3d") api arcModule("extensions:fx") api arcModule("extensions:arcnet") api "com.github.Anuken:rhino:$rhinoVersion" if(localArc() && debugged()) api arcModule("extensions:recorder") + if(localArc()) api arcModule(":extensions:packer") annotationProcessor 'com.github.Anuken:jabel:34e4c172e65b3928cd9eabe1993654ea79c409cd' compileOnly project(":annotations") @@ -425,7 +433,7 @@ project(":tests"){ test{ useJUnitPlatform() workingDir = new File("../core/assets") - testLogging { + testLogging{ exceptionFormat = 'full' showStandardStreams = true } @@ -453,6 +461,21 @@ project(":annotations"){ } } +configure([":core", ":desktop", ":server", ":tools"].collect{project(it)}){ + java{ + withJavadocJar() + withSourcesJar() + } + + publishing{ + publications{ + maven(MavenPublication){ + from components.java + } + } + } +} + task deployAll{ task cleanDeployOutput{ doFirst{ diff --git a/core/assets-raw/sprites/blocks/campaign/launch-pad-large.png b/core/assets-raw/sprites/blocks/campaign/launch-pad-large.png deleted file mode 100644 index 5eee359191..0000000000 Binary files a/core/assets-raw/sprites/blocks/campaign/launch-pad-large.png and /dev/null differ diff --git a/core/assets-raw/sprites/blocks/campaign/payload-launch-pad-light.png b/core/assets-raw/sprites/blocks/campaign/payload-launch-pad-light.png new file mode 100644 index 0000000000..bf9ab10863 Binary files /dev/null and b/core/assets-raw/sprites/blocks/campaign/payload-launch-pad-light.png differ diff --git a/core/assets-raw/sprites/blocks/campaign/payload-launch-pad.png b/core/assets-raw/sprites/blocks/campaign/payload-launch-pad.png new file mode 100644 index 0000000000..ec96499d67 Binary files /dev/null and b/core/assets-raw/sprites/blocks/campaign/payload-launch-pad.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-0.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-0.png new file mode 100644 index 0000000000..329fc2f756 Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-0.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-1.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-1.png new file mode 100644 index 0000000000..6e793c7cac Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-1.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-2.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-2.png new file mode 100644 index 0000000000..a2dd7b5e0c Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-2.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-3.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-3.png new file mode 100644 index 0000000000..a2dd7b5e0c Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-3.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-4.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-4.png new file mode 100644 index 0000000000..a2dd7b5e0c Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom-4.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom.png new file mode 100644 index 0000000000..04cecf2c96 Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bottom.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-arrow.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-arrow.png new file mode 100644 index 0000000000..f47060e98c Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-arrow.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge-bottom.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge-bottom.png new file mode 100644 index 0000000000..16256794a0 Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge-bottom.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge.png new file mode 100644 index 0000000000..ff853cecf6 Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-bridge.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-dir.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-dir.png new file mode 100644 index 0000000000..8349ac6837 Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge-dir.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge.png new file mode 100644 index 0000000000..428785c66c Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-bridge.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-router-top.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-router-top.png new file mode 100644 index 0000000000..f93d8639c9 Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-router-top.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-router.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-router.png new file mode 100644 index 0000000000..428785c66c Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-router.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-0.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-0.png new file mode 100644 index 0000000000..04cd2abceb Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-0.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-1.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-1.png new file mode 100644 index 0000000000..2e1b8c091e Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-1.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-2.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-2.png new file mode 100644 index 0000000000..10fde0b35b Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-2.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-3.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-3.png new file mode 100644 index 0000000000..491efae7c9 Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-3.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-4.png b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-4.png new file mode 100644 index 0000000000..adad9a7fc6 Binary files /dev/null and b/core/assets-raw/sprites/blocks/distribution/ducts/duct-top-4.png differ diff --git a/core/assets-raw/sprites/blocks/drills/oil-extractor-liquid.png b/core/assets-raw/sprites/blocks/drills/oil-extractor-liquid.png index 9a058e3fc9..6639f0889b 100644 Binary files a/core/assets-raw/sprites/blocks/drills/oil-extractor-liquid.png and b/core/assets-raw/sprites/blocks/drills/oil-extractor-liquid.png differ diff --git a/core/assets-raw/sprites/blocks/environment/char1.png b/core/assets-raw/sprites/blocks/environment/char1.png index d8a1dae14f..e7e7f42580 100644 Binary files a/core/assets-raw/sprites/blocks/environment/char1.png and b/core/assets-raw/sprites/blocks/environment/char1.png differ diff --git a/core/assets-raw/sprites/blocks/environment/char2.png b/core/assets-raw/sprites/blocks/environment/char2.png index c37787ba4f..c418289e48 100644 Binary files a/core/assets-raw/sprites/blocks/environment/char2.png and b/core/assets-raw/sprites/blocks/environment/char2.png differ diff --git a/core/assets-raw/sprites/blocks/environment/char3.png b/core/assets-raw/sprites/blocks/environment/char3.png index c45e69823a..cfb54e0ea0 100644 Binary files a/core/assets-raw/sprites/blocks/environment/char3.png and b/core/assets-raw/sprites/blocks/environment/char3.png differ diff --git a/core/assets-raw/sprites/blocks/environment/dune-wall-large.png b/core/assets-raw/sprites/blocks/environment/dune-wall-large.png index 23082592c7..9966c564e9 100644 Binary files a/core/assets-raw/sprites/blocks/environment/dune-wall-large.png and b/core/assets-raw/sprites/blocks/environment/dune-wall-large.png differ diff --git a/core/assets-raw/sprites/blocks/environment/dune-wall1.png b/core/assets-raw/sprites/blocks/environment/dune-wall1.png index 64d13b3f24..fbd9cb8090 100644 Binary files a/core/assets-raw/sprites/blocks/environment/dune-wall1.png and b/core/assets-raw/sprites/blocks/environment/dune-wall1.png differ diff --git a/core/assets-raw/sprites/blocks/environment/edgier.png b/core/assets-raw/sprites/blocks/environment/edgier.png deleted file mode 100644 index 0e4e66c84c..0000000000 Binary files a/core/assets-raw/sprites/blocks/environment/edgier.png and /dev/null differ diff --git a/core/assets-raw/sprites/blocks/environment/env-error.png b/core/assets-raw/sprites/blocks/environment/env-error.png new file mode 100644 index 0000000000..b72971ab8a Binary files /dev/null and b/core/assets-raw/sprites/blocks/environment/env-error.png differ diff --git a/core/assets-raw/sprites/blocks/environment/metal-floor.png b/core/assets-raw/sprites/blocks/environment/metal-floor.png index f60eff075e..a4fe16e944 100644 Binary files a/core/assets-raw/sprites/blocks/environment/metal-floor.png and b/core/assets-raw/sprites/blocks/environment/metal-floor.png differ diff --git a/core/assets-raw/sprites/blocks/environment/spawn.png b/core/assets-raw/sprites/blocks/environment/spawn.png index dc1a4c30a7..2267baba5e 100644 Binary files a/core/assets-raw/sprites/blocks/environment/spawn.png and b/core/assets-raw/sprites/blocks/environment/spawn.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire0.png b/core/assets-raw/sprites/blocks/fire/fire0.png new file mode 100644 index 0000000000..d9e1db18e6 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire0.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire1.png b/core/assets-raw/sprites/blocks/fire/fire1.png new file mode 100644 index 0000000000..d3026cedd2 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire1.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire10.png b/core/assets-raw/sprites/blocks/fire/fire10.png new file mode 100644 index 0000000000..b55606ede5 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire10.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire11.png b/core/assets-raw/sprites/blocks/fire/fire11.png new file mode 100644 index 0000000000..4602c384ed Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire11.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire12.png b/core/assets-raw/sprites/blocks/fire/fire12.png new file mode 100644 index 0000000000..3860c2ce1d Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire12.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire13.png b/core/assets-raw/sprites/blocks/fire/fire13.png new file mode 100644 index 0000000000..27e48033ad Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire13.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire14.png b/core/assets-raw/sprites/blocks/fire/fire14.png new file mode 100644 index 0000000000..0cc1071961 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire14.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire15.png b/core/assets-raw/sprites/blocks/fire/fire15.png new file mode 100644 index 0000000000..717130d434 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire15.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire16.png b/core/assets-raw/sprites/blocks/fire/fire16.png new file mode 100644 index 0000000000..7e61b28a69 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire16.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire17.png b/core/assets-raw/sprites/blocks/fire/fire17.png new file mode 100644 index 0000000000..5de74405a2 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire17.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire18.png b/core/assets-raw/sprites/blocks/fire/fire18.png new file mode 100644 index 0000000000..cdf83dfab6 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire18.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire19.png b/core/assets-raw/sprites/blocks/fire/fire19.png new file mode 100644 index 0000000000..e79a1538e9 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire19.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire2.png b/core/assets-raw/sprites/blocks/fire/fire2.png new file mode 100644 index 0000000000..0b6ffb4c50 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire2.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire20.png b/core/assets-raw/sprites/blocks/fire/fire20.png new file mode 100644 index 0000000000..5d974a427b Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire20.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire21.png b/core/assets-raw/sprites/blocks/fire/fire21.png new file mode 100644 index 0000000000..409d597aa7 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire21.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire22.png b/core/assets-raw/sprites/blocks/fire/fire22.png new file mode 100644 index 0000000000..5bbd4b93d7 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire22.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire23.png b/core/assets-raw/sprites/blocks/fire/fire23.png new file mode 100644 index 0000000000..85ede17d2f Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire23.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire24.png b/core/assets-raw/sprites/blocks/fire/fire24.png new file mode 100644 index 0000000000..5544ee65e8 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire24.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire25.png b/core/assets-raw/sprites/blocks/fire/fire25.png new file mode 100644 index 0000000000..989beef8e7 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire25.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire26.png b/core/assets-raw/sprites/blocks/fire/fire26.png new file mode 100644 index 0000000000..b0eb8eff34 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire26.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire27.png b/core/assets-raw/sprites/blocks/fire/fire27.png new file mode 100644 index 0000000000..e76de8c346 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire27.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire28.png b/core/assets-raw/sprites/blocks/fire/fire28.png new file mode 100644 index 0000000000..464e0c3797 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire28.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire29.png b/core/assets-raw/sprites/blocks/fire/fire29.png new file mode 100644 index 0000000000..2342d9a35a Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire29.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire3.png b/core/assets-raw/sprites/blocks/fire/fire3.png new file mode 100644 index 0000000000..0d99250e41 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire3.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire30.png b/core/assets-raw/sprites/blocks/fire/fire30.png new file mode 100644 index 0000000000..88cb4a0bc2 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire30.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire31.png b/core/assets-raw/sprites/blocks/fire/fire31.png new file mode 100644 index 0000000000..ea83d1566b Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire31.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire32.png b/core/assets-raw/sprites/blocks/fire/fire32.png new file mode 100644 index 0000000000..e69ca2dd68 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire32.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire33.png b/core/assets-raw/sprites/blocks/fire/fire33.png new file mode 100644 index 0000000000..a76b48aed2 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire33.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire34.png b/core/assets-raw/sprites/blocks/fire/fire34.png new file mode 100644 index 0000000000..57544d4292 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire34.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire35.png b/core/assets-raw/sprites/blocks/fire/fire35.png new file mode 100644 index 0000000000..4a9f15335d Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire35.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire36.png b/core/assets-raw/sprites/blocks/fire/fire36.png new file mode 100644 index 0000000000..bec029e19a Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire36.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire37.png b/core/assets-raw/sprites/blocks/fire/fire37.png new file mode 100644 index 0000000000..c94dd0d01e Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire37.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire38.png b/core/assets-raw/sprites/blocks/fire/fire38.png new file mode 100644 index 0000000000..c9f23580f9 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire38.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire39.png b/core/assets-raw/sprites/blocks/fire/fire39.png new file mode 100644 index 0000000000..fe133c0794 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire39.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire4.png b/core/assets-raw/sprites/blocks/fire/fire4.png new file mode 100644 index 0000000000..f0b83d2d55 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire4.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire5.png b/core/assets-raw/sprites/blocks/fire/fire5.png new file mode 100644 index 0000000000..fe58e31e52 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire5.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire6.png b/core/assets-raw/sprites/blocks/fire/fire6.png new file mode 100644 index 0000000000..0e2a45ff80 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire6.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire7.png b/core/assets-raw/sprites/blocks/fire/fire7.png new file mode 100644 index 0000000000..b74acf05a5 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire7.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire8.png b/core/assets-raw/sprites/blocks/fire/fire8.png new file mode 100644 index 0000000000..461e0cbc76 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire8.png differ diff --git a/core/assets-raw/sprites/blocks/fire/fire9.png b/core/assets-raw/sprites/blocks/fire/fire9.png new file mode 100644 index 0000000000..f306ed27e7 Binary files /dev/null and b/core/assets-raw/sprites/blocks/fire/fire9.png differ diff --git a/core/assets-raw/sprites/blocks/liquid/conduit-bottom.png b/core/assets-raw/sprites/blocks/liquid/conduit-bottom.png index f2f7dfdd99..04cecf2c96 100644 Binary files a/core/assets-raw/sprites/blocks/liquid/conduit-bottom.png and b/core/assets-raw/sprites/blocks/liquid/conduit-bottom.png differ diff --git a/core/assets-raw/sprites/blocks/liquid/phase-conduit.png b/core/assets-raw/sprites/blocks/liquid/phase-conduit.png index ec2633c5cc..402c082bc3 100644 Binary files a/core/assets-raw/sprites/blocks/liquid/phase-conduit.png and b/core/assets-raw/sprites/blocks/liquid/phase-conduit.png differ diff --git a/core/assets-raw/sprites/blocks/liquid/plated-conduit-top-4.png b/core/assets-raw/sprites/blocks/liquid/plated-conduit-top-4.png index bf29a459a8..a131889d9f 100644 Binary files a/core/assets-raw/sprites/blocks/liquid/plated-conduit-top-4.png and b/core/assets-raw/sprites/blocks/liquid/plated-conduit-top-4.png differ diff --git a/core/assets-raw/sprites/blocks/production/block-forge.png b/core/assets-raw/sprites/blocks/payload/block-forge.png similarity index 100% rename from core/assets-raw/sprites/blocks/production/block-forge.png rename to core/assets-raw/sprites/blocks/payload/block-forge.png diff --git a/core/assets-raw/sprites/blocks/distribution/block-loader.png b/core/assets-raw/sprites/blocks/payload/block-loader.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/block-loader.png rename to core/assets-raw/sprites/blocks/payload/block-loader.png diff --git a/core/assets-raw/sprites/blocks/distribution/block-unloader.png b/core/assets-raw/sprites/blocks/payload/block-unloader.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/block-unloader.png rename to core/assets-raw/sprites/blocks/payload/block-unloader.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-conveyor-edge.png b/core/assets-raw/sprites/blocks/payload/payload-conveyor-edge.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-conveyor-edge.png rename to core/assets-raw/sprites/blocks/payload/payload-conveyor-edge.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-conveyor-icon.png b/core/assets-raw/sprites/blocks/payload/payload-conveyor-icon.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-conveyor-icon.png rename to core/assets-raw/sprites/blocks/payload/payload-conveyor-icon.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-conveyor-top.png b/core/assets-raw/sprites/blocks/payload/payload-conveyor-top.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-conveyor-top.png rename to core/assets-raw/sprites/blocks/payload/payload-conveyor-top.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-conveyor.png b/core/assets-raw/sprites/blocks/payload/payload-conveyor.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-conveyor.png rename to core/assets-raw/sprites/blocks/payload/payload-conveyor.png diff --git a/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-base.png b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-base.png new file mode 100644 index 0000000000..22f0f40623 Binary files /dev/null and b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-base.png differ diff --git a/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-cap.png b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-cap.png new file mode 100644 index 0000000000..ed5066a871 Binary files /dev/null and b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-cap.png differ diff --git a/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-left.png b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-left.png new file mode 100644 index 0000000000..ac79f7b59b Binary files /dev/null and b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-left.png differ diff --git a/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-right.png b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-right.png new file mode 100644 index 0000000000..0bb4969d84 Binary files /dev/null and b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-right.png differ diff --git a/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-top.png b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-top.png new file mode 100644 index 0000000000..2e40b34e97 Binary files /dev/null and b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower-top.png differ diff --git a/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower.png b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower.png new file mode 100644 index 0000000000..d66a166412 Binary files /dev/null and b/core/assets-raw/sprites/blocks/payload/payload-propulsion-tower.png differ diff --git a/core/assets-raw/sprites/blocks/distribution/payload-router-edge.png b/core/assets-raw/sprites/blocks/payload/payload-router-edge.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-router-edge.png rename to core/assets-raw/sprites/blocks/payload/payload-router-edge.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-router-icon.png b/core/assets-raw/sprites/blocks/payload/payload-router-icon.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-router-icon.png rename to core/assets-raw/sprites/blocks/payload/payload-router-icon.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-router-over.png b/core/assets-raw/sprites/blocks/payload/payload-router-over.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-router-over.png rename to core/assets-raw/sprites/blocks/payload/payload-router-over.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-router-top.png b/core/assets-raw/sprites/blocks/payload/payload-router-top.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-router-top.png rename to core/assets-raw/sprites/blocks/payload/payload-router-top.png diff --git a/core/assets-raw/sprites/blocks/distribution/payload-router.png b/core/assets-raw/sprites/blocks/payload/payload-router.png similarity index 100% rename from core/assets-raw/sprites/blocks/distribution/payload-router.png rename to core/assets-raw/sprites/blocks/payload/payload-router.png diff --git a/core/assets-raw/sprites/blocks/payload/payload-source-top.png b/core/assets-raw/sprites/blocks/payload/payload-source-top.png new file mode 100644 index 0000000000..2de4c170be Binary files /dev/null and b/core/assets-raw/sprites/blocks/payload/payload-source-top.png differ diff --git a/core/assets-raw/sprites/blocks/payload/payload-source.png b/core/assets-raw/sprites/blocks/payload/payload-source.png new file mode 100644 index 0000000000..4c6b7b2070 Binary files /dev/null and b/core/assets-raw/sprites/blocks/payload/payload-source.png differ diff --git a/core/assets-raw/sprites/blocks/payload/payload-void-top.png b/core/assets-raw/sprites/blocks/payload/payload-void-top.png new file mode 100644 index 0000000000..f334d23032 Binary files /dev/null and b/core/assets-raw/sprites/blocks/payload/payload-void-top.png differ diff --git a/core/assets-raw/sprites/blocks/payload/payload-void.png b/core/assets-raw/sprites/blocks/payload/payload-void.png new file mode 100644 index 0000000000..4c6b7b2070 Binary files /dev/null and b/core/assets-raw/sprites/blocks/payload/payload-void.png differ diff --git a/core/assets-raw/sprites/blocks/storage/container.png b/core/assets-raw/sprites/blocks/storage/container.png index 959e2b6edf..038adbcd82 100644 Binary files a/core/assets-raw/sprites/blocks/storage/container.png and b/core/assets-raw/sprites/blocks/storage/container.png differ diff --git a/core/assets-raw/sprites/blocks/storage/vault.png b/core/assets-raw/sprites/blocks/storage/vault.png index 36a4ee5ea5..150f9acf24 100644 Binary files a/core/assets-raw/sprites/blocks/storage/vault.png and b/core/assets-raw/sprites/blocks/storage/vault.png differ diff --git a/core/assets-raw/sprites/blocks/units/factory-in-5.png b/core/assets-raw/sprites/blocks/units/factory-in-5.png index ccb7fbc5a3..785faba9cf 100644 Binary files a/core/assets-raw/sprites/blocks/units/factory-in-5.png and b/core/assets-raw/sprites/blocks/units/factory-in-5.png differ diff --git a/core/assets-raw/sprites/blocks/units/factory-out-5.png b/core/assets-raw/sprites/blocks/units/factory-out-5.png index 801f1ce6f9..b82842d043 100644 Binary files a/core/assets-raw/sprites/blocks/units/factory-out-5.png and b/core/assets-raw/sprites/blocks/units/factory-out-5.png differ diff --git a/core/assets-raw/sprites/blocks/units/factory-top-5.png b/core/assets-raw/sprites/blocks/units/factory-top-5.png new file mode 100644 index 0000000000..d48314ce89 Binary files /dev/null and b/core/assets-raw/sprites/blocks/units/factory-top-5.png differ diff --git a/core/assets-raw/sprites/blocks/units/repair-point.png b/core/assets-raw/sprites/blocks/units/repair-point.png index 2cadeae5cb..f3c0a07472 100644 Binary files a/core/assets-raw/sprites/blocks/units/repair-point.png and b/core/assets-raw/sprites/blocks/units/repair-point.png differ diff --git a/core/assets-raw/sprites/blocks/units/repair-turret.png b/core/assets-raw/sprites/blocks/units/repair-turret.png new file mode 100644 index 0000000000..4cc4ffc5bd Binary files /dev/null and b/core/assets-raw/sprites/blocks/units/repair-turret.png differ diff --git a/core/assets-raw/sprites/effects/circle-bullet-back.png b/core/assets-raw/sprites/effects/circle-bullet-back.png new file mode 100644 index 0000000000..e08bfeae85 Binary files /dev/null and b/core/assets-raw/sprites/effects/circle-bullet-back.png differ diff --git a/core/assets-raw/sprites/effects/circle-bullet.png b/core/assets-raw/sprites/effects/circle-bullet.png new file mode 100644 index 0000000000..3039f67c85 Binary files /dev/null and b/core/assets-raw/sprites/effects/circle-bullet.png differ diff --git a/core/assets-raw/sprites/effects/laser-top-end.png b/core/assets-raw/sprites/effects/laser-top-end.png new file mode 100644 index 0000000000..5f4b05e6a9 Binary files /dev/null and b/core/assets-raw/sprites/effects/laser-top-end.png differ diff --git a/core/assets-raw/sprites/effects/laser-top.png b/core/assets-raw/sprites/effects/laser-top.png new file mode 100644 index 0000000000..c50f6d69a6 Binary files /dev/null and b/core/assets-raw/sprites/effects/laser-top.png differ diff --git a/core/assets-raw/sprites/effects/laser-white-end.png b/core/assets-raw/sprites/effects/laser-white-end.png new file mode 100644 index 0000000000..1e6aeffcc5 Binary files /dev/null and b/core/assets-raw/sprites/effects/laser-white-end.png differ diff --git a/core/assets-raw/sprites/effects/laser-white.png b/core/assets-raw/sprites/effects/laser-white.png new file mode 100644 index 0000000000..13ac448bbe Binary files /dev/null and b/core/assets-raw/sprites/effects/laser-white.png differ diff --git a/core/assets-raw/sprites/effects/mine-bullet-back.png b/core/assets-raw/sprites/effects/mine-bullet-back.png new file mode 100644 index 0000000000..33738a0c4d Binary files /dev/null and b/core/assets-raw/sprites/effects/mine-bullet-back.png differ diff --git a/core/assets-raw/sprites/effects/mine-bullet.png b/core/assets-raw/sprites/effects/mine-bullet.png new file mode 100644 index 0000000000..9525c22bfc Binary files /dev/null and b/core/assets-raw/sprites/effects/mine-bullet.png differ diff --git a/core/assets-raw/sprites/effects/missile-large-back.png b/core/assets-raw/sprites/effects/missile-large-back.png new file mode 100644 index 0000000000..bc3d3c2ef4 Binary files /dev/null and b/core/assets-raw/sprites/effects/missile-large-back.png differ diff --git a/core/assets-raw/sprites/effects/missile-large.png b/core/assets-raw/sprites/effects/missile-large.png new file mode 100644 index 0000000000..f1353b6b2e Binary files /dev/null and b/core/assets-raw/sprites/effects/missile-large.png differ diff --git a/core/assets-raw/sprites/items/item-graphite.png b/core/assets-raw/sprites/items/item-graphite.png index 3d802be63f..a6cbb3454a 100644 Binary files a/core/assets-raw/sprites/items/item-graphite.png and b/core/assets-raw/sprites/items/item-graphite.png differ diff --git a/core/assets-raw/sprites/shapes/hcircle.png b/core/assets-raw/sprites/shapes/hcircle.png new file mode 100644 index 0000000000..5eb12e968f Binary files /dev/null and b/core/assets-raw/sprites/shapes/hcircle.png differ diff --git a/core/assets-raw/sprites/statuses/status-electrified.png b/core/assets-raw/sprites/statuses/status-electrified.png new file mode 100644 index 0000000000..2a08b271b8 Binary files /dev/null and b/core/assets-raw/sprites/statuses/status-electrified.png differ diff --git a/core/assets-raw/sprites/units/aegires-cell.png b/core/assets-raw/sprites/units/aegires-cell.png new file mode 100644 index 0000000000..f85cfc8e17 Binary files /dev/null and b/core/assets-raw/sprites/units/aegires-cell.png differ diff --git a/core/assets-raw/sprites/units/aegires.png b/core/assets-raw/sprites/units/aegires.png new file mode 100644 index 0000000000..21f7743fd5 Binary files /dev/null and b/core/assets-raw/sprites/units/aegires.png differ diff --git a/core/assets-raw/sprites/units/bryde.png b/core/assets-raw/sprites/units/bryde.png index b4a49bbeee..97990553b7 100644 Binary files a/core/assets-raw/sprites/units/bryde.png and b/core/assets-raw/sprites/units/bryde.png differ diff --git a/core/assets-raw/sprites/units/cyerce-cell.png b/core/assets-raw/sprites/units/cyerce-cell.png new file mode 100644 index 0000000000..6dba8c9285 Binary files /dev/null and b/core/assets-raw/sprites/units/cyerce-cell.png differ diff --git a/core/assets-raw/sprites/units/cyerce.png b/core/assets-raw/sprites/units/cyerce.png new file mode 100644 index 0000000000..8d60ebd2e3 Binary files /dev/null and b/core/assets-raw/sprites/units/cyerce.png differ diff --git a/core/assets-raw/sprites/units/eclipse.aseprite b/core/assets-raw/sprites/units/eclipse.aseprite deleted file mode 100644 index 51eb8f345b..0000000000 Binary files a/core/assets-raw/sprites/units/eclipse.aseprite and /dev/null differ diff --git a/core/assets-raw/sprites/units/eclipse.png b/core/assets-raw/sprites/units/eclipse.png index 1e071d6331..5d49e2132e 100644 Binary files a/core/assets-raw/sprites/units/eclipse.png and b/core/assets-raw/sprites/units/eclipse.png differ diff --git a/core/assets-raw/sprites/units/navanax-cell.png b/core/assets-raw/sprites/units/navanax-cell.png new file mode 100644 index 0000000000..79e2b847b7 Binary files /dev/null and b/core/assets-raw/sprites/units/navanax-cell.png differ diff --git a/core/assets-raw/sprites/units/navanax.png b/core/assets-raw/sprites/units/navanax.png new file mode 100644 index 0000000000..051b01e908 Binary files /dev/null and b/core/assets-raw/sprites/units/navanax.png differ diff --git a/core/assets-raw/sprites/units/oxynoe-cell.png b/core/assets-raw/sprites/units/oxynoe-cell.png new file mode 100644 index 0000000000..92120a6bd4 Binary files /dev/null and b/core/assets-raw/sprites/units/oxynoe-cell.png differ diff --git a/core/assets-raw/sprites/units/oxynoe.png b/core/assets-raw/sprites/units/oxynoe.png new file mode 100644 index 0000000000..35a4d45cbc Binary files /dev/null and b/core/assets-raw/sprites/units/oxynoe.png differ diff --git a/core/assets-raw/sprites/units/quad.png b/core/assets-raw/sprites/units/quad.png index 0ea0bc82ad..e238c06f37 100644 Binary files a/core/assets-raw/sprites/units/quad.png and b/core/assets-raw/sprites/units/quad.png differ diff --git a/core/assets-raw/sprites/units/retusa-cell.png b/core/assets-raw/sprites/units/retusa-cell.png new file mode 100644 index 0000000000..bb044aa5a3 Binary files /dev/null and b/core/assets-raw/sprites/units/retusa-cell.png differ diff --git a/core/assets-raw/sprites/units/retusa.png b/core/assets-raw/sprites/units/retusa.png new file mode 100644 index 0000000000..367e0d5921 Binary files /dev/null and b/core/assets-raw/sprites/units/retusa.png differ diff --git a/core/assets-raw/sprites/units/weapons/emp-cannon-mount-heat.png b/core/assets-raw/sprites/units/weapons/emp-cannon-mount-heat.png new file mode 100644 index 0000000000..b23f9257be Binary files /dev/null and b/core/assets-raw/sprites/units/weapons/emp-cannon-mount-heat.png differ diff --git a/core/assets-raw/sprites/units/weapons/emp-cannon-mount.png b/core/assets-raw/sprites/units/weapons/emp-cannon-mount.png new file mode 100644 index 0000000000..1fcaf470b1 Binary files /dev/null and b/core/assets-raw/sprites/units/weapons/emp-cannon-mount.png differ diff --git a/core/assets-raw/sprites/units/weapons/plasma-laser-mount-heat.png b/core/assets-raw/sprites/units/weapons/plasma-laser-mount-heat.png new file mode 100644 index 0000000000..63e3eafc53 Binary files /dev/null and b/core/assets-raw/sprites/units/weapons/plasma-laser-mount-heat.png differ diff --git a/core/assets-raw/sprites/units/weapons/plasma-laser-mount.png b/core/assets-raw/sprites/units/weapons/plasma-laser-mount.png new file mode 100644 index 0000000000..8a7ac5a27b Binary files /dev/null and b/core/assets-raw/sprites/units/weapons/plasma-laser-mount.png differ diff --git a/core/assets-raw/sprites/units/weapons/plasma-missile-mount.png b/core/assets-raw/sprites/units/weapons/plasma-missile-mount.png new file mode 100644 index 0000000000..f8c5476bdc Binary files /dev/null and b/core/assets-raw/sprites/units/weapons/plasma-missile-mount.png differ diff --git a/core/assets-raw/sprites/units/weapons/plasma-mount-weapon.png b/core/assets-raw/sprites/units/weapons/plasma-mount-weapon.png new file mode 100644 index 0000000000..57dad881de Binary files /dev/null and b/core/assets-raw/sprites/units/weapons/plasma-mount-weapon.png differ diff --git a/core/assets-raw/sprites/units/weapons/point-defense-mount.png b/core/assets-raw/sprites/units/weapons/point-defense-mount.png new file mode 100644 index 0000000000..b14acbc73b Binary files /dev/null and b/core/assets-raw/sprites/units/weapons/point-defense-mount.png differ diff --git a/core/assets-raw/sprites/units/weapons/repair-beam-weapon-center.png b/core/assets-raw/sprites/units/weapons/repair-beam-weapon-center.png new file mode 100644 index 0000000000..4f38701c30 Binary files /dev/null and b/core/assets-raw/sprites/units/weapons/repair-beam-weapon-center.png differ diff --git a/core/assets-raw/sprites/units/weapons/repair-beam-weapon.png b/core/assets-raw/sprites/units/weapons/repair-beam-weapon.png new file mode 100644 index 0000000000..373cf433ba Binary files /dev/null and b/core/assets-raw/sprites/units/weapons/repair-beam-weapon.png differ diff --git a/core/assets/bloomshaders/alpha_bloom.frag b/core/assets/bloomshaders/alpha_bloom.frag index b3475ff259..346dbfe232 100644 --- a/core/assets/bloomshaders/alpha_bloom.frag +++ b/core/assets/bloomshaders/alpha_bloom.frag @@ -7,7 +7,7 @@ varying vec2 v_texCoords; void main(){ vec4 original = texture2D(u_texture0, v_texCoords) * OriginalIntensity; - vec4 bloom = texture2D(u_texture1, v_texCoords) * BloomIntensity; - original = original * (vec4(1.0) - bloom); - gl_FragColor = original + bloom; + vec4 bloom = texture2D(u_texture1, v_texCoords) * BloomIntensity; + original = original * (vec4(1.0) - bloom); + gl_FragColor = original + bloom; } diff --git a/core/assets/bloomshaders/maskedtreshold.frag b/core/assets/bloomshaders/maskedtreshold.frag deleted file mode 100644 index ec2634ef3f..0000000000 --- a/core/assets/bloomshaders/maskedtreshold.frag +++ /dev/null @@ -1,9 +0,0 @@ -uniform lowp sampler2D u_texture0; -uniform lowp vec2 threshold; -varying vec2 v_texCoords; - -void main(){ - vec4 tex = texture2D(u_texture0, v_texCoords); - vec3 colors = (tex.rgb - threshold.r) * threshold.g * tex.a; - gl_FragColor = vec4(colors, tex.a); -} diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 0ff58a6fab..e918d798aa 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -67,6 +67,14 @@ schematic.delete.confirm = This schematic will be utterly eradicated. schematic.rename = Rename Schematic schematic.info = {0}x{1}, {2} blocks schematic.disabled = [scarlet]Schematics disabled[]\nYou are not allowed to use schematics on this [accent]map[] or [accent]server. +schematic.tags = Tags: +schematic.edittags = Edit Tags +schematic.addtag = Add Tag +schematic.texttag = Text Tag +schematic.icontag = Icon Tag +schematic.renametag = Rename Tag +schematic.tagdelconfirm = Delete this tag completely? +schematic.tagexists = That tag already exists. stats = Stats stat.wave = Waves Defeated:[accent] {0} @@ -306,7 +314,6 @@ data.exported = Data exported. data.invalid = This isn't valid game data. data.import.confirm = Importing external data will overwrite[scarlet] all[] your current game data.\n[accent]This cannot be undone![]\n\nOnce the data is imported, your game will exit immediately. quit.confirm = Are you sure you want to quit? -quit.confirm.tutorial = Are you sure you know what you're doing?\nThe tutorial can be re-taken in[accent] Settings->Game->Re-Take Tutorial.[] loading = [accent]Loading... reloading = [accent]Reloading Mods... saving = [accent]Saving... @@ -498,6 +505,7 @@ load = Load save = Save fps = FPS: {0} ping = Ping: {0}ms +tps = TPS: {0} memory = Mem: {0}mb memory2 = Mem:\n {0}mb +\n {1}mb language.restart = Restart your game for the language settings to take effect. @@ -530,7 +538,7 @@ launch.from = Launching From: [accent]{0} launch.destination = Destination: {0} configure.invalid = Amount must be a number between 0 and {0}. add = Add... -boss.health = Guardian Health +guardian = Guardian connectfail = [scarlet]Connection error:\n\n[accent]{0} error.unreachable = Server unreachable.\nIs the address spelled correctly? @@ -574,6 +582,7 @@ sector.attacked = Sector [accent]{0}[white] under attack! sector.lost = Sector [accent]{0}[white] lost! #note: the missing space in the line below is intentional sector.captured = Sector [accent]{0}[white]captured! +sector.changeicon = Change Icon threat.low = Low threat.medium = Medium @@ -626,6 +635,7 @@ status.wet.name = Wet status.muddy.name = Muddy status.melting.name = Melting status.sapped.name = Sapped +status.electrified.name = Electrified status.spore-slowed.name = Spore Slowed status.tarred.name = Tarred status.overclock.name = Overclock @@ -654,6 +664,7 @@ settings.clearcampaignsaves.confirm = Are you sure you want to clear all of your paused = [accent]< Paused > clear = Clear banned = [scarlet]Banned +unsupported.environment = [scarlet]Unsupported Environment yes = Yes no = No info.title = Info @@ -692,6 +703,7 @@ stat.memorycapacity = Memory Capacity stat.basepowergeneration = Base Power Generation stat.productiontime = Production Time stat.repairtime = Block Full Repair Time +stat.repairspeed = Repair Speed stat.weapons = Weapons stat.bullet = Bullet stat.speedincrease = Speed Increase @@ -741,10 +753,11 @@ stat.healing = Healing ability.forcefield = Force Field ability.repairfield = Repair Field -ability.statusfield = Status Field +ability.statusfield = {0} Status Field ability.unitspawn = {0} Factory ability.shieldregenfield = Shield Regen Field ability.movelightning = Movement Lightning +ability.energyfield = Energy Field: [accent]{0}[] damage ~ [accent]{1}[] blocks / [accent]{2}[] targets bar.drilltierreq = Better Drill Required bar.noresources = Missing Resources @@ -767,6 +780,7 @@ bar.power = Power bar.progress = Build Progress bar.input = Input bar.output = Output +bar.strength = [stat]{0}[lightgray]x strength units.processorcontrol = [lightgray]Processor Controlled @@ -975,6 +989,7 @@ rules.wavetimer = Wave Timer rules.waves = Waves rules.attack = Attack Mode rules.buildai = AI Building +rules.corecapture = Capture Core On Destruction rules.enemyCheat = Infinite AI (Red Team) Resources rules.blockhealthmultiplier = Block Health Multiplier rules.blockdamagemultiplier = Block Damage Multiplier @@ -1030,6 +1045,7 @@ item.blast-compound.name = Blast Compound item.pyratite.name = Pyratite item.metaglass.name = Metaglass item.scrap.name = Scrap + liquid.water.name = Water liquid.slag.name = Slag liquid.oil.name = Oil @@ -1121,6 +1137,7 @@ block.sand-water.name = Sand Water block.darksand-water.name = Dark Sand Water block.char.name = Char block.dacite.name = Dacite +block.rhyolite.name = Rhyolite block.dacite-wall.name = Dacite Wall block.dacite-boulder.name = Dacite Boulder block.ice-snow.name = Ice Snow @@ -1228,6 +1245,7 @@ block.solar-panel.name = Solar Panel block.solar-panel-large.name = Large Solar Panel block.oil-extractor.name = Oil Extractor block.repair-point.name = Repair Point +block.repair-turret.name = Repair Turret block.pulse-conduit.name = Pulse Conduit block.plated-conduit.name = Plated Conduit block.phase-conduit.name = Phase Conduit @@ -1289,9 +1307,8 @@ block.memory-cell.name = Memory Cell block.memory-bank.name = Memory Bank team.blue.name = blue -team.crux.name = red -team.sharded.name = orange -team.orange.name = orange +team.crux.name = crux +team.sharded.name = sharded team.derelict.name = derelict team.green.name = green team.purple.name = purple @@ -1312,6 +1329,7 @@ hint.placeConveyor.mobile = Conveyors move items from drills into other blocks. hint.placeTurret = Place \uf861 [accent]Turrets[] to defend your base from enemies.\n\nTurrets require ammo - in this case, \uf838copper.\nUse conveyors and drills to supply them. hint.breaking = [accent]Right-click[] and drag to break blocks. hint.breaking.mobile = Activate the \ue817 [accent]hammer[] in the bottom right and tap to break blocks.\n\nHold down your finger for a second and drag to break in a selection. +hint.blockInfo = View information of a block by selecting it in the [accent]build menu[], then selecting the [accent][[?][] button at the right. hint.research = Use the \ue875 [accent]Research[] button to research new technology. hint.research.mobile = Use the \ue875 [accent]Research[] button in the \ue88c [accent]Menu[] to research new technology. hint.unitControl = Hold [accent][[L-ctrl][] and [accent]click[] to control friendly units or turrets. @@ -1345,7 +1363,7 @@ item.graphite.description = Used in electrical components and turret ammunition. item.sand.description = Used for production of other refined materials. item.coal.description = Used for fuel and refined material production. item.coal.details = Appears to be fossilized plant matter, formed long before the seeding event. -item.titanium.description = Used in liquid transportation structures, drills and aircraft. +item.titanium.description = Used in liquid transportation structures, drills and factories. item.thorium.description = Used in durable structures and as nuclear fuel. item.scrap.description = Used in Melters and Pulverizers for refining into other materials. item.scrap.details = Leftover remnants of old structures and units. @@ -1566,7 +1584,7 @@ logic.nounitbuild = [red]Unit building logic is not allowed here. lenum.type = Type of building/unit.\ne.g. for any router, this will return [accent]@router[].\nNot a string. lenum.shoot = Shoot at a position. lenum.shootp = Shoot at a unit/building with velocity prediction. -lenum.configure = Building configuration, e.g. sorter item. +lenum.config = Building configuration, e.g. sorter item. lenum.enabled = Whether the block is enabled. laccess.color = Illuminator color. @@ -1574,6 +1592,7 @@ laccess.controller = Unit controller. If processor controlled, returns processor laccess.dead = Whether a unit/building is dead or no longer valid. laccess.controlled = Returns:\n[accent]@ctrlProcessor[] if unit controller is processor\n[accent]@ctrlPlayer[] if unit/building controller is player\n[accent]@ctrlFormation[] if unit is in formation\nOtherwise, 0. laccess.commanded = [red]Deprecated. Will be removed![]\nUse [accent]controlled[] instead. +laccess.progress = Action progress, 0 to 1.\nReturns production, turret reload or construction progress. graphicstype.clear = Fill the display with a color. graphicstype.color = Set color for next drawing operations. diff --git a/core/assets/bundles/bundle_ru.properties b/core/assets/bundles/bundle_ru.properties index 90d1731a0b..94bde81323 100644 --- a/core/assets/bundles/bundle_ru.properties +++ b/core/assets/bundles/bundle_ru.properties @@ -740,7 +740,7 @@ stat.reactive = Реактивен ability.forcefield = Силовое поле ability.repairfield = Ремонтирующее поле -ability.statusfield = Усиливающее поле +ability.statusfield = {0} Усиливающее поле ability.unitspawn = Завод единиц «{0}» ability.shieldregenfield = Поле восстановления щита ability.movelightning = Молнии при движении diff --git a/core/assets/icons/icons.properties b/core/assets/icons/icons.properties index 88c6b9ccb7..552da17570 100755 --- a/core/assets/icons/icons.properties +++ b/core/assets/icons/icons.properties @@ -1,336 +1,353 @@ -63743=spawn|block-spawn-medium -63742=deepwater|block-deepwater-medium -63741=water|block-water-medium -63740=tainted-water|block-tainted-water-medium -63739=darksand-tainted-water|block-darksand-tainted-water-medium -63738=sand-water|block-sand-water-medium -63737=darksand-water|block-darksand-water-medium -63736=tar|block-tar-medium -63735=stone|block-stone-medium -63734=craters|block-craters-medium -63733=char|block-char-medium -63732=ignarock|block-ignarock-medium -63731=hotrock|block-hotrock-medium -63730=magmarock|block-magmarock-medium -63729=sand|block-sand-medium -63728=darksand|block-darksand-medium -63726=grass|block-grass-medium -63725=salt|block-salt-medium -63724=snow|block-snow-medium -63723=ice|block-ice-medium -63722=ice-snow|block-ice-snow-medium -63721=cliffs|block-cliffs-medium -63718=rock|block-rock-medium -63717=snowrock|block-snowrock-medium -63711=spore-pine|block-spore-pine-medium -63710=snow-pine|block-snow-pine-medium -63709=pine|block-pine-medium -63708=shrubs|block-shrubs-medium -63707=white-tree-dead|block-white-tree-dead-medium -63706=white-tree|block-white-tree-medium -63705=spore-cluster|block-spore-cluster-medium -63704=shale|block-shale-medium -63702=shale-boulder|block-shale-boulder-medium -63701=sand-boulder|block-sand-boulder-medium -63700=moss|block-moss-medium -63699=spore-moss|block-spore-moss-medium -63698=metal-floor|block-metal-floor-medium -63697=metal-floor-damaged|block-metal-floor-damaged-medium -63696=metal-floor-2|block-metal-floor-2-medium -63695=metal-floor-3|block-metal-floor-3-medium -63694=metal-floor-5|block-metal-floor-5-medium -63693=dark-panel-1|block-dark-panel-1-medium -63692=dark-panel-2|block-dark-panel-2-medium -63691=dark-panel-3|block-dark-panel-3-medium -63690=dark-panel-4|block-dark-panel-4-medium -63689=dark-panel-5|block-dark-panel-5-medium -63688=dark-panel-6|block-dark-panel-6-medium -63687=dark-metal|block-dark-metal-medium -63686=pebbles|block-pebbles-medium -63685=tendrils|block-tendrils-medium -63684=ore-copper|block-ore-copper-medium -63683=ore-lead|block-ore-lead-medium -63682=ore-scrap|block-ore-scrap-medium -63681=ore-coal|block-ore-coal-medium -63680=ore-titanium|block-ore-titanium-medium -63679=ore-thorium|block-ore-thorium-medium -63678=graphite-press|block-graphite-press-medium -63677=multi-press|block-multi-press-medium -63676=silicon-smelter|block-silicon-smelter-medium -63675=kiln|block-kiln-medium -63674=plastanium-compressor|block-plastanium-compressor-medium -63673=phase-weaver|block-phase-weaver-medium -63672=alloy-smelter|block-alloy-smelter-medium -63671=cryofluid-mixer|block-cryofluid-mixer-medium -63670=blast-mixer|block-blast-mixer-medium -63669=pyratite-mixer|block-pyratite-mixer-medium -63668=melter|block-melter-medium -63667=separator|block-separator-medium -63666=spore-press|block-spore-press-medium -63665=pulverizer|block-pulverizer-medium -63664=coal-centrifuge|block-coal-centrifuge-medium -63663=incinerator|block-incinerator-medium -63662=copper-wall|block-copper-wall-medium -63661=copper-wall-large|block-copper-wall-large-medium -63660=titanium-wall|block-titanium-wall-medium -63659=titanium-wall-large|block-titanium-wall-large-medium -63658=plastanium-wall|block-plastanium-wall-medium -63657=plastanium-wall-large|block-plastanium-wall-large-medium -63656=thorium-wall|block-thorium-wall-medium -63655=thorium-wall-large|block-thorium-wall-large-medium -63654=phase-wall|block-phase-wall-medium -63653=phase-wall-large|block-phase-wall-large-medium -63652=surge-wall|block-surge-wall-medium -63651=surge-wall-large|block-surge-wall-large-medium -63650=door|block-door-medium -63649=door-large|block-door-large-medium -63648=scrap-wall|block-scrap-wall-medium -63647=scrap-wall-large|block-scrap-wall-large-medium -63646=scrap-wall-huge|block-scrap-wall-huge-medium -63645=scrap-wall-gigantic|block-scrap-wall-gigantic-medium -63644=thruster|block-thruster-medium -63643=mender|block-mender-medium -63642=mend-projector|block-mend-projector-medium -63641=overdrive-projector|block-overdrive-projector-medium -63640=force-projector|block-force-projector-medium -63639=shock-mine|block-shock-mine-medium -63638=conveyor|block-conveyor-medium -63637=titanium-conveyor|block-titanium-conveyor-medium -63636=armored-conveyor|block-armored-conveyor-medium -63635=junction|block-junction-medium -63634=bridge-conveyor|block-bridge-conveyor-medium -63633=phase-conveyor|block-phase-conveyor-medium -63632=sorter|block-sorter-medium -63631=inverted-sorter|block-inverted-sorter-medium -63630=router|block-router-medium -63629=distributor|block-distributor-medium -63628=overflow-gate|block-overflow-gate-medium -63627=mass-driver|block-mass-driver-medium -63626=mechanical-pump|block-mechanical-pump-medium -63625=rotary-pump|block-rotary-pump-medium -63624=thermal-pump|block-thermal-pump-medium -63623=conduit|block-conduit-medium -63622=pulse-conduit|block-pulse-conduit-medium -63621=plated-conduit|block-plated-conduit-medium -63620=liquid-router|block-liquid-router-medium -63619=liquid-tank|block-liquid-tank-medium -63618=liquid-junction|block-liquid-junction-medium -63617=bridge-conduit|block-bridge-conduit-medium -63616=phase-conduit|block-phase-conduit-medium -63615=power-node|block-power-node-medium -63614=power-node-large|block-power-node-large-medium -63613=surge-tower|block-surge-tower-medium -63612=diode|block-diode-medium -63611=battery|block-battery-medium -63610=battery-large|block-battery-large-medium -63609=combustion-generator|block-combustion-generator-medium -63608=thermal-generator|block-thermal-generator-medium -63607=steam-generator|block-steam-generator-medium -63606=differential-generator|block-differential-generator-medium -63605=rtg-generator|block-rtg-generator-medium -63604=solar-panel|block-solar-panel-medium -63603=solar-panel-large|block-solar-panel-large-medium -63602=thorium-reactor|block-thorium-reactor-medium -63601=impact-reactor|block-impact-reactor-medium -63600=mechanical-drill|block-mechanical-drill-medium -63599=pneumatic-drill|block-pneumatic-drill-medium -63598=laser-drill|block-laser-drill-medium -63597=blast-drill|block-blast-drill-medium -63596=water-extractor|block-water-extractor-medium -63595=cultivator|block-cultivator-medium -63594=oil-extractor|block-oil-extractor-medium -63593=core-shard|block-core-shard-medium -63592=core-foundation|block-core-foundation-medium -63591=core-nucleus|block-core-nucleus-medium -63590=vault|block-vault-medium -63589=container|block-container-medium -63588=unloader|block-unloader-medium -63587=launch-pad|block-launch-pad-medium -63586=launch-pad-large|block-launch-pad-large-medium -63585=duo|block-duo-medium -63584=scatter|block-scatter-medium -63583=scorch|block-scorch-medium -63582=hail|block-hail-medium -63581=wave|block-wave-medium -63580=lancer|block-lancer-medium -63579=arc|block-arc-medium -63578=swarmer|block-swarmer-medium -63577=salvo|block-salvo-medium -63576=fuse|block-fuse-medium -63575=ripple|block-ripple-medium -63574=cyclone|block-cyclone-medium -63573=spectre|block-spectre-medium -63572=meltdown|block-meltdown-medium -63571=draug-factory|block-draug-factory-medium -63570=spirit-factory|block-spirit-factory-medium -63569=phantom-factory|block-phantom-factory-medium -63568=command-center|block-command-center-medium -63567=wraith-factory|block-wraith-factory-medium -63566=ghoul-factory|block-ghoul-factory-medium -63565=revenant-factory|block-revenant-factory-medium -63564=dagger-factory|block-dagger-factory-medium -63563=crawler-factory|block-crawler-factory-medium -63562=titan-factory|block-titan-factory-medium -63561=fortress-factory|block-fortress-factory-medium -63560=repair-point|block-repair-point-medium -63559=dart-mech-pad|block-dart-mech-pad-medium -63558=delta-mech-pad|block-delta-mech-pad-medium -63557=tau-mech-pad|block-tau-mech-pad-medium -63556=omega-mech-pad|block-omega-mech-pad-medium -63555=javelin-ship-pad|block-javelin-ship-pad-medium -63554=trident-ship-pad|block-trident-ship-pad-medium -63553=glaive-ship-pad|block-glaive-ship-pad-medium -63552=power-source|block-power-source-medium -63551=power-void|block-power-void-medium -63550=item-source|block-item-source-medium -63549=item-void|block-item-void-medium -63548=liquid-source|block-liquid-source-medium -63547=liquid-void|block-liquid-void-medium -63546=message|block-message-medium -63545=illuminator|block-illuminator-medium -63544=copper|item-copper-icon -63543=lead|item-lead-icon -63542=metaglass|item-metaglass-icon -63541=graphite|item-graphite-icon -63540=sand|item-sand-icon -63539=coal|item-coal-icon -63538=titanium|item-titanium-icon -63537=thorium|item-thorium-icon -63536=scrap|item-scrap-icon -63535=silicon|item-silicon-icon -63534=plastanium|item-plastanium-icon -63533=phase-fabric|item-phase-fabric-icon -63532=surge-alloy|item-surge-alloy-icon -63531=spore-pod|item-spore-pod-icon -63530=blast-compound|item-blast-compound-icon -63529=pyratite|item-pyratite-icon -63528=water|liquid-water-icon -63527=slag|liquid-slag-icon -63526=oil|liquid-oil-icon -63525=cryofluid|liquid-cryofluid-icon -63524=underflow-gate|block-underflow-gate-medium -63523=dart-ship-pad|block-dart-ship-pad-medium -63522=alpha-mech-pad|block-alpha-mech-pad-medium -63521=cliff|block-cliff-medium -63520=legacy-mech-pad|block-legacy-mech-pad-medium -63519=ground-factory|block-ground-factory-medium -63518=legacy-unit-factory|block-legacy-unit-factory-medium -63517=mass-conveyor|block-mass-conveyor-medium -63516=legacy-command-center|block-legacy-command-center-medium -63515=block-forge|block-block-forge-medium -63514=block-launcher|block-block-launcher-medium -63513=plastanium-conveyor|block-plastanium-conveyor-medium +63743=spawn|block-spawn-ui +63742=deepwater|block-deepwater-ui +63741=water|block-water-ui +63740=tainted-water|block-tainted-water-ui +63739=darksand-tainted-water|block-darksand-tainted-water-ui +63738=sand-water|block-sand-water-ui +63737=darksand-water|block-darksand-water-ui +63736=tar|block-tar-ui +63735=stone|block-stone-ui +63734=craters|block-craters-ui +63733=char|block-char-ui +63732=ignarock|block-ignarock-ui +63731=hotrock|block-hotrock-ui +63730=magmarock|block-magmarock-ui +63729=sand|block-sand-ui +63728=darksand|block-darksand-ui +63726=grass|block-grass-ui +63725=salt|block-salt-ui +63724=snow|block-snow-ui +63723=ice|block-ice-ui +63722=ice-snow|block-ice-snow-ui +63721=cliffs|block-cliffs-ui +63718=rock|block-rock-ui +63717=snowrock|block-snowrock-ui +63711=spore-pine|block-spore-pine-ui +63710=snow-pine|block-snow-pine-ui +63709=pine|block-pine-ui +63708=shrubs|block-shrubs-ui +63707=white-tree-dead|block-white-tree-dead-ui +63706=white-tree|block-white-tree-ui +63705=spore-cluster|block-spore-cluster-ui +63704=shale|block-shale-ui +63702=shale-boulder|block-shale-boulder-ui +63701=sand-boulder|block-sand-boulder-ui +63700=moss|block-moss-ui +63699=spore-moss|block-spore-moss-ui +63698=metal-floor|block-metal-floor-ui +63697=metal-floor-damaged|block-metal-floor-damaged-ui +63696=metal-floor-2|block-metal-floor-2-ui +63695=metal-floor-3|block-metal-floor-3-ui +63694=metal-floor-5|block-metal-floor-5-ui +63693=dark-panel-1|block-dark-panel-1-ui +63692=dark-panel-2|block-dark-panel-2-ui +63691=dark-panel-3|block-dark-panel-3-ui +63690=dark-panel-4|block-dark-panel-4-ui +63689=dark-panel-5|block-dark-panel-5-ui +63688=dark-panel-6|block-dark-panel-6-ui +63687=dark-metal|block-dark-metal-ui +63686=pebbles|block-pebbles-ui +63685=tendrils|block-tendrils-ui +63684=ore-copper|block-ore-copper-ui +63683=ore-lead|block-ore-lead-ui +63682=ore-scrap|block-ore-scrap-ui +63681=ore-coal|block-ore-coal-ui +63680=ore-titanium|block-ore-titanium-ui +63679=ore-thorium|block-ore-thorium-ui +63678=graphite-press|block-graphite-press-ui +63677=multi-press|block-multi-press-ui +63676=silicon-smelter|block-silicon-smelter-ui +63675=kiln|block-kiln-ui +63674=plastanium-compressor|block-plastanium-compressor-ui +63673=phase-weaver|block-phase-weaver-ui +63672=alloy-smelter|block-alloy-smelter-ui +63671=cryofluid-mixer|block-cryofluid-mixer-ui +63670=blast-mixer|block-blast-mixer-ui +63669=pyratite-mixer|block-pyratite-mixer-ui +63668=melter|block-melter-ui +63667=separator|block-separator-ui +63666=spore-press|block-spore-press-ui +63665=pulverizer|block-pulverizer-ui +63664=coal-centrifuge|block-coal-centrifuge-ui +63663=incinerator|block-incinerator-ui +63662=copper-wall|block-copper-wall-ui +63661=copper-wall-large|block-copper-wall-large-ui +63660=titanium-wall|block-titanium-wall-ui +63659=titanium-wall-large|block-titanium-wall-large-ui +63658=plastanium-wall|block-plastanium-wall-ui +63657=plastanium-wall-large|block-plastanium-wall-large-ui +63656=thorium-wall|block-thorium-wall-ui +63655=thorium-wall-large|block-thorium-wall-large-ui +63654=phase-wall|block-phase-wall-ui +63653=phase-wall-large|block-phase-wall-large-ui +63652=surge-wall|block-surge-wall-ui +63651=surge-wall-large|block-surge-wall-large-ui +63650=door|block-door-ui +63649=door-large|block-door-large-ui +63648=scrap-wall|block-scrap-wall-ui +63647=scrap-wall-large|block-scrap-wall-large-ui +63646=scrap-wall-huge|block-scrap-wall-huge-ui +63645=scrap-wall-gigantic|block-scrap-wall-gigantic-ui +63644=thruster|block-thruster-ui +63643=mender|block-mender-ui +63642=mend-projector|block-mend-projector-ui +63641=overdrive-projector|block-overdrive-projector-ui +63640=force-projector|block-force-projector-ui +63639=shock-mine|block-shock-mine-ui +63638=conveyor|block-conveyor-ui +63637=titanium-conveyor|block-titanium-conveyor-ui +63636=armored-conveyor|block-armored-conveyor-ui +63635=junction|block-junction-ui +63634=bridge-conveyor|block-bridge-conveyor-ui +63633=phase-conveyor|block-phase-conveyor-ui +63632=sorter|block-sorter-ui +63631=inverted-sorter|block-inverted-sorter-ui +63630=router|block-router-ui +63629=distributor|block-distributor-ui +63628=overflow-gate|block-overflow-gate-ui +63627=mass-driver|block-mass-driver-ui +63626=mechanical-pump|block-mechanical-pump-ui +63625=rotary-pump|block-rotary-pump-ui +63624=thermal-pump|block-thermal-pump-ui +63623=conduit|block-conduit-ui +63622=pulse-conduit|block-pulse-conduit-ui +63621=plated-conduit|block-plated-conduit-ui +63620=liquid-router|block-liquid-router-ui +63619=liquid-tank|block-liquid-tank-ui +63618=liquid-junction|block-liquid-junction-ui +63617=bridge-conduit|block-bridge-conduit-ui +63616=phase-conduit|block-phase-conduit-ui +63615=power-node|block-power-node-ui +63614=power-node-large|block-power-node-large-ui +63613=surge-tower|block-surge-tower-ui +63612=diode|block-diode-ui +63611=battery|block-battery-ui +63610=battery-large|block-battery-large-ui +63609=combustion-generator|block-combustion-generator-ui +63608=thermal-generator|block-thermal-generator-ui +63607=steam-generator|block-steam-generator-ui +63606=differential-generator|block-differential-generator-ui +63605=rtg-generator|block-rtg-generator-ui +63604=solar-panel|block-solar-panel-ui +63603=solar-panel-large|block-solar-panel-large-ui +63602=thorium-reactor|block-thorium-reactor-ui +63601=impact-reactor|block-impact-reactor-ui +63600=mechanical-drill|block-mechanical-drill-ui +63599=pneumatic-drill|block-pneumatic-drill-ui +63598=laser-drill|block-laser-drill-ui +63597=blast-drill|block-blast-drill-ui +63596=water-extractor|block-water-extractor-ui +63595=cultivator|block-cultivator-ui +63594=oil-extractor|block-oil-extractor-ui +63593=core-shard|block-core-shard-ui +63592=core-foundation|block-core-foundation-ui +63591=core-nucleus|block-core-nucleus-ui +63590=vault|block-vault-ui +63589=container|block-container-ui +63588=unloader|block-unloader-ui +63587=launch-pad|block-launch-pad-ui +63586=launch-pad-large|block-launch-pad-large-ui +63585=duo|block-duo-ui +63584=scatter|block-scatter-ui +63583=scorch|block-scorch-ui +63582=hail|block-hail-ui +63581=wave|block-wave-ui +63580=lancer|block-lancer-ui +63579=arc|block-arc-ui +63578=swarmer|block-swarmer-ui +63577=salvo|block-salvo-ui +63576=fuse|block-fuse-ui +63575=ripple|block-ripple-ui +63574=cyclone|block-cyclone-ui +63573=spectre|block-spectre-ui +63572=meltdown|block-meltdown-ui +63571=draug-factory|block-draug-factory-ui +63570=spirit-factory|block-spirit-factory-ui +63569=phantom-factory|block-phantom-factory-ui +63568=command-center|block-command-center-ui +63567=wraith-factory|block-wraith-factory-ui +63566=ghoul-factory|block-ghoul-factory-ui +63565=revenant-factory|block-revenant-factory-ui +63564=dagger-factory|block-dagger-factory-ui +63563=crawler-factory|block-crawler-factory-ui +63562=titan-factory|block-titan-factory-ui +63561=fortress-factory|block-fortress-factory-ui +63560=repair-point|block-repair-point-ui +63559=dart-mech-pad|block-dart-mech-pad-ui +63558=delta-mech-pad|block-delta-mech-pad-ui +63557=tau-mech-pad|block-tau-mech-pad-ui +63556=omega-mech-pad|block-omega-mech-pad-ui +63555=javelin-ship-pad|block-javelin-ship-pad-ui +63554=trident-ship-pad|block-trident-ship-pad-ui +63553=glaive-ship-pad|block-glaive-ship-pad-ui +63552=power-source|block-power-source-ui +63551=power-void|block-power-void-ui +63550=item-source|block-item-source-ui +63549=item-void|block-item-void-ui +63548=liquid-source|block-liquid-source-ui +63547=liquid-void|block-liquid-void-ui +63546=message|block-message-ui +63545=illuminator|block-illuminator-ui +63544=copper|item-copper-ui +63543=lead|item-lead-ui +63542=metaglass|item-metaglass-ui +63541=graphite|item-graphite-ui +63540=sand|item-sand-ui +63539=coal|item-coal-ui +63538=titanium|item-titanium-ui +63537=thorium|item-thorium-ui +63536=scrap|item-scrap-ui +63535=silicon|item-silicon-ui +63534=plastanium|item-plastanium-ui +63533=phase-fabric|item-phase-fabric-ui +63532=surge-alloy|item-surge-alloy-ui +63531=spore-pod|item-spore-pod-ui +63530=blast-compound|item-blast-compound-ui +63529=pyratite|item-pyratite-ui +63528=water|liquid-water-ui +63527=slag|liquid-slag-ui +63526=oil|liquid-oil-ui +63525=cryofluid|liquid-cryofluid-ui +63524=underflow-gate|block-underflow-gate-ui +63523=dart-ship-pad|block-dart-ship-pad-ui +63522=alpha-mech-pad|block-alpha-mech-pad-ui +63521=cliff|block-cliff-ui +63520=legacy-mech-pad|block-legacy-mech-pad-ui +63519=ground-factory|block-ground-factory-ui +63518=legacy-unit-factory|block-legacy-unit-factory-ui +63517=mass-conveyor|block-mass-conveyor-ui +63516=legacy-command-center|block-legacy-command-center-ui +63515=block-forge|block-block-forge-ui +63514=block-launcher|block-block-launcher-ui +63513=plastanium-conveyor|block-plastanium-conveyor-ui 63512=crater|crater -63511=naval-factory|block-naval-factory-medium -63510=air-factory|block-air-factory-medium -63509=basic-reconstructor|block-basic-reconstructor-medium -63508=block-loader|block-block-loader-medium -63507=block-unloader|block-block-unloader-medium -63506=core-silo|block-core-silo-medium -63505=data-processor|block-data-processor-medium -63504=payload-router|block-payload-router-medium -63503=silicon-crucible|block-silicon-crucible-medium -63502=segment|block-segment-medium -63501=large-overdrive-projector|block-large-overdrive-projector-medium -63500=disassembler|block-disassembler-medium -63499=advanced-reconstructor|block-advanced-reconstructor-medium -63498=reconstructor-basis|block-reconstructor-basis-medium -63497=reconstructor-morphism|block-reconstructor-morphism-medium -63496=reconstructor-functor|block-reconstructor-functor-medium -63495=reconstructor-prime|block-reconstructor-prime-medium -63494=additive-reconstructor|block-additive-reconstructor-medium -63493=multiplicative-reconstructor|block-multiplicative-reconstructor-medium -63492=exponential-reconstructor|block-exponential-reconstructor-medium -63491=tetrative-reconstructor|block-tetrative-reconstructor-medium -63490=resupply-point|block-resupply-point-medium -63489=parallax|block-parallax-medium -63488=dagger|unit-dagger-medium -63487=mace|unit-mace-medium -63486=fortress|unit-fortress-medium -63485=nova|unit-nova-medium -63484=pulsar|unit-pulsar-medium -63483=quasar|unit-quasar-medium -63482=crawler|unit-crawler-medium -63481=atrax|unit-atrax-medium -63480=spiroct|unit-spiroct-medium -63479=arkyid|unit-arkyid-medium -63478=flare|unit-flare-medium -63477=horizon|unit-horizon-medium -63476=zenith|unit-zenith-medium -63475=antumbra|unit-antumbra-medium -63474=eclipse|unit-eclipse-medium -63473=mono|unit-mono-medium -63472=poly|unit-poly-medium -63471=mega|unit-mega-medium -63470=risse|unit-risse-medium -63469=minke|unit-minke-medium -63468=bryde|unit-bryde-medium -63467=alpha|unit-alpha-medium -63466=beta|unit-beta-medium -63465=gamma|unit-gamma-medium -63464=block|unit-block-medium -63463=risso|unit-risso-medium -63462=overdrive-dome|block-overdrive-dome-medium -63461=logic-processor|block-logic-processor-medium -63460=micro-processor|block-micro-processor-medium -63459=logic-display|block-logic-display-medium -63458=switch|block-switch-medium -63457=memory-cell|block-memory-cell-medium -63456=payload-conveyor|block-payload-conveyor-medium -63455=hyper-processor|block-hyper-processor-medium -63454=toxopid|unit-toxopid-medium -63453=vestige|unit-vestige-medium -63452=cataclyst|unit-cataclyst-medium -63451=scepter|unit-scepter-medium -63450=reign|unit-reign-medium -63449=dirt|block-dirt-medium -63447=stone-wall|block-stone-wall-medium -63446=spore-wall|block-spore-wall-medium -63445=ice-wall|block-ice-wall-medium -63444=snow-wall|block-snow-wall-medium -63443=dune-wall|block-dune-wall-medium -63442=sand-wall|block-sand-wall-medium -63441=salt-wall|block-salt-wall-medium -63440=shale-wall|block-shale-wall-medium -63439=dirt-wall|block-dirt-wall-medium -63437=basalt|block-basalt-medium -63436=dacite|block-dacite-medium -63435=boulder|block-boulder-medium -63434=snow-boulder|block-snow-boulder-medium -63433=dacite-wall|block-dacite-wall-medium -63432=dacite-boulder|block-dacite-boulder-medium -63431=large-logic-display|block-large-logic-display-medium -63430=omura|unit-omura-medium -63429=mud|block-mud-medium -63428=sei|unit-sei-medium -63427=quad|unit-quad-medium -63426=oct|unit-oct-medium -63425=vela|unit-vela-medium -63424=corvus|unit-corvus-medium -63423=memory-bank|block-memory-bank-medium -63422=foreshadow|block-foreshadow-medium -63421=tsunami|block-tsunami-medium -63420=space|block-space-medium -63419=legacy-unit-factory-air|block-legacy-unit-factory-air-medium -63418=legacy-unit-factory-ground|block-legacy-unit-factory-ground-medium -63417=interplanetary-accelerator|block-interplanetary-accelerator-medium -63416=basalt-boulder|block-basalt-boulder-medium -63415=none|status-none-icon -63414=burning|status-burning-icon -63413=freezing|status-freezing-icon -63412=unmoving|status-unmoving-icon -63411=slow|status-slow-icon -63410=wet|status-wet-icon -63409=muddy|status-muddy-icon -63408=melting|status-melting-icon -63407=sapped|status-sapped-icon -63406=spore-slowed|status-spore-slowed-icon -63405=tarred|status-tarred-icon -63404=overdrive|status-overdrive-icon -63403=overclock|status-overclock-icon -63402=shielded|status-shielded-icon -63401=boss|status-boss-icon -63400=shocked|status-shocked-icon -63399=blasted|status-blasted-icon -63398=corroded|status-corroded-icon -63397=disarmed|status-disarmed-icon +63511=naval-factory|block-naval-factory-ui +63510=air-factory|block-air-factory-ui +63509=basic-reconstructor|block-basic-reconstructor-ui +63508=block-loader|block-block-loader-ui +63507=block-unloader|block-block-unloader-ui +63506=core-silo|block-core-silo-ui +63505=data-processor|block-data-processor-ui +63504=payload-router|block-payload-router-ui +63503=silicon-crucible|block-silicon-crucible-ui +63502=segment|block-segment-ui +63501=large-overdrive-projector|block-large-overdrive-projector-ui +63500=disassembler|block-disassembler-ui +63499=advanced-reconstructor|block-advanced-reconstructor-ui +63498=reconstructor-basis|block-reconstructor-basis-ui +63497=reconstructor-morphism|block-reconstructor-morphism-ui +63496=reconstructor-functor|block-reconstructor-functor-ui +63495=reconstructor-prime|block-reconstructor-prime-ui +63494=additive-reconstructor|block-additive-reconstructor-ui +63493=multiplicative-reconstructor|block-multiplicative-reconstructor-ui +63492=exponential-reconstructor|block-exponential-reconstructor-ui +63491=tetrative-reconstructor|block-tetrative-reconstructor-ui +63490=resupply-point|block-resupply-point-ui +63489=parallax|block-parallax-ui +63488=dagger|unit-dagger-ui +63487=mace|unit-mace-ui +63486=fortress|unit-fortress-ui +63485=nova|unit-nova-ui +63484=pulsar|unit-pulsar-ui +63483=quasar|unit-quasar-ui +63482=crawler|unit-crawler-ui +63481=atrax|unit-atrax-ui +63480=spiroct|unit-spiroct-ui +63479=arkyid|unit-arkyid-ui +63478=flare|unit-flare-ui +63477=horizon|unit-horizon-ui +63476=zenith|unit-zenith-ui +63475=antumbra|unit-antumbra-ui +63474=eclipse|unit-eclipse-ui +63473=mono|unit-mono-ui +63472=poly|unit-poly-ui +63471=mega|unit-mega-ui +63470=risse|unit-risse-ui +63469=minke|unit-minke-ui +63468=bryde|unit-bryde-ui +63467=alpha|unit-alpha-ui +63466=beta|unit-beta-ui +63465=gamma|unit-gamma-ui +63464=block|unit-block-ui +63463=risso|unit-risso-ui +63462=overdrive-dome|block-overdrive-dome-ui +63461=logic-processor|block-logic-processor-ui +63460=micro-processor|block-micro-processor-ui +63459=logic-display|block-logic-display-ui +63458=switch|block-switch-ui +63457=memory-cell|block-memory-cell-ui +63456=payload-conveyor|block-payload-conveyor-ui +63455=hyper-processor|block-hyper-processor-ui +63454=toxopid|unit-toxopid-ui +63453=vestige|unit-vestige-ui +63452=cataclyst|unit-cataclyst-ui +63451=scepter|unit-scepter-ui +63450=reign|unit-reign-ui +63449=dirt|block-dirt-ui +63447=stone-wall|block-stone-wall-ui +63446=spore-wall|block-spore-wall-ui +63445=ice-wall|block-ice-wall-ui +63444=snow-wall|block-snow-wall-ui +63443=dune-wall|block-dune-wall-ui +63442=sand-wall|block-sand-wall-ui +63441=salt-wall|block-salt-wall-ui +63440=shale-wall|block-shale-wall-ui +63439=dirt-wall|block-dirt-wall-ui +63437=basalt|block-basalt-ui +63436=dacite|block-dacite-ui +63435=boulder|block-boulder-ui +63434=snow-boulder|block-snow-boulder-ui +63433=dacite-wall|block-dacite-wall-ui +63432=dacite-boulder|block-dacite-boulder-ui +63431=large-logic-display|block-large-logic-display-ui +63430=omura|unit-omura-ui +63429=mud|block-mud-ui +63428=sei|unit-sei-ui +63427=quad|unit-quad-ui +63426=oct|unit-oct-ui +63425=vela|unit-vela-ui +63424=corvus|unit-corvus-ui +63423=memory-bank|block-memory-bank-ui +63422=foreshadow|block-foreshadow-ui +63421=tsunami|block-tsunami-ui +63420=space|block-space-ui +63419=legacy-unit-factory-air|block-legacy-unit-factory-air-ui +63418=legacy-unit-factory-ground|block-legacy-unit-factory-ground-ui +63417=interplanetary-accelerator|block-interplanetary-accelerator-ui +63416=basalt-boulder|block-basalt-boulder-ui +63415=none|status-none-ui +63414=burning|status-burning-ui +63413=freezing|status-freezing-ui +63412=unmoving|status-unmoving-ui +63411=slow|status-slow-ui +63410=wet|status-wet-ui +63409=muddy|status-muddy-ui +63408=melting|status-melting-ui +63407=sapped|status-sapped-ui +63406=spore-slowed|status-spore-slowed-ui +63405=tarred|status-tarred-ui +63404=overdrive|status-overdrive-ui +63403=overclock|status-overclock-ui +63402=shielded|status-shielded-ui +63401=boss|status-boss-ui +63400=shocked|status-shocked-ui +63399=blasted|status-blasted-ui +63398=corroded|status-corroded-ui +63397=disarmed|status-disarmed-ui +63385=duct|block-duct-ui +63376=repair-turret|block-repair-turret-ui +63375=payload-propulsion-tower|block-payload-propulsion-tower-ui +63374=payload-incinerator|block-payload-incinerator-ui +63373=payload-void|block-payload-void-ui +63372=payload-source|block-payload-source-ui +63368=retusa|unit-retusa-ui +63367=directional-item-bridge|block-directional-item-bridge-ui +63366=duct-router|block-duct-router-ui +63365=duct-bridge|block-duct-bridge-ui +63364=oxynoe|unit-oxynoe-ui +63363=cyerce|unit-cyerce-ui +63362=aegires|unit-aegires-ui +63361=electrified|status-electrified-ui +63360=navanax|unit-navanax-ui +63354=payload-launch-pad|block-payload-launch-pad-ui +63353=silicon-arc-furnace|block-silicon-arc-furnace-ui diff --git a/core/assets/maps/shoreline.msav b/core/assets/maps/shoreline.msav deleted file mode 100644 index b0320b4350..0000000000 Binary files a/core/assets/maps/shoreline.msav and /dev/null differ diff --git a/core/assets/scripts/global.js b/core/assets/scripts/global.js index ac1a812efd..2c40cc4466 100755 --- a/core/assets/scripts/global.js +++ b/core/assets/scripts/global.js @@ -53,10 +53,12 @@ function extend(/*Base, ..., def*/){ const extendContent = extend; importPackage(Packages.arc) +importPackage(Packages.arc.audio) importPackage(Packages.arc.func) importPackage(Packages.arc.graphics) importPackage(Packages.arc.graphics.g2d) importPackage(Packages.arc.graphics.gl) +importPackage(Packages.arc.input) importPackage(Packages.arc.math) importPackage(Packages.arc.math.geom) importPackage(Packages.arc.scene) @@ -68,6 +70,12 @@ importPackage(Packages.arc.scene.ui.layout) importPackage(Packages.arc.scene.utils) importPackage(Packages.arc.struct) importPackage(Packages.arc.util) +importPackage(Packages.arc.util.async) +importPackage(Packages.arc.util.io) +importPackage(Packages.arc.util.noise) +importPackage(Packages.arc.util.pooling) +importPackage(Packages.arc.util.serialization) +importPackage(Packages.arc.util.viewport) importPackage(Packages.mindustry) importPackage(Packages.mindustry.ai) importPackage(Packages.mindustry.ai.formations) @@ -97,7 +105,9 @@ importPackage(Packages.mindustry.maps.filters) importPackage(Packages.mindustry.maps.generators) importPackage(Packages.mindustry.maps.planet) importPackage(Packages.mindustry.net) +importPackage(Packages.mindustry.service) importPackage(Packages.mindustry.type) +importPackage(Packages.mindustry.type.weapons) importPackage(Packages.mindustry.type.weather) importPackage(Packages.mindustry.ui) importPackage(Packages.mindustry.ui.dialogs) @@ -123,7 +133,6 @@ importPackage(Packages.mindustry.world.blocks.units) importPackage(Packages.mindustry.world.consumers) importPackage(Packages.mindustry.world.draw) importPackage(Packages.mindustry.world.meta) -importPackage(Packages.mindustry.world.meta.values) importPackage(Packages.mindustry.world.modules) const PlayerIpUnbanEvent = Packages.mindustry.game.EventType.PlayerIpUnbanEvent const PlayerIpBanEvent = Packages.mindustry.game.EventType.PlayerIpBanEvent @@ -132,6 +141,8 @@ const PlayerBanEvent = Packages.mindustry.game.EventType.PlayerBanEvent const PlayerLeave = Packages.mindustry.game.EventType.PlayerLeave const PlayerConnect = Packages.mindustry.game.EventType.PlayerConnect const PlayerJoin = Packages.mindustry.game.EventType.PlayerJoin +const ConnectPacketEvent = Packages.mindustry.game.EventType.ConnectPacketEvent +const ConnectionEvent = Packages.mindustry.game.EventType.ConnectionEvent const UnitChangeEvent = Packages.mindustry.game.EventType.UnitChangeEvent const UnitUnloadEvent = Packages.mindustry.game.EventType.UnitUnloadEvent const UnitCreateEvent = Packages.mindustry.game.EventType.UnitCreateEvent @@ -145,6 +156,7 @@ const ResearchEvent = Packages.mindustry.game.EventType.ResearchEvent const UnlockEvent = Packages.mindustry.game.EventType.UnlockEvent const StateChangeEvent = Packages.mindustry.game.EventType.StateChangeEvent const TileChangeEvent = Packages.mindustry.game.EventType.TileChangeEvent +const TilePreChangeEvent = Packages.mindustry.game.EventType.TilePreChangeEvent const GameOverEvent = Packages.mindustry.game.EventType.GameOverEvent const UnitControlEvent = Packages.mindustry.game.EventType.UnitControlEvent const PickupEvent = Packages.mindustry.game.EventType.PickupEvent diff --git a/core/assets/shaders/atmosphere.frag b/core/assets/shaders/atmosphere.frag index f5f437f298..1c8c4966df 100644 --- a/core/assets/shaders/atmosphere.frag +++ b/core/assets/shaders/atmosphere.frag @@ -1,3 +1,5 @@ +#define HIGHP + const float PI = 3.14159265359; const float MAX = 10000.0; diff --git a/core/assets/shaders/blockbuild.frag b/core/assets/shaders/blockbuild.frag index fe877e873b..6009ea6e27 100644 --- a/core/assets/shaders/blockbuild.frag +++ b/core/assets/shaders/blockbuild.frag @@ -1,3 +1,5 @@ +#define HIGHP + uniform sampler2D u_texture; uniform vec2 u_texsize; diff --git a/core/assets/shaders/caustics.frag b/core/assets/shaders/caustics.frag new file mode 100644 index 0000000000..ddc4b80bbc --- /dev/null +++ b/core/assets/shaders/caustics.frag @@ -0,0 +1,24 @@ +#define HIGHP + +#define NSCALE 200.0 / 1.8 + +uniform sampler2D u_texture; +uniform sampler2D u_noise; + +uniform vec2 u_campos; +uniform vec2 u_resolution; +uniform float u_time; + +varying vec2 v_texCoords; + +void main(){ + vec2 c = v_texCoords.xy; + vec2 coords = vec2(c.x * u_resolution.x + u_campos.x, c.y * u_resolution.y + u_campos.y); + + float btime = u_time / 3400.0; + vec4 noise1 = texture2D(u_noise, (coords) / NSCALE + vec2(btime) * vec2(-0.9, 0.8)); + vec4 noise2 = texture2D(u_noise, (coords) / NSCALE + vec2(btime * 1.1) * vec2(0.8, -1.0)); + //vec4 noise3 = texture2D(u_noise, (coords) / (NSCALE * 2.0) + vec2(btime * 0.9) * vec2(0.8, 1.0)); + + gl_FragColor = vec4(vec3(min(noise1.r, noise2.r)), 0.2); +} diff --git a/core/assets/shaders/water.frag b/core/assets/shaders/water.frag index e7f4ee1016..51c56f26a1 100644 --- a/core/assets/shaders/water.frag +++ b/core/assets/shaders/water.frag @@ -14,7 +14,6 @@ const float mth = 7.0; void main(){ vec2 c = v_texCoords; - vec2 v = vec2(1.0/u_resolution.x, 1.0/u_resolution.y); vec2 coords = vec2(c.x / v.x + u_campos.x, c.y / v.y + u_campos.y); diff --git a/core/assets/sprites/clouds.png b/core/assets/sprites/clouds.png new file mode 100644 index 0000000000..93a8684689 Binary files /dev/null and b/core/assets/sprites/clouds.png differ diff --git a/core/src/mindustry/ClientLauncher.java b/core/src/mindustry/ClientLauncher.java index f2ec9a1291..6042a38b19 100644 --- a/core/src/mindustry/ClientLauncher.java +++ b/core/src/mindustry/ClientLauncher.java @@ -47,12 +47,14 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform loadFileLogger(); platform = this; + maxTextureSize = Gl.getInt(Gl.maxTextureSize); beginTime = Time.millis(); //debug GL information Log.info("[GL] Version: @", graphics.getGLVersion()); - Log.info("[GL] Max texture size: @", Gl.getInt(Gl.maxTextureSize)); + Log.info("[GL] Max texture size: @", maxTextureSize); Log.info("[GL] Using @ context.", gl30 != null ? "OpenGL 3" : "OpenGL 2"); + if(maxTextureSize < 4096) Log.warn("[GL] Your maximum texture size is below the recommended minimum of 4096. This will cause severe performance issues."); Log.info("[JAVA] Version: @", System.getProperty("java.version")); Time.setDeltaProvider(() -> { @@ -81,11 +83,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform Fonts.loadDefaultFont(); //load fallback atlas if max texture size is below 4096 - assets.load(new AssetDescriptor<>(Gl.getInt(Gl.maxTextureSize) >= 4096 ? "sprites/sprites.atlas" : "sprites/fallback/sprites.atlas", TextureAtlas.class)).loaded = t -> { - atlas = (TextureAtlas)t; - Fonts.mergeFontAtlas(atlas); - }; - + assets.load(new AssetDescriptor<>(maxTextureSize >= 4096 ? "sprites/sprites.aatls" : "sprites/fallback/sprites.aatls", TextureAtlas.class)).loaded = t -> atlas = (TextureAtlas)t; assets.loadRun("maps", Map.class, () -> maps.loadPreviews()); Musics.load(); @@ -99,6 +97,9 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform content.createModContent(); }); + assets.load(mods); + assets.loadRun("mergeUI", PixmapPacker.class, () -> {}, () -> Fonts.mergeFontAtlas(atlas)); + add(logic = new Logic()); add(control = new Control()); add(renderer = new Renderer()); @@ -106,7 +107,6 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform add(netServer = new NetServer()); add(netClient = new NetClient()); - assets.load(mods); assets.load(schematics); assets.loadRun("contentinit", ContentLoader.class, () -> content.init(), () -> content.load()); @@ -150,6 +150,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform mods.eachClass(Mod::init); finished = true; Events.fire(new ClientLoadEvent()); + clientLoaded = true; super.resize(graphics.getWidth(), graphics.getHeight()); app.post(() -> app.post(() -> app.post(() -> app.post(() -> { super.resize(graphics.getWidth(), graphics.getHeight()); diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index d9a2632c42..2c263508cf 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -11,10 +11,13 @@ import arc.util.Log.*; import mindustry.ai.*; import mindustry.async.*; import mindustry.core.*; +import mindustry.ctype.*; +import mindustry.editor.*; import mindustry.entities.*; import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.gen.*; +import mindustry.graphics.*; import mindustry.input.*; import mindustry.io.*; import mindustry.logic.*; @@ -23,6 +26,7 @@ import mindustry.maps.*; import mindustry.mod.*; import mindustry.net.Net; import mindustry.net.*; +import mindustry.service.*; import mindustry.world.*; import java.io.*; @@ -42,6 +46,10 @@ public class Vars implements Loadable{ public static boolean experimental = false; /** Name of current Steam player. */ public static String steamPlayerName = ""; + /** Default accessible content types used for player-selectable icons. */ + public static final ContentType[] defaultContentIcons = {ContentType.item, ContentType.liquid, ContentType.block}; + /** Wall darkness radius. */ + public static final int darkRadius = 4; /** Maximum extra padding around deployment schematics. */ public static final int maxLoadoutSchematicPad = 5; /** Maximum schematic size.*/ @@ -81,7 +89,7 @@ public class Vars implements Loadable{ /** displayed item size when ingame. */ public static final float itemSize = 5f; /** units outside of this bound will die instantly */ - public static final float finalWorldBounds = 500; + public static final float finalWorldBounds = 250; /** range for building */ public static final float buildingRange = 220f; /** range for moving items */ @@ -102,6 +110,8 @@ public class Vars implements Loadable{ public static final int tilesize = 8; /** size of one tile payload (^2) */ public static final float tilePayload = tilesize * tilesize; + /** icon sizes for UI */ + public static final float iconXLarge = 8*6f, iconLarge = 8*5f, iconMed = 8*4f, iconSmall = 8*3f; /** tile used in certain situations, instead of null */ public static Tile emptyTile; /** for map generator dialog */ @@ -131,6 +141,14 @@ public class Vars implements Loadable{ public static final int multicastPort = 20151; /** multicast group for discovery.*/ public static final String multicastGroup = "227.2.7.7"; + /** whether the graphical game client has loaded */ + public static boolean clientLoaded = false; + /** max GL texture size */ + public static int maxTextureSize = 2048; + /** Whether to show the core landing animation. */ + public static boolean showLandAnimation = true; + /** Whether to prompt the user to confirm exiting. */ + public static boolean confirmExit = true; /** if true, UI is not drawn */ public static boolean disableUI; /** if true, game is set up in mobile mode, even on desktop. used for debugging */ @@ -199,6 +217,8 @@ public class Vars implements Loadable{ public static AsyncCore asyncCore; public static BaseRegistry bases; public static GlobalConstants constants; + public static MapEditor editor; + public static GameService service = new GameService(); public static Universe universe; public static World world; @@ -243,6 +263,7 @@ public class Vars implements Loadable{ } Version.init(); + CacheLayer.init(); dataDirectory = settings.getDataDirectory(); screenshotDirectory = dataDirectory.child("screenshots/"); @@ -266,6 +287,7 @@ public class Vars implements Loadable{ universe = new Universe(); becontrol = new BeControl(); asyncCore = new AsyncCore(); + if(!headless) editor = new MapEditor(); maps = new Maps(); spawner = new WaveSpawner(); @@ -394,7 +416,7 @@ public class Vars implements Loadable{ Log.info("NOTE: external translation bundle has been loaded."); if(!headless){ - Time.run(10f, () -> ui.showInfo("Note: You have successfully loaded an external translation bundle.")); + Time.run(10f, () -> ui.showInfo("Note: You have successfully loaded an external translation bundle.\n[accent]" + handle.absolutePath())); } }catch(Throwable e){ //no external bundle found diff --git a/core/src/mindustry/ai/BaseAI.java b/core/src/mindustry/ai/BaseAI.java index 768b529759..bebbdc1e8c 100644 --- a/core/src/mindustry/ai/BaseAI.java +++ b/core/src/mindustry/ai/BaseAI.java @@ -16,6 +16,7 @@ import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.defense.*; import mindustry.world.blocks.distribution.*; +import mindustry.world.blocks.payloads.*; import mindustry.world.blocks.production.*; import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.CoreBlock.*; @@ -208,7 +209,7 @@ public class BaseAI{ } Tile wtile = world.tile(realX, realY); - if(tile.block instanceof PayloadConveyor || tile.block instanceof PayloadAcceptor){ + if(tile.block instanceof PayloadConveyor || tile.block instanceof PayloadBlock){ //near a building for(Point2 point : Edges.getEdges(tile.block.size)){ var t = world.build(tile.x + point.x, tile.y + point.y); @@ -288,7 +289,7 @@ public class BaseAI{ } Tile o = world.tile(tile.x + p.x, tile.y + p.y); - if(o != null && (o.block() instanceof PayloadAcceptor || o.block() instanceof PayloadConveyor)){ + if(o != null && (o.block() instanceof PayloadBlock || o.block() instanceof PayloadConveyor)){ continue outer; } diff --git a/core/src/mindustry/ai/BlockIndexer.java b/core/src/mindustry/ai/BlockIndexer.java index ca945b67a9..6a232f426c 100644 --- a/core/src/mindustry/ai/BlockIndexer.java +++ b/core/src/mindustry/ai/BlockIndexer.java @@ -4,11 +4,9 @@ import arc.*; import arc.func.*; import arc.math.*; import arc.math.geom.*; -import arc.struct.EnumSet; import arc.struct.*; import arc.util.*; import mindustry.content.*; -import mindustry.core.*; import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.game.Teams.*; @@ -25,16 +23,16 @@ import static mindustry.Vars.*; /** Class used for indexing special target blocks for AI. */ public class BlockIndexer{ /** Size of one quadrant. */ - private static final int quadrantSize = 16; + private static final int quadrantSize = 20; + private static final Rect rect = new Rect(); + private static boolean returnBool = false; - /** Set of all ores that are being scanned. */ - private final ObjectSet scanOres = new ObjectSet<>(); private final IntSet intSet = new IntSet(); - private final ObjectSet itemSet = new ObjectSet<>(); - /** Stores all ore quadtrants on the map. */ - private ObjectMap ores = new ObjectMap<>(); - /** Maps each team ID to a quarant. A quadrant is a grid of bits, where each bit is set if and only if there is a block of that team in that quadrant. */ - private GridBits[] structQuadrants; + + private int quadWidth, quadHeight; + + /** Stores all ore quadrants on the map. Maps ID to qX to qY to a list of tiles with that ore. */ + private IntSeq[][][] ores; /** Stores all damaged tile entities by team. */ private ObjectSet[] damagedTiles = new ObjectSet[Team.all.length]; /** All ores available on this map. */ @@ -43,28 +41,27 @@ public class BlockIndexer{ private Seq activeTeams = new Seq<>(Team.class); /** Maps teams to a map of flagged tiles by flag. */ private TileArray[][] flagMap = new TileArray[Team.all.length][BlockFlag.all.length]; - /** Max units by team. */ - private int[] unitCaps = new int[Team.all.length]; - /** Maps tile positions to their last known tile index data. */ - private IntMap typeMap = new IntMap<>(); + /** Empty set used for returning. */ private TileArray emptySet = new TileArray(); /** Array used for returning and reusing. */ private Seq returnArray = new Seq<>(); /** Array used for returning and reusing. */ - private Seq breturnArray = new Seq<>(); + private Seq breturnArray = new Seq<>(Building.class); public BlockIndexer(){ + + Events.on(TilePreChangeEvent.class, event -> { + removeIndex(event.tile); + }); + Events.on(TileChangeEvent.class, event -> { - updateIndices(event.tile); + addIndex(event.tile); }); Events.on(WorldLoadEvent.class, event -> { - scanOres.clear(); - scanOres.addAll(Item.getAllOres()); damagedTiles = new ObjectSet[Team.all.length]; flagMap = new TileArray[Team.all.length][BlockFlag.all.length]; - unitCaps = new int[Team.all.length]; activeTeams = new Seq<>(Team.class); for(int i = 0; i < flagMap.length; i++){ @@ -73,12 +70,10 @@ public class BlockIndexer{ } } - typeMap.clear(); allOres.clear(); - ores = null; - - //create bitset for each team type that contains each quadrant - structQuadrants = new GridBits[Team.all.length]; + ores = new IntSeq[content.items().size][][]; + quadWidth = Mathf.ceil(world.width() / (float)quadrantSize); + quadHeight = Mathf.ceil(world.height() / (float)quadrantSize); for(Tile tile : world.tiles){ process(tile); @@ -87,59 +82,83 @@ public class BlockIndexer{ notifyTileDamaged(tile.build); } - if(tile.drop() != null) allOres.add(tile.drop()); - } + var drop = tile.drop(); - for(int x = 0; x < quadWidth(); x++){ - for(int y = 0; y < quadHeight(); y++){ - updateQuadrant(world.tile(x * quadrantSize, y * quadrantSize)); + if(drop != null){ + allOres.add(drop); + + int qx = (tile.x / quadrantSize); + int qy = (tile.y / quadrantSize); + + //add position of quadrant to list + if(tile.block() == Blocks.air){ + if(ores[drop.id] == null){ + ores[drop.id] = new IntSeq[quadWidth][quadHeight]; + } + if(ores[drop.id][qx][qy] == null){ + ores[drop.id][qx][qy] = new IntSeq(false, 16); + } + ores[drop.id][qx][qy].add(tile.pos()); + } } } - - scanOres(); }); } - public void updateIndices(Tile tile){ - if(typeMap.get(tile.pos()) != null){ - TileIndex index = typeMap.get(tile.pos()); - for(BlockFlag flag : index.flags){ - getFlagged(index.team)[flag.ordinal()].remove(tile); + public void removeIndex(Tile tile){ + var team = tile.team(); + if(team != Team.derelict && tile.isCenter()){ + var flags = tile.block().flags; + var data = team.data(); + + if(flags.size() > 0){ + for(BlockFlag flag : flags){ + getFlagged(team)[flag.ordinal()].remove(tile); + } } - if(index.flags.contains(BlockFlag.unitModifier)){ - updateCap(index.team); + //update the unit cap when building is remove + data.unitCap -= tile.block().unitCapModifier; + + //unregister building from building quadtree + if(data.buildings != null){ + data.buildings.remove(tile.build); } } + } + + public void addIndex(Tile tile){ process(tile); - updateQuadrant(tile); + + var drop = tile.drop(); + if(drop != null){ + int qx = tile.x / quadrantSize; + int qy = tile.y / quadrantSize; + + if(ores[drop.id] == null){ + ores[drop.id] = new IntSeq[quadWidth][quadHeight]; + } + if(ores[drop.id][qx][qy] == null){ + ores[drop.id][qx][qy] = new IntSeq(false, 16); + } + + int pos = tile.pos(); + var seq = ores[drop.id][qx][qy]; + + //when the drop can be mined, record the ore position + if(tile.block() == Blocks.air && !seq.contains(pos)){ + seq.add(pos); + }else{ + //otherwise, it likely became blocked, remove it (even if it wasn't there) + seq.removeValue(pos); + } + } } private TileArray[] getFlagged(Team team){ return flagMap[team.id]; } - private GridBits structQuadrant(Team t){ - if(structQuadrants[t.id] == null){ - structQuadrants[t.id] = new GridBits(Mathf.ceil(world.width() / (float)quadrantSize), Mathf.ceil(world.height() / (float)quadrantSize)); - } - return structQuadrants[t.id]; - } - - /** Updates all the structure quadrants for a newly activated team. */ - public void updateTeamIndex(Team team){ - if(structQuadrants == null) return; - - //go through every tile... ouch - for(Tile tile : world.tiles){ - if(tile.team() == team){ - int quadrantX = tile.x / quadrantSize; - int quadrantY = tile.y / quadrantSize; - structQuadrant(team).set(quadrantX, quadrantY); - } - } - } - /** @return whether this item is present on this map. */ public boolean hasOre(Item item){ return allOres.contains(item); @@ -181,31 +200,28 @@ public class BlockIndexer{ return eachBlock(team.team(), team.getX(), team.getY(), range, pred, cons); } - public boolean eachBlock(Team team, float wx, float wy, float range, Boolf pred, Cons cons){ - intSet.clear(); + public boolean eachBlock(@Nullable Team team, float wx, float wy, float range, Boolf pred, Cons cons){ + returnBool = false; - int tx = World.toTile(wx); - int ty = World.toTile(wy); - - int tileRange = (int)(range / tilesize + 1); - boolean any = false; - - for(int x = -tileRange + tx; x <= tileRange + tx; x++){ - for(int y = -tileRange + ty; y <= tileRange + ty; y++){ - if(!Mathf.within(x * tilesize, y * tilesize, wx, wy, range)) continue; - - Building other = world.build(x, y); - - if(other == null) continue; - - if((team == null || other.team == team) && pred.get(other) && intSet.add(other.pos())){ - cons.get(other); - any = true; + if(team == null){ + allBuildings(wx, wy, range, b -> { + if(pred.get(b)){ + returnBool = true; + cons.get(b); } - } + }); + }else{ + var buildings = team.data().buildings; + if(buildings == null) return false; + buildings.intersect(wx - range, wy - range, range*2f, range*2f, b -> { + if(b.within(wx, wy, range + b.hitSize() / 2f) && pred.get(b)){ + returnBool = true; + cons.get(b); + } + }); } - return any; + return returnBool; } /** Get all enemy blocks with a flag. */ @@ -247,7 +263,20 @@ public class BlockIndexer{ damagedTiles[entity.team.id].add(entity); } - public Building findEnemyTile(Team team, float x, float y, float range, Boolf pred){ + public void allBuildings(float x, float y, float range, Cons cons){ + for(int i = 0; i < activeTeams.size; i++){ + Team team = activeTeams.items[i]; + var buildings = team.data().buildings; + if(buildings == null) continue; + buildings.intersect(x - range, y - range, range*2f, range*2f, b -> { + if(b.within(x, y, range + b.hitSize()/2f)){ + cons.get(b); + } + }); + } + } + + public Building findEnemyTile(@Nullable Team team, float x, float y, float range, Boolf pred){ for(int i = 0; i < activeTeams.size; i++){ Team enemy = activeTeams.items[i]; @@ -269,58 +298,50 @@ public class BlockIndexer{ public Building findTile(Team team, float x, float y, float range, Boolf pred, boolean usePriority){ Building closest = null; float dst = 0; + var buildings = team.data().buildings; + if(buildings == null) return null; - for(int rx = Math.max((int)((x - range) / tilesize / quadrantSize), 0); rx <= (int)((x + range) / tilesize / quadrantSize) && rx < quadWidth(); rx++){ - for(int ry = Math.max((int)((y - range) / tilesize / quadrantSize), 0); ry <= (int)((y + range) / tilesize / quadrantSize) && ry < quadHeight(); ry++){ + breturnArray.clear(); + buildings.intersect(rect.setCentered(x, y, range * 2f), breturnArray); - if(!getQuad(team, rx, ry)) continue; + for(int i = 0; i < breturnArray.size; i++){ + var next = breturnArray.items[i]; - for(int tx = rx * quadrantSize; tx < (rx + 1) * quadrantSize && tx < world.width(); tx++){ - for(int ty = ry * quadrantSize; ty < (ry + 1) * quadrantSize && ty < world.height(); ty++){ - Building e = world.build(tx, ty); + if(!pred.get(next) || !next.block.targetable) continue; - if(e == null || e.team != team || !pred.get(e) || !e.block.targetable || e.team == Team.derelict) continue; - - float bdst = e.dst(x, y) - e.hitSize() / 2f; - if(bdst < range && (closest == null || - //this one is closer, and it is at least of equal priority - (bdst < dst && (!usePriority || closest.block.priority.ordinal() <= e.block.priority.ordinal())) || - //priority is used, and new block has higher priority regardless of range - (usePriority && closest.block.priority.ordinal() < e.block.priority.ordinal()))){ - dst = bdst; - closest = e; - } - } - } + float bdst = next.dst(x, y) - next.hitSize() / 2f; + if(bdst < range && (closest == null || + //this one is closer, and it is at least of equal priority + (bdst < dst && (!usePriority || closest.block.priority.ordinal() <= next.block.priority.ordinal())) || + //priority is used, and new block has higher priority regardless of range + (usePriority && closest.block.priority.ordinal() < next.block.priority.ordinal()))){ + dst = bdst; + closest = next; } } return closest; } - /** - * Returns a set of tiles that have ores of the specified type nearby. - * While each tile in the set is not guaranteed to have an ore directly on it, - * each tile will at least have an ore within {@link #quadrantSize} / 2 blocks of it. - * Only specific ore types are scanned. See {@link #scanOres}. - */ - public TileArray getOrePositions(Item item){ - return ores.get(item, emptySet); - } - /** Find the closest ore block relative to a position. */ public Tile findClosestOre(float xp, float yp, Item item){ - Tile tile = Geometry.findClosest(xp, yp, getOrePositions(item)); - - if(tile == null) return null; - - for(int x = Math.max(0, tile.x - quadrantSize / 2); x < tile.x + quadrantSize / 2 && x < world.width(); x++){ - for(int y = Math.max(0, tile.y - quadrantSize / 2); y < tile.y + quadrantSize / 2 && y < world.height(); y++){ - Tile res = world.tile(x, y); - if(res.block() == Blocks.air && res.drop() == item){ - return res; + if(ores[item.id] != null){ + float minDst = 0f; + Tile closest = null; + for(int qx = 0; qx < quadWidth; qx++){ + for(int qy = 0; qy < quadHeight; qy++){ + var arr = ores[item.id][qx][qy]; + if(arr != null && arr.size > 0){ + Tile tile = world.tile(arr.first()); + float dst = Mathf.dst2(xp, yp, tile.worldx(), tile.worldy()); + if(closest == null || dst < minDst){ + closest = tile; + minDst = dst; + } + } } } + return closest; } return null; @@ -331,147 +352,36 @@ public class BlockIndexer{ return findClosestOre(unit.x, unit.y, item); } - /** @return extra unit cap of a team. This is added onto the base value. */ - public int getExtraUnits(Team team){ - return unitCaps[team.id]; - } - - private void updateCap(Team team){ - TileArray capped = getFlagged(team)[BlockFlag.unitModifier.ordinal()]; - unitCaps[team.id] = 0; - for(Tile capper : capped){ - unitCaps[team.id] += capper.block().unitCapModifier; - } - } - private void process(Tile tile){ - if(tile.block().flags.size() > 0 && tile.team() != Team.derelict && tile.isCenter()){ - TileArray[] map = getFlagged(tile.team()); + var team = tile.team(); + //only process entity changes with centered tiles + if(tile.isCenter() && team != Team.derelict){ + var data = team.data(); + if(tile.block().flags.size() > 0 && tile.isCenter()){ + TileArray[] map = getFlagged(team); - for(BlockFlag flag : tile.block().flags){ + for(BlockFlag flag : tile.block().flags){ - TileArray arr = map[flag.ordinal()]; + TileArray arr = map[flag.ordinal()]; - arr.add(tile); + arr.add(tile); - map[flag.ordinal()] = arr; - } - - if(tile.block().flags.contains(BlockFlag.unitModifier)){ - updateCap(tile.team()); - } - - typeMap.put(tile.pos(), new TileIndex(tile.block().flags, tile.team())); - } - - if(!activeTeams.contains(tile.team())){ - activeTeams.add(tile.team()); - } - - if(ores == null) return; - - int quadrantX = tile.x / quadrantSize; - int quadrantY = tile.y / quadrantSize; - itemSet.clear(); - - Tile rounded = world.rawTile(Mathf.clamp(quadrantX * quadrantSize + quadrantSize / 2, 0, world.width() - 1), Mathf.clamp(quadrantY * quadrantSize + quadrantSize / 2, 0, world.height() - 1)); - - //find all items that this quadrant contains - for(int x = Math.max(0, rounded.x - quadrantSize / 2); x < rounded.x + quadrantSize / 2 && x < world.width(); x++){ - for(int y = Math.max(0, rounded.y - quadrantSize / 2); y < rounded.y + quadrantSize / 2 && y < world.height(); y++){ - Tile result = world.tile(x, y); - if(result == null || result.drop() == null || !scanOres.contains(result.drop()) || result.block() != Blocks.air) continue; - - itemSet.add(result.drop()); - } - } - - //update quadrant at this position - for(Item item : scanOres){ - TileArray set = ores.get(item); - - //update quadrant status depending on whether the item is in it - if(!itemSet.contains(item)){ - set.remove(rounded); - }else{ - set.add(rounded); - } - } - } - - private void updateQuadrant(Tile tile){ - if(structQuadrants == null) return; - - //this quadrant is now 'dirty', re-scan the whole thing - int quadrantX = tile.x / quadrantSize; - int quadrantY = tile.y / quadrantSize; - - for(Team team : activeTeams){ - GridBits bits = structQuadrant(team); - - //fast-set this quadrant to 'occupied' if the tile just placed is already of this team - if(tile.team() == team && tile.build != null && tile.block().targetable){ - bits.set(quadrantX, quadrantY); - continue; //no need to process futher - } - - bits.set(quadrantX, quadrantY, false); - - outer: - for(int x = quadrantX * quadrantSize; x < world.width() && x < (quadrantX + 1) * quadrantSize; x++){ - for(int y = quadrantY * quadrantSize; y < world.height() && y < (quadrantY + 1) * quadrantSize; y++){ - Building result = world.build(x, y); - //when a targetable block is found, mark this quadrant as occupied and stop searching - if(result != null && result.team == team){ - bits.set(quadrantX, quadrantY); - break outer; - } + map[flag.ordinal()] = arr; } } - } - } - private boolean getQuad(Team team, int quadrantX, int quadrantY){ - return structQuadrant(team).get(quadrantX, quadrantY); - } + //update the unit cap when new tile is registered + data.unitCap += tile.block().unitCapModifier; - private int quadWidth(){ - return Mathf.ceil(world.width() / (float)quadrantSize); - } - - private int quadHeight(){ - return Mathf.ceil(world.height() / (float)quadrantSize); - } - - private void scanOres(){ - ores = new ObjectMap<>(); - - //initialize ore map with empty sets - for(Item item : scanOres){ - ores.put(item, new TileArray()); - } - - for(Tile tile : world.tiles){ - int qx = (tile.x / quadrantSize); - int qy = (tile.y / quadrantSize); - - //add position of quadrant to list when an ore is found - if(tile.drop() != null && scanOres.contains(tile.drop()) && tile.block() == Blocks.air){ - ores.get(tile.drop()).add(world.tile( - //make sure to clamp quadrant middle position, since it might go off bounds - Mathf.clamp(qx * quadrantSize + quadrantSize / 2, 0, world.width() - 1), - Mathf.clamp(qy * quadrantSize + quadrantSize / 2, 0, world.height() - 1))); + if(!activeTeams.contains(team)){ + activeTeams.add(team); } - } - } - private static class TileIndex{ - public final EnumSet flags; - public final Team team; - - public TileIndex(EnumSet flags, Team team){ - this.flags = flags; - this.team = team; + //insert the new tile into the quadtree for targeting + if(data.buildings == null){ + data.buildings = new QuadTree<>(new Rect(0, 0, world.unitWidth(), world.unitHeight())); + } + data.buildings.insert(tile.build); } } diff --git a/core/src/mindustry/ai/types/BuilderAI.java b/core/src/mindustry/ai/types/BuilderAI.java index 40a008c6c7..5a3c5b00f8 100644 --- a/core/src/mindustry/ai/types/BuilderAI.java +++ b/core/src/mindustry/ai/types/BuilderAI.java @@ -70,13 +70,15 @@ public class BuilderAI extends AIController{ for(Player player : Groups.player){ if(player.isBuilder() && player.unit().activelyBuilding() && player.unit().buildPlan().samePos(req) && player.unit().buildPlan().breaking){ unit.plans.removeFirst(); + //remove from list of plans + unit.team.data().blocks.remove(p -> p.x == req.x && p.y == req.y); return; } } } boolean valid = - (req.tile() != null && req.tile().build instanceof ConstructBuild cons && cons.cblock == req.block) || + (req.tile() != null && req.tile().build instanceof ConstructBuild cons && cons.current == req.block) || (req.breaking ? Build.validBreak(unit.team(), req.x, req.y) : Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation)); diff --git a/core/src/mindustry/ai/types/FormationAI.java b/core/src/mindustry/ai/types/FormationAI.java index 36d6c5169f..a08d9bb95c 100644 --- a/core/src/mindustry/ai/types/FormationAI.java +++ b/core/src/mindustry/ai/types/FormationAI.java @@ -33,7 +33,12 @@ public class FormationAI extends AIController implements FormationMember{ } if(unit.type.canBoost){ - unit.elevation = Mathf.approachDelta(unit.elevation, unit.onSolid() ? 1f : leader.type.canBoost ? leader.elevation : 0f, unit.type.riseSpeed); + unit.elevation = Mathf.approachDelta(unit.elevation, + unit.onSolid() ? 1f : //definitely cannot land + unit.isFlying() && !unit.canLand() ? unit.elevation : //try to maintain altitude + leader.type.canBoost ? leader.elevation : //follow leader + 0f, + unit.type.riseSpeed); } unit.controlWeapons(true, leader.isShooting); @@ -85,7 +90,7 @@ public class FormationAI extends AIController implements FormationMember{ @Override public float formationSize(){ - return unit.hitSize * 1.1f; + return unit.hitSize * 1.3f; } @Override diff --git a/core/src/mindustry/ai/types/GroundAI.java b/core/src/mindustry/ai/types/GroundAI.java index 5a9db8db6a..4ebdfe1595 100644 --- a/core/src/mindustry/ai/types/GroundAI.java +++ b/core/src/mindustry/ai/types/GroundAI.java @@ -8,8 +8,6 @@ import mindustry.gen.*; import mindustry.world.*; import mindustry.world.meta.*; -import java.util.*; - import static mindustry.Vars.*; public class GroundAI extends AIController{ @@ -19,16 +17,16 @@ public class GroundAI extends AIController{ Building core = unit.closestEnemyCore(); - if(core != null && unit.within(core, unit.range() / 1.1f + core.block.size * tilesize / 2f)){ + if(core != null && unit.within(core, unit.range() / 1.3f + core.block.size * tilesize / 2f)){ target = core; - for(int i = 0; i < targets.length; i++){ - if(unit.mounts[i].weapon.bullet.collidesGround){ - targets[i] = core; + for(var mount : unit.mounts){ + if(mount.weapon.controllable && mount.weapon.bullet.collidesGround){ + mount.target = core; } } } - if((core == null || !unit.within(core, unit.range() * 0.5f)) && command() == UnitCommand.attack){ + if((core == null || !unit.within(core, unit.type.range * 0.5f)) && command() == UnitCommand.attack){ boolean move = true; if(state.rules.waves && unit.team == state.rules.defaultTeam){ @@ -47,7 +45,7 @@ public class GroundAI extends AIController{ } } - if(unit.type.canBoost && !unit.onSolid()){ + if(unit.type.canBoost && unit.elevation > 0.001f && !unit.onSolid()){ unit.elevation = Mathf.approachDelta(unit.elevation, 0f, unit.type.riseSpeed); } diff --git a/core/src/mindustry/ai/types/HugAI.java b/core/src/mindustry/ai/types/HugAI.java new file mode 100644 index 0000000000..76acdec8c8 --- /dev/null +++ b/core/src/mindustry/ai/types/HugAI.java @@ -0,0 +1,81 @@ +package mindustry.ai.types; + +import arc.math.*; +import arc.math.geom.*; +import mindustry.*; +import mindustry.ai.*; +import mindustry.entities.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.world.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +public class HugAI extends AIController{ + + @Override + public void updateMovement(){ + + Building core = unit.closestEnemyCore(); + + if(core != null && unit.within(core, unit.range() / 1.1f + core.block.size * tilesize / 2f)){ + target = core; + for(var mount : unit.mounts){ + if(mount.weapon.controllable && mount.weapon.bullet.collidesGround){ + mount.target = core; + } + } + } + + if(command() == UnitCommand.attack){ + boolean move = true; + + if(state.rules.waves && unit.team == state.rules.defaultTeam){ + Tile spawner = getClosestSpawner(); + if(spawner != null && unit.within(spawner, state.rules.dropZoneRadius + 120f)) move = false; + } + + //raycast for target + if(target != null && unit.within(target, unit.type.range) && !Vars.world.raycast(unit.tileX(), unit.tileY(), target.tileX(), target.tileY(), (x, y) -> { + for(Point2 p : Geometry.d4c){ + if(!unit.canPass(x + p.x, y + p.y)){ + return true; + } + } + return false; + })){ + if(unit.within(target, (unit.hitSize + (target instanceof Sized s ? s.hitSize() : 1f)) * 0.6f)){ + //circle target + unit.moveAt(vec.set(target).sub(unit).rotate(90f).setLength(unit.speed())); + }else{ + //move toward target in a straight line + unit.moveAt(vec.set(target).sub(unit).limit(unit.speed())); + } + }else if(move){ + pathfind(Pathfinder.fieldCore); + } + } + + if(command() == UnitCommand.rally){ + Teamc target = targetFlag(unit.x, unit.y, BlockFlag.rally, false); + + if(target != null && !unit.within(target, 70f)){ + pathfind(Pathfinder.fieldRally); + } + } + + if(unit.type.canBoost && unit.elevation > 0.001f && !unit.onSolid()){ + unit.elevation = Mathf.approachDelta(unit.elevation, 0f, unit.type.riseSpeed); + } + + if(!Units.invalidateTarget(target, unit, unit.range()) && unit.type.rotateShooting){ + if(unit.type.hasWeapons()){ + unit.lookAt(Predict.intercept(unit, target, unit.type.weapons.first().bullet.speed)); + } + }else if(unit.moving()){ + unit.lookAt(unit.vel().angle()); + } + + } +} diff --git a/core/src/mindustry/ai/types/LogicAI.java b/core/src/mindustry/ai/types/LogicAI.java index abd76201e6..8c1128c7e0 100644 --- a/core/src/mindustry/ai/types/LogicAI.java +++ b/core/src/mindustry/ai/types/LogicAI.java @@ -98,7 +98,7 @@ public class LogicAI extends AIController{ } if(unit.type.canBoost && !unit.type.flying){ - unit.elevation = Mathf.approachDelta(unit.elevation, Mathf.num(boost || unit.onSolid()), 0.08f); + unit.elevation = Mathf.approachDelta(unit.elevation, Mathf.num(boost || unit.onSolid() || (unit.isFlying() && !unit.canLand())), unit.type.riseSpeed); } //look where moving if there's nothing to aim at diff --git a/core/src/mindustry/ai/types/MinerAI.java b/core/src/mindustry/ai/types/MinerAI.java index fbf7cf5526..5e80c52779 100644 --- a/core/src/mindustry/ai/types/MinerAI.java +++ b/core/src/mindustry/ai/types/MinerAI.java @@ -31,7 +31,7 @@ public class MinerAI extends AIController{ //core full of the target item, do nothing if(targetItem != null && core.acceptStack(targetItem, 1, unit) == 0){ unit.clearItem(); - unit.mineTile(null); + unit.mineTile =null; return; } @@ -46,7 +46,7 @@ public class MinerAI extends AIController{ if(ore != null){ moveTo(ore, unit.type.miningRange / 2f, 20f); - if(unit.within(ore, unit.type.miningRange)){ + if(ore.block() == Blocks.air && unit.within(ore, unit.type.miningRange)){ unit.mineTile = ore; } diff --git a/core/src/mindustry/ai/types/SuicideAI.java b/core/src/mindustry/ai/types/SuicideAI.java index e4c1461227..8f2a04c2dc 100644 --- a/core/src/mindustry/ai/types/SuicideAI.java +++ b/core/src/mindustry/ai/types/SuicideAI.java @@ -9,8 +9,11 @@ import mindustry.gen.*; import mindustry.world.*; import mindustry.world.blocks.distribution.*; import mindustry.world.blocks.liquid.*; +import mindustry.world.blocks.storage.*; import mindustry.world.meta.*; +import static mindustry.Vars.*; + public class SuicideAI extends GroundAI{ static boolean blockedByBlock; @@ -29,6 +32,10 @@ public class SuicideAI extends GroundAI{ boolean rotate = false, shoot = false, moveToTarget = false; + if(target == null){ + target = core; + } + if(!Units.invalidateTarget(target, unit, unit.range()) && unit.hasWeapons()){ rotate = true; shoot = unit.within(target, unit.type.weapons.first().bullet.range() + @@ -39,7 +46,7 @@ public class SuicideAI extends GroundAI{ } //do not move toward walls or transport blocks - if(!(target instanceof Building build && ( + if(!(target instanceof Building build && !(build.block instanceof CoreBlock) && ( build.block.group == BlockGroup.walls || build.block.group == BlockGroup.liquids || build.block.group == BlockGroup.transportation @@ -81,8 +88,20 @@ public class SuicideAI extends GroundAI{ if(target != null && !unit.within(target, 70f)){ pathfind(Pathfinder.fieldRally); } - }else if(command() == UnitCommand.attack && core != null){ - pathfind(Pathfinder.fieldCore); + }else if(command() == UnitCommand.attack){ + boolean move = true; + + //stop moving toward the drop zone if applicable + if(core == null && state.rules.waves && unit.team == state.rules.defaultTeam){ + Tile spawner = getClosestSpawner(); + if(spawner != null && unit.within(spawner, state.rules.dropZoneRadius + 120f)){ + move = false; + } + } + + if(move){ + pathfind(Pathfinder.fieldCore); + } } if(unit.moving()) unit.lookAt(unit.vel().angle()); diff --git a/core/src/mindustry/async/AsyncCore.java b/core/src/mindustry/async/AsyncCore.java index a9d4cdd90d..88272feb95 100644 --- a/core/src/mindustry/async/AsyncCore.java +++ b/core/src/mindustry/async/AsyncCore.java @@ -10,19 +10,14 @@ import static mindustry.Vars.*; public class AsyncCore{ //all processes to be executed each frame - private final Seq processes = Seq.with( + public final Seq processes = Seq.with( new PhysicsProcess() ); //futures to be awaited private final Seq> futures = new Seq<>(); - private final ExecutorService executor = Executors.newFixedThreadPool(processes.size, r -> { - Thread thread = new Thread(r, "AsyncLogic-Thread"); - thread.setDaemon(true); - thread.setUncaughtExceptionHandler((t, e) -> Core.app.post(() -> { throw new RuntimeException(e); })); - return thread; - }); + private ExecutorService executor; public AsyncCore(){ Events.on(WorldLoadEvent.class, e -> { @@ -49,6 +44,16 @@ public class AsyncCore{ futures.clear(); + //init executor with size of potentially-modified process list + if(executor == null){ + executor = Executors.newFixedThreadPool(processes.size, r -> { + Thread thread = new Thread(r, "AsyncLogic-Thread"); + thread.setDaemon(true); + thread.setUncaughtExceptionHandler((t, e) -> Core.app.post(() -> { throw new RuntimeException(e); })); + return thread; + }); + } + //submit all tasks for(AsyncProcess p : processes){ if(p.shouldProcess()){ diff --git a/core/src/mindustry/audio/SoundControl.java b/core/src/mindustry/audio/SoundControl.java index c7d61df9b9..a5c34814bb 100644 --- a/core/src/mindustry/audio/SoundControl.java +++ b/core/src/mindustry/audio/SoundControl.java @@ -134,6 +134,8 @@ public class SoundControl{ } } + Core.audio.setPaused(Core.audio.soundBus.id, state.isPaused()); + if(state.isMenu()){ silenced = false; if(ui.planet.isShown()){ diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index 4f8bb350af..0f1a658539 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -20,6 +20,7 @@ import mindustry.world.blocks.experimental.*; import mindustry.world.blocks.legacy.*; import mindustry.world.blocks.liquid.*; import mindustry.world.blocks.logic.*; +import mindustry.world.blocks.payloads.*; import mindustry.world.blocks.power.*; import mindustry.world.blocks.production.*; import mindustry.world.blocks.sandbox.*; @@ -36,7 +37,8 @@ public class Blocks implements ContentList{ //environment air, spawn, cliff, deepwater, water, taintedWater, tar, slag, stone, craters, charr, sand, darksand, dirt, mud, ice, snow, darksandTaintedWater, space, - dacite, stoneWall, dirtWall, sporeWall, iceWall, daciteWall, sporePine, snowPine, pine, shrubs, whiteTree, whiteTreeDead, sporeCluster, + dacite, + stoneWall, dirtWall, sporeWall, iceWall, daciteWall, sporePine, snowPine, pine, shrubs, whiteTree, whiteTreeDead, sporeCluster, iceSnow, sandWater, darksandWater, duneWall, sandWall, moss, sporeMoss, shale, shaleWall, shaleBoulder, sandBoulder, daciteBoulder, boulder, snowBoulder, basaltBoulder, grass, salt, metalFloor, metalFloorDamaged, metalFloor2, metalFloor3, metalFloor5, basalt, magmarock, hotrock, snowWall, saltWall, darkPanel1, darkPanel2, darkPanel3, darkPanel4, darkPanel5, darkPanel6, darkMetal, @@ -50,7 +52,7 @@ public class Blocks implements ContentList{ melter, separator, disassembler, sporePress, pulverizer, incinerator, coalCentrifuge, //sandbox - powerSource, powerVoid, itemSource, itemVoid, liquidSource, liquidVoid, illuminator, + powerSource, powerVoid, itemSource, itemVoid, liquidSource, liquidVoid, payloadVoid, payloadSource, illuminator, //defense copperWall, copperWallLarge, titaniumWall, titaniumWallLarge, plastaniumWall, plastaniumWallLarge, thoriumWall, thoriumWallLarge, door, doorLarge, @@ -59,7 +61,8 @@ public class Blocks implements ContentList{ //transport conveyor, titaniumConveyor, plastaniumConveyor, armoredConveyor, distributor, junction, itemBridge, phaseConveyor, sorter, invertedSorter, router, - overflowGate, underflowGate, massDriver, payloadConveyor, payloadRouter, + overflowGate, underflowGate, massDriver, + duct, ductRouter, ductBridge, //liquid mechanicalPump, rotaryPump, thermalPump, conduit, pulseConduit, platedConduit, liquidRouter, liquidTank, liquidJunction, bridgeConduit, phaseConduit, @@ -81,16 +84,20 @@ public class Blocks implements ContentList{ commandCenter, groundFactory, airFactory, navalFactory, additiveReconstructor, multiplicativeReconstructor, exponentialReconstructor, tetrativeReconstructor, - repairPoint, resupplyPoint, + repairPoint, repairTurret, resupplyPoint, + + //payloads + payloadConveyor, payloadRouter, payloadPropulsionTower, //logic message, switchBlock, microProcessor, logicProcessor, hyperProcessor, largeLogicDisplay, logicDisplay, memoryCell, memoryBank, //campaign - launchPad, launchPadLarge, interplanetaryAccelerator, + launchPad, interplanetaryAccelerator, //misc experimental - blockForge, blockLoader, blockUnloader; + blockForge, blockLoader, blockUnloader + ; @Override public void load(){ @@ -361,32 +368,37 @@ public class Blocks implements ContentList{ whiteTree = new TreeBlock("white-tree"); - sporeCluster = new Boulder("spore-cluster"){{ + sporeCluster = new Prop("spore-cluster"){{ variants = 3; }}; - boulder = new Boulder("boulder"){{ + //glowBlob = new Prop("glowblob"){{ + // variants = 1; + //}}; + + boulder = new Prop("boulder"){{ variants = 2; + stone.asFloor().decoration = this; }}; - snowBoulder = new Boulder("snow-boulder"){{ + snowBoulder = new Prop("snow-boulder"){{ variants = 2; snow.asFloor().decoration = ice.asFloor().decoration = iceSnow.asFloor().decoration = salt.asFloor().decoration = this; }}; - shaleBoulder = new Boulder("shale-boulder"){{ + shaleBoulder = new Prop("shale-boulder"){{ variants = 2; }}; - sandBoulder = new Boulder("sand-boulder"){{ + sandBoulder = new Prop("sand-boulder"){{ variants = 2; }}; - daciteBoulder = new Boulder("dacite-boulder"){{ + daciteBoulder = new Prop("dacite-boulder"){{ variants = 2; }}; - basaltBoulder = new Boulder("basalt-boulder"){{ + basaltBoulder = new Prop("basalt-boulder"){{ variants = 2; }}; @@ -491,6 +503,7 @@ public class Blocks implements ContentList{ craftEffect = Fx.pulverizeMedium; outputItem = new ItemStack(Items.graphite, 2); craftTime = 30f; + itemCapacity = 20; size = 3; hasItems = true; hasLiquids = true; @@ -501,7 +514,7 @@ public class Blocks implements ContentList{ consumes.liquid(Liquids.water, 0.1f); }}; - siliconSmelter = new GenericSmelter("silicon-smelter"){{ + siliconSmelter = new GenericCrafter("silicon-smelter"){{ requirements(Category.crafting, with(Items.copper, 30, Items.lead, 25)); craftEffect = Fx.smeltsmoke; outputItem = new ItemStack(Items.silicon, 1); @@ -509,13 +522,13 @@ public class Blocks implements ContentList{ size = 2; hasPower = true; hasLiquids = false; - flameColor = Color.valueOf("ffef99"); + drawer = new DrawSmelter(Color.valueOf("ffef99")); consumes.items(with(Items.coal, 1, Items.sand, 2)); consumes.power(0.50f); }}; - siliconCrucible = new AttributeSmelter("silicon-crucible"){{ + siliconCrucible = new AttributeCrafter("silicon-crucible"){{ requirements(Category.crafting, with(Items.titanium, 120, Items.metaglass, 80, Items.plastanium, 35, Items.silicon, 60)); craftEffect = Fx.smeltsmoke; outputItem = new ItemStack(Items.silicon, 8); @@ -523,22 +536,22 @@ public class Blocks implements ContentList{ size = 3; hasPower = true; hasLiquids = false; - flameColor = Color.valueOf("ffef99"); itemCapacity = 30; boostScale = 0.15f; + drawer = new DrawSmelter(Color.valueOf("ffef99")); consumes.items(with(Items.coal, 4, Items.sand, 6, Items.pyratite, 1)); consumes.power(4f); }}; - kiln = new GenericSmelter("kiln"){{ + kiln = new GenericCrafter("kiln"){{ requirements(Category.crafting, with(Items.copper, 60, Items.graphite, 30, Items.lead, 30)); craftEffect = Fx.smeltsmoke; outputItem = new ItemStack(Items.metaglass, 1); craftTime = 30f; size = 2; hasPower = hasItems = true; - flameColor = Color.valueOf("ffc099"); + drawer = new DrawSmelter(Color.valueOf("ffc099")); consumes.items(with(Items.lead, 1, Items.sand, 1)); consumes.power(0.60f); @@ -579,7 +592,7 @@ public class Blocks implements ContentList{ itemCapacity = 20; }}; - surgeSmelter = new GenericSmelter("alloy-smelter"){{ + surgeSmelter = new GenericCrafter("alloy-smelter"){{ requirements(Category.crafting, with(Items.silicon, 80, Items.lead, 80, Items.thorium, 70)); craftEffect = Fx.smeltsmoke; outputItem = new ItemStack(Items.surgeAlloy, 1); @@ -587,6 +600,7 @@ public class Blocks implements ContentList{ size = 3; hasPower = true; itemCapacity = 20; + drawer = new DrawSmelter(); consumes.power(4f); consumes.items(with(Items.copper, 3, Items.lead, 4, Items.titanium, 2, Items.silicon, 3)); @@ -610,9 +624,8 @@ public class Blocks implements ContentList{ consumes.liquid(Liquids.water, 0.2f); }}; - pyratiteMixer = new GenericSmelter("pyratite-mixer"){{ + pyratiteMixer = new GenericCrafter("pyratite-mixer"){{ requirements(Category.crafting, with(Items.copper, 50, Items.lead, 25)); - flameColor = Color.clear; hasItems = true; hasPower = true; outputItem = new ItemStack(Items.pyratite, 1); @@ -757,7 +770,7 @@ public class Blocks implements ContentList{ plastaniumWall = new Wall("plastanium-wall"){{ requirements(Category.defense, with(Items.plastanium, 5, Items.metaglass, 2)); - health = 130 * wallHealthMultiplier; + health = 125 * wallHealthMultiplier; insulated = true; absorbLasers = true; schematicPriority = 10; @@ -765,7 +778,7 @@ public class Blocks implements ContentList{ plastaniumWallLarge = new Wall("plastanium-wall-large"){{ requirements(Category.defense, ItemStack.mult(plastaniumWall.requirements, 4)); - health = 130 * wallHealthMultiplier * 4; + health = 125 * wallHealthMultiplier * 4; size = 2; insulated = true; absorbLasers = true; @@ -972,7 +985,6 @@ public class Blocks implements ContentList{ phaseConveyor = new ItemBridge("phase-conveyor"){{ requirements(Category.distribution, with(Items.phaseFabric, 5, Items.silicon, 7, Items.lead, 10, Items.graphite, 10)); range = 12; - canOverdrive = false; hasPower = true; consumes.power(0.30f); }}; @@ -1018,14 +1030,20 @@ public class Blocks implements ContentList{ consumes.power(1.75f); }}; - payloadConveyor = new PayloadConveyor("payload-conveyor"){{ - requirements(Category.distribution, with(Items.graphite, 10, Items.copper, 20)); - canOverdrive = false; + //special transport blocks + + duct = new Duct("duct"){{ + requirements(Category.distribution, BuildVisibility.debugOnly, with(Items.graphite, 5, Items.copper, 5)); + speed = 5f; }}; - payloadRouter = new PayloadRouter("payload-router"){{ - requirements(Category.distribution, with(Items.graphite, 15, Items.copper, 20)); - canOverdrive = false; + ductRouter = new DuctRouter("duct-router"){{ + requirements(Category.distribution, BuildVisibility.debugOnly, with(Items.graphite, 10, Items.copper, 5)); + speed = 5f; + }}; + + ductBridge = new DuctBridge("duct-bridge"){{ + requirements(Category.distribution, BuildVisibility.debugOnly, with(Items.graphite, 20, Items.copper, 15)); }}; //endregion @@ -1294,21 +1312,30 @@ public class Blocks implements ContentList{ liquidCapacity = 30f; rotateSpeed = 1.4f; attribute = Attribute.water; + envRequired |= Env.groundWater; consumes.power(1.5f); }}; - cultivator = new Cultivator("cultivator"){{ + cultivator = new AttributeCrafter("cultivator"){{ requirements(Category.production, with(Items.copper, 25, Items.lead, 25, Items.silicon, 10)); outputItem = new ItemStack(Items.sporePod, 1); - craftTime = 140; + craftTime = 100; size = 2; hasLiquids = true; hasPower = true; hasItems = true; - consumes.power(0.9f); - consumes.liquid(Liquids.water, 0.2f); + craftEffect = Fx.none; + envRequired |= Env.spores; + attribute = Attribute.spores; + + legacyReadWarmup = true; + drawer = new DrawCultivator(); + maxBoost = 2f; + + consumes.power(80f / 60f); + consumes.liquid(Liquids.water, 20f / 60f); }}; oilExtractor = new Fracker("oil-extractor"){{ @@ -1353,7 +1380,7 @@ public class Blocks implements ContentList{ size = 4; unitCapModifier = 16; - researchCostMultiplier = 0.04f; + researchCostMultiplier = 0.07f; }}; coreNucleus = new CoreBlock("core-nucleus"){{ @@ -1365,24 +1392,26 @@ public class Blocks implements ContentList{ size = 5; unitCapModifier = 24; - researchCostMultiplier = 0.06f; + researchCostMultiplier = 0.11f; }}; vault = new StorageBlock("vault"){{ requirements(Category.effect, with(Items.titanium, 250, Items.thorium, 125)); size = 3; itemCapacity = 1000; + health = size * size * 55; }}; container = new StorageBlock("container"){{ requirements(Category.effect, with(Items.titanium, 100)); size = 2; itemCapacity = 300; + health = size * size * 55; }}; unloader = new Unloader("unloader"){{ requirements(Category.effect, with(Items.titanium, 25, Items.silicon, 30)); - speed = 6f; + speed = 60f / 11f; group = BlockGroup.transportation; }}; @@ -1403,12 +1432,14 @@ public class Blocks implements ContentList{ alternate = true; reloadTime = 20f; restitution = 0.03f; - range = 100; + range = 110; shootCone = 15f; ammoUseEffect = Fx.casing1; health = 250; inaccuracy = 2f; rotateSpeed = 10f; + + limitRange(); }}; scatter = new ItemTurret("scatter"){{ @@ -1419,7 +1450,7 @@ public class Blocks implements ContentList{ Items.metaglass, Bullets.flakGlass ); reloadTime = 18f; - range = 160f; + range = 220f; size = 2; burstSpacing = 5f; shots = 2; @@ -1432,6 +1463,8 @@ public class Blocks implements ContentList{ health = 200 * size * size; shootSound = Sounds.shootSnap; + + limitRange(2); }}; scorch = new ItemTurret("scorch"){{ @@ -1461,11 +1494,12 @@ public class Blocks implements ContentList{ targetAir = false; reloadTime = 60f; recoilAmount = 2f; - range = 230f; + range = 235f; inaccuracy = 1f; shootCone = 10f; health = 260; shootSound = Sounds.bang; + limitRange(0f); }}; wave = new LiquidTurret("wave"){{ @@ -1478,7 +1512,7 @@ public class Blocks implements ContentList{ ); size = 2; recoilAmount = 0f; - reloadTime = 2f; + reloadTime = 3f; inaccuracy = 5f; shootCone = 50f; liquidCapacity = 10f; @@ -1512,12 +1546,12 @@ public class Blocks implements ContentList{ shootType = new LaserBulletType(140){{ colors = new Color[]{Pal.lancerLaser.cpy().a(0.4f), Pal.lancerLaser, Color.white}; hitEffect = Fx.hitLancer; - despawnEffect = Fx.none; hitSize = 4; lifetime = 16f; drawSize = 400f; collidesAir = false; length = 173f; + ammoMultiplier = 1f; }}; }}; @@ -1527,6 +1561,7 @@ public class Blocks implements ContentList{ damage = 20; lightningLength = 25; collidesAir = false; + ammoMultiplier = 1f; }}; reloadTime = 35f; shootCone = 40f; @@ -1547,9 +1582,9 @@ public class Blocks implements ContentList{ hasPower = true; size = 2; - force = 8f; - scaledForce = 7f; - range = 230f; + force = 12f; + scaledForce = 6f; + range = 240f; damage = 0.3f; health = 160 * size * size; rotateSpeed = 10; @@ -1568,15 +1603,17 @@ public class Blocks implements ContentList{ shots = 4; burstSpacing = 5; inaccuracy = 10f; - range = 200f; + range = 210f; xRand = 6f; size = 2; health = 300 * size * size; shootSound = Sounds.missile; + + limitRange(2f); }}; salvo = new ItemTurret("salvo"){{ - requirements(Category.turret, with(Items.copper, 100, Items.graphite, 90, Items.titanium, 60)); + requirements(Category.turret, with(Items.copper, 100, Items.graphite, 80, Items.titanium, 50)); ammo( Items.copper, Bullets.standardCopper, Items.graphite, Bullets.standardDense, @@ -1586,7 +1623,7 @@ public class Blocks implements ContentList{ ); size = 2; - range = 150f; + range = 180f; reloadTime = 38f; restitution = 0.03f; ammoEjectBack = 3f; @@ -1598,6 +1635,8 @@ public class Blocks implements ContentList{ ammoUseEffect = Fx.casing2; health = 240 * size * size; shootSound = Sounds.shootBig; + + limitRange(); }}; segment = new PointDefenseTurret("segment"){{ @@ -1610,7 +1649,7 @@ public class Blocks implements ContentList{ size = 2; shootLength = 5f; bulletDamage = 30f; - reloadTime = 9f; + reloadTime = 8f; }}; tsunami = new LiquidTurret("tsunami"){{ @@ -1622,7 +1661,7 @@ public class Blocks implements ContentList{ Liquids.oil, Bullets.heavyOilShot ); size = 3; - reloadTime = 2f; + reloadTime = 3f; shots = 2; velocityInaccuracy = 0.1f; inaccuracy = 4f; @@ -1721,6 +1760,7 @@ public class Blocks implements ContentList{ shootSound = Sounds.shootSnap; health = 145 * size * size; + limitRange(); }}; foreshadow = new ItemTurret("foreshadow"){{ @@ -1736,7 +1776,7 @@ public class Blocks implements ContentList{ despawnEffect = Fx.instBomb; trailSpacing = 20f; damage = 1350; - buildingDamageMultiplier = 0.3f; + buildingDamageMultiplier = 0.25f; speed = brange; hitShake = 6f; ammoMultiplier = 1f; @@ -1744,7 +1784,7 @@ public class Blocks implements ContentList{ ); maxAmmo = 40; - ammoPerShot = 4; + ammoPerShot = 5; rotateSpeed = 2f; reloadTime = 200f; ammoUseEffect = Fx.casing3Double; @@ -1773,11 +1813,11 @@ public class Blocks implements ContentList{ Items.pyratite, Bullets.standardIncendiaryBig, Items.thorium, Bullets.standardThoriumBig ); - reloadTime = 6f; + reloadTime = 7f; coolantMultiplier = 0.5f; restitution = 0.1f; ammoUseEffect = Fx.casing3; - range = 200f; + range = 260f; inaccuracy = 3f; recoilAmount = 3f; spread = 8f; @@ -1790,6 +1830,8 @@ public class Blocks implements ContentList{ health = 160 * size * size; coolantUsage = 1f; + + limitRange(); }}; meltdown = new LaserTurret("meltdown"){{ @@ -1799,7 +1841,7 @@ public class Blocks implements ContentList{ recoilAmount = 4f; size = 4; shootShake = 2f; - range = 190f; + range = 195f; reloadTime = 90f; firingMoveFract = 0.5f; shootDuration = 220f; @@ -1817,6 +1859,7 @@ public class Blocks implements ContentList{ incendChance = 0.4f; incendSpread = 5f; incendAmount = 1; + ammoMultiplier = 1f; }}; health = 200 * size * size; @@ -1856,7 +1899,8 @@ public class Blocks implements ContentList{ navalFactory = new UnitFactory("naval-factory"){{ requirements(Category.units, with(Items.copper, 150, Items.lead, 130, Items.metaglass, 120)); plans = Seq.with( - new UnitPlan(UnitTypes.risso, 60f * 45f, with(Items.silicon, 20, Items.metaglass, 35)) + new UnitPlan(UnitTypes.risso, 60f * 45f, with(Items.silicon, 20, Items.metaglass, 35)), + new UnitPlan(UnitTypes.retusa, 60f * 60f, with(Items.silicon, 15, Items.metaglass, 25, Items.titanium, 20)) ); size = 3; consumes.power(1.2f); @@ -1878,7 +1922,8 @@ public class Blocks implements ContentList{ new UnitType[]{UnitTypes.crawler, UnitTypes.atrax}, new UnitType[]{UnitTypes.flare, UnitTypes.horizon}, new UnitType[]{UnitTypes.mono, UnitTypes.poly}, - new UnitType[]{UnitTypes.risso, UnitTypes.minke} + new UnitType[]{UnitTypes.risso, UnitTypes.minke}, + new UnitType[]{UnitTypes.retusa, UnitTypes.oxynoe} ); }}; @@ -1944,10 +1989,26 @@ public class Blocks implements ContentList{ }}; repairPoint = new RepairPoint("repair-point"){{ - requirements(Category.units, with(Items.lead, 15, Items.copper, 15, Items.silicon, 15)); + requirements(Category.units, with(Items.lead, 20, Items.copper, 20, Items.silicon, 15)); repairSpeed = 0.5f; repairRadius = 65f; + beamWidth = 0.73f; powerUse = 1f; + pulseRadius = 5f; + }}; + + repairTurret = new RepairPoint("repair-turret"){{ + requirements(Category.units, with(Items.silicon, 70, Items.thorium, 60, Items.plastanium, 60)); + size = 2; + length = 6f; + repairSpeed = 5f; + repairRadius = 140f; + powerUse = 5f; + beamWidth = 1.1f; + pulseRadius = 6.1f; + coolantUse = 0.15f; + coolantMultiplier = 1.7f; + acceptCoolant = true; }}; resupplyPoint = new ResupplyPoint("resupply-point"){{ @@ -1961,6 +2022,28 @@ public class Blocks implements ContentList{ consumes.item(Items.copper, 1); }}; + //endregion + //region payloads + + payloadConveyor = new PayloadConveyor("payload-conveyor"){{ + requirements(Category.units, with(Items.graphite, 10, Items.copper, 20)); + canOverdrive = false; + }}; + + payloadRouter = new PayloadRouter("payload-router"){{ + requirements(Category.units, with(Items.graphite, 15, Items.copper, 20)); + canOverdrive = false; + }}; + + payloadPropulsionTower = new PayloadMassDriver("payload-propulsion-tower"){{ + requirements(Category.units, with(Items.thorium, 300, Items.silicon, 200, Items.plastanium, 200, Items.phaseFabric, 50)); + size = 5; + reloadTime = 150f; + chargeTime = 100f; + range = 300f; + consumes.power(10f); + }}; + //endregion //region sandbox @@ -1995,10 +2078,21 @@ public class Blocks implements ContentList{ alwaysUnlocked = true; }}; + payloadVoid = new PayloadVoid("payload-void"){{ + requirements(Category.units, BuildVisibility.sandboxOnly, with()); + size = 5; + }}; + + payloadSource = new PayloadSource("payload-source"){{ + requirements(Category.units, BuildVisibility.sandboxOnly, with()); + size = 5; + }}; + + //TODO move illuminator = new LightBlock("illuminator"){{ requirements(Category.effect, BuildVisibility.lightingOnly, with(Items.graphite, 12, Items.silicon, 8)); brightness = 0.75f; - radius = 120f; + radius = 140f; consumes.power(0.05f); }}; @@ -2027,16 +2121,6 @@ public class Blocks implements ContentList{ consumes.power(4f); }}; - //TODO remove - launchPadLarge = new LaunchPad("launch-pad-large"){{ - requirements(Category.effect, BuildVisibility.debugOnly, ItemStack.with(Items.titanium, 200, Items.silicon, 150, Items.lead, 250, Items.plastanium, 75)); - size = 4; - itemCapacity = 300; - launchTime = 60f * 35; - hasPower = true; - consumes.power(6f); - }}; - interplanetaryAccelerator = new Accelerator("interplanetary-accelerator"){{ requirements(Category.effect, BuildVisibility.campaignOnly, with(Items.copper, 16000, Items.silicon, 11000, Items.thorium, 13000, Items.titanium, 12000, Items.surgeAlloy, 6000, Items.phaseFabric, 5000)); researchCostMultiplier = 0.1f; @@ -2059,7 +2143,7 @@ public class Blocks implements ContentList{ }}; microProcessor = new LogicBlock("micro-processor"){{ - requirements(Category.logic, with(Items.copper, 80, Items.lead, 50, Items.silicon, 30)); + requirements(Category.logic, with(Items.copper, 90, Items.lead, 50, Items.silicon, 50)); instructionsPerTick = 2; @@ -2067,7 +2151,7 @@ public class Blocks implements ContentList{ }}; logicProcessor = new LogicBlock("logic-processor"){{ - requirements(Category.logic, with(Items.lead, 320, Items.silicon, 60, Items.graphite, 60, Items.thorium, 50)); + requirements(Category.logic, with(Items.lead, 320, Items.silicon, 80, Items.graphite, 60, Items.thorium, 50)); instructionsPerTick = 8; @@ -2077,7 +2161,7 @@ public class Blocks implements ContentList{ }}; hyperProcessor = new LogicBlock("hyper-processor"){{ - requirements(Category.logic, with(Items.lead, 450, Items.silicon, 130, Items.thorium, 75, Items.surgeAlloy, 50)); + requirements(Category.logic, with(Items.lead, 450, Items.silicon, 150, Items.thorium, 75, Items.surgeAlloy, 50)); consumes.liquid(Liquids.cryofluid, 0.08f); hasLiquids = true; @@ -2122,21 +2206,21 @@ public class Blocks implements ContentList{ //region experimental blockForge = new BlockForge("block-forge"){{ - requirements(Category.crafting, BuildVisibility.debugOnly, with(Items.thorium, 100)); + requirements(Category.units, BuildVisibility.debugOnly, with(Items.thorium, 100)); hasPower = true; consumes.power(2f); size = 3; }}; blockLoader = new BlockLoader("block-loader"){{ - requirements(Category.distribution, BuildVisibility.debugOnly, with(Items.thorium, 100)); + requirements(Category.units, BuildVisibility.debugOnly, with(Items.thorium, 100)); hasPower = true; consumes.power(2f); size = 3; }}; blockUnloader = new BlockUnloader("block-unloader"){{ - requirements(Category.distribution, BuildVisibility.debugOnly, with(Items.thorium, 100)); + requirements(Category.units, BuildVisibility.debugOnly, with(Items.thorium, 100)); hasPower = true; consumes.power(2f); size = 3; diff --git a/core/src/mindustry/content/Bullets.java b/core/src/mindustry/content/Bullets.java index 6cc2a15757..e318d80f98 100644 --- a/core/src/mindustry/content/Bullets.java +++ b/core/src/mindustry/content/Bullets.java @@ -9,7 +9,6 @@ import mindustry.entities.*; import mindustry.entities.bullet.*; import mindustry.gen.*; import mindustry.graphics.*; -import mindustry.io.*; import mindustry.world.*; import static mindustry.Vars.*; @@ -53,8 +52,7 @@ public class Bullets implements ContentList{ }}; //this is just a copy of the damage lightning bullet that doesn't damage air units - damageLightningGround = new BulletType(0.0001f, 0f){}; - JsonIO.copy(damageLightning, damageLightningGround); + damageLightningGround = damageLightning.copy(); damageLightningGround.collidesAir = false; artilleryDense = new ArtilleryBulletType(3f, 20, "shell"){{ @@ -262,7 +260,7 @@ public class Bullets implements ContentList{ drag = -0.01f; splashDamageRadius = 30f; splashDamage = 30f * 1.5f; - ammoMultiplier = 4f; + ammoMultiplier = 5f; hitEffect = Fx.blastExplosion; despawnEffect = Fx.blastExplosion; @@ -281,6 +279,7 @@ public class Bullets implements ContentList{ splashDamageRadius = 20f; splashDamage = 20f * 1.5f; makeFire = true; + ammoMultiplier = 5f; hitEffect = Fx.blastExplosion; status = StatusEffects.burning; }}; @@ -291,9 +290,10 @@ public class Bullets implements ContentList{ shrinkY = 0f; drag = -0.01f; splashDamageRadius = 25f; - splashDamage = 25f * 1.5f; + splashDamage = 25f * 1.4f; hitEffect = Fx.blastExplosion; despawnEffect = Fx.blastExplosion; + ammoMultiplier = 4f; lightningDamage = 10; lightning = 2; lightningLength = 10; @@ -346,12 +346,14 @@ public class Bullets implements ContentList{ }}; standardDenseBig = new BasicBulletType(7f, 55, "bullet"){{ + hitSize = 5; width = 15f; height = 21f; shootEffect = Fx.shootBig; }}; standardThoriumBig = new BasicBulletType(8f, 80, "bullet"){{ + hitSize = 5; width = 16f; height = 23f; shootEffect = Fx.shootBig; @@ -361,6 +363,7 @@ public class Bullets implements ContentList{ }}; standardIncendiaryBig = new BasicBulletType(7f, 60, "bullet"){{ + hitSize = 5; width = 16f; height = 21f; frontColor = Pal.lightishOrange; diff --git a/core/src/mindustry/content/Fx.java b/core/src/mindustry/content/Fx.java index fa492d0bee..f0bef945ce 100644 --- a/core/src/mindustry/content/Fx.java +++ b/core/src/mindustry/content/Fx.java @@ -12,7 +12,6 @@ import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.type.*; -import mindustry.ui.*; import static arc.graphics.g2d.Draw.rect; import static arc.graphics.g2d.Draw.*; @@ -21,16 +20,29 @@ import static arc.math.Angles.*; import static mindustry.Vars.*; public class Fx{ + private static final Rand rand = new Rand(); + private static final Vec2 v = new Vec2(); + public static final Effect none = new Effect(0, 0f, e -> {}), + trailFade = new Effect(400f, e -> { + if(!(e.data instanceof Trail trail)) return; + //lifetime is how many frames it takes to fade out the trail + e.lifetime = trail.length * 1.4f; + + trail.shorten(); + trail.drawCap(e.color, e.rotation); + trail.draw(e.color, e.rotation); + }), + unitSpawn = new Effect(30f, e -> { if(!(e.data instanceof UnitType unit)) return; float scl = 1f + e.fout() * 2f; - TextureRegion region = unit.icon(Cicon.full); + TextureRegion region = unit.fullIcon; alpha(e.fout()); mixcol(Color.white, e.fin()); @@ -41,9 +53,7 @@ public class Fx{ alpha(e.fin()); - rect(region, e.x, e.y, - region.width * Draw.scl * scl, region.height * Draw.scl * scl, e.rotation - 90); - + rect(region, e.x, e.y, region.width * Draw.scl * scl, region.height * Draw.scl * scl, e.rotation - 90); }), unitCapKill = new Effect(80f, e -> { @@ -62,7 +72,7 @@ public class Fx{ mixcol(Pal.accent, 1f); alpha(e.fout()); - rect(block ? ((BlockUnitc)select).tile().block.icon(Cicon.full) : select.type.icon(Cicon.full), select.x, select.y, block ? 0f : select.rotation - 90f); + rect(block ? ((BlockUnitc)select).tile().block.fullIcon : select.type.fullIcon, select.x, select.y, block ? 0f : select.rotation - 90f); alpha(1f); Lines.stroke(e.fslope()); Lines.square(select.x, select.y, e.fout() * select.hitSize * 2f, 45); @@ -80,7 +90,7 @@ public class Fx{ Draw.scl *= scl; mixcol(Pal.accent, 1f); - rect(select.type.icon(Cicon.full), select.x, select.y, select.rotation - 90f); + rect(select.type.fullIcon, select.x, select.y, select.rotation - 90f); reset(); Draw.scl = p; @@ -125,10 +135,10 @@ public class Fx{ Position pos = e.data(); - Draw.color(e.color); - Draw.alpha(e.fout()); + Draw.color(e.color, e.fout()); Lines.stroke(1.5f); Lines.line(e.x, e.y, pos.getX(), pos.getY()); + Drawf.light(null, e.x, e.y, pos.getX(), pos.getY(), 20f, e.color, 0.6f * e.fout()); }), pointHit = new Effect(8f, e -> { @@ -258,6 +268,14 @@ public class Fx{ Fill.circle(e.x + trnsx(ang, len), e.y + trnsy(ang, len), 2f * e.fout()); }), + breakProp = new Effect(23, e -> { + float scl = Math.max(e.rotation, 1); + color(Tmp.c1.set(e.color).mul(1.1f)); + randLenVectors(e.id, 6, 19f * e.finpow() * scl, (x, y) -> { + Fill.circle(e.x + x, e.y + y, e.fout() * 3.5f * scl + 0.3f); + }); + }).layer(Layer.debris), + unitDrop = new Effect(30, e -> { color(Pal.lightishGray); randLenVectors(e.id, 9, 3 + 20f * e.finpow(), (x, y) -> { @@ -300,7 +318,8 @@ public class Fx{ greenBomb = new Effect(40f, 100f, e -> { color(Pal.heal); stroke(e.fout() * 2f); - Lines.circle(e.x, e.y, 4f + e.finpow() * 65f); + float circleRad = 4f + e.finpow() * 65f; + Lines.circle(e.x, e.y, circleRad); color(Pal.heal); for(int i = 0; i < 4; i++){ @@ -311,6 +330,8 @@ public class Fx{ for(int i = 0; i < 4; i++){ Drawf.tri(e.x, e.y, 3f, 35f * e.fout(), i*90); } + + Drawf.light(e.x, e.y, circleRad * 1.6f, Pal.heal, e.fout()); }), greenLaserCharge = new Effect(80f, 100f, e -> { @@ -322,11 +343,13 @@ public class Fx{ randLenVectors(e.id, 20, 40f * e.fout(), (x, y) -> { Fill.circle(e.x + x, e.y + y, e.fin() * 5f); + Drawf.light(e.x + x, e.y + y, e.fin() * 15f, Pal.heal, 0.7f); }); color(); Fill.circle(e.x, e.y, e.fin() * 10); + Drawf.light(e.x, e.y, e.fin() * 20f, Pal.heal, 0.7f); }), greenLaserChargeSmall = new Effect(40f, 100f, e -> { @@ -335,6 +358,13 @@ public class Fx{ Lines.circle(e.x, e.y, e.fout() * 50f); }), + greenCloud = new Effect(80f, e -> { + color(Pal.heal); + randLenVectors(e.id, e.fin(), 7, 9f, (x, y, fin, fout) -> { + Fill.circle(e.x + x, e.y + y, 5f * fout); + }); + }), + healWaveDynamic = new Effect(22, e -> { color(Pal.heal); stroke(e.fout() * 2f); @@ -353,14 +383,20 @@ public class Fx{ Lines.circle(e.x, e.y, 2f + e.finpow() * 7f); }), + healDenamic = new Effect(11, e -> { + color(Pal.heal); + stroke(e.fout() * 2f); + Lines.circle(e.x, e.y, 2f + e.finpow() * e.rotation); + }), + shieldWave = new Effect(22, e -> { - color(Pal.shield); + color(e.color, 0.7f); stroke(e.fout() * 2f); Lines.circle(e.x, e.y, 4f + e.finpow() * 60f); }), shieldApply = new Effect(11, e -> { - color(Pal.shield); + color(e.color, 0.7f); stroke(e.fout() * 2f); Lines.circle(e.x, e.y, 2f + e.finpow() * 7f); }), @@ -379,6 +415,8 @@ public class Fx{ float ang = Mathf.angle(x, y); lineAngle(e.x + x, e.y + y, ang, e.fout() * 3 + 1f); }); + + Drawf.light(e.x, e.y, 20f, Pal.lightOrange, 0.6f * e.fout()); }), hitFuse = new Effect(14, e -> { @@ -417,6 +455,16 @@ public class Fx{ }); }), + hitFlamePlasma = new Effect(14, e -> { + color(Color.white, Pal.heal, e.fin()); + stroke(0.5f + e.fout()); + + randLenVectors(e.id, 2, e.fin() * 15f, e.rotation, 50f, (x, y) -> { + float ang = Mathf.angle(x, y); + lineAngle(e.x + x, e.y + y, ang, e.fout() * 3 + 1f); + }); + }), + hitLiquid = new Effect(16, e -> { color(e.color); @@ -435,6 +483,16 @@ public class Fx{ }); }), + hitEmpSpark = new Effect(40, e -> { + color(Pal.heal); + stroke(e.fout() * 1.6f); + + randLenVectors(e.id, 18, e.finpow() * 27f, e.rotation, 360f, (x, y) -> { + float ang = Mathf.angle(x, y); + lineAngle(e.x + x, e.y + y, ang, e.fout() * 6 + 1f); + }); + }), + hitLancer = new Effect(12, e -> { color(Color.white); stroke(e.fout() * 1.5f); @@ -488,6 +546,8 @@ public class Fx{ for(int i = 0; i < 4; i++){ Drawf.tri(e.x, e.y, 3f, 30f * e.fout(), i*90 + 45); } + + Drawf.light(e.x, e.y, 150f, Pal.bulletYellowBack, 0.9f * e.fout()); }), instTrail = new Effect(30, e -> { @@ -501,6 +561,8 @@ public class Fx{ Drawf.tri(e.x, e.y, w, (30f + Mathf.randomSeedRange(e.id, 15f)) * m, rot); Drawf.tri(e.x, e.y, w, 10f * m, rot + 180f); } + + Drawf.light(e.x, e.y, 60f, Pal.bulletYellowBack, 0.6f * e.fout()); }), instShoot = new Effect(24f, e -> { @@ -516,6 +578,8 @@ public class Fx{ Drawf.tri(e.x, e.y, 13f * e.fout(), 85f, e.rotation + 90f * i); Drawf.tri(e.x, e.y, 13f * e.fout(), 50f, e.rotation + 20f * i); } + + Drawf.light(e.x, e.y, 180f, Pal.bulletYellowBack, 0.9f * e.fout()); }), instHit = new Effect(20f, 200f, e -> { @@ -552,6 +616,8 @@ public class Fx{ color(Color.white, Pal.heal, e.fin()); stroke(0.5f + e.fout()); Lines.circle(e.x, e.y, e.fin() * 5f); + + Drawf.light(e.x, e.y, 23f, Pal.heal, e.fout() * 0.7f); }), hitYellowLaser = new Effect(8, e -> { @@ -571,6 +637,12 @@ public class Fx{ }), + airBubble = new Effect(100f, e -> { + randLenVectors(e.id, 1, e.fin() * 12f, (x, y) -> { + rect(renderer.bubbles[Math.min((int)(renderer.bubbles.length * Mathf.curveMargin(e.fin(), 0.11f, 0.06f)), renderer.bubbles.length - 1)], e.x + x, e.y + y); + }); + }).layer(Layer.flyingUnitLow + 1), + flakExplosion = new Effect(20, e -> { color(Pal.bulletYellow); @@ -591,6 +663,8 @@ public class Fx{ randLenVectors(e.id + 1, 4, 1f + 23f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f); }); + + Drawf.light(e.x, e.y, 50f, Pal.lighterOrange, 0.8f * e.fout()); }), plasticExplosion = new Effect(24, e -> { @@ -613,6 +687,8 @@ public class Fx{ randLenVectors(e.id + 1, 4, 1f + 25f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f); }); + + Drawf.light(e.x, e.y, 50f, Pal.plastaniumBack, 0.8f * e.fout()); }), plasticExplosionFlak = new Effect(28, e -> { @@ -657,6 +733,8 @@ public class Fx{ randLenVectors(e.id + 1, 4, 1f + 23f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f); }); + + Drawf.light(e.x, e.y, 45f, Pal.missileYellowBack, 0.8f * e.fout()); }), sapExplosion = new Effect(25, e -> { @@ -679,6 +757,8 @@ public class Fx{ randLenVectors(e.id + 1, 8, 1f + 60f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f); }); + + Drawf.light(e.x, e.y, 90f, Pal.sapBulletBack, 0.8f * e.fout()); }), massiveExplosion = new Effect(30, e -> { @@ -701,6 +781,8 @@ public class Fx{ randLenVectors(e.id + 1, 6, 1f + 29f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 4f); }); + + Drawf.light(e.x, e.y, 50f, Pal.missileYellowBack, 0.8f * e.fout()); }), artilleryTrail = new Effect(50, e -> { @@ -716,7 +798,7 @@ public class Fx{ missileTrail = new Effect(50, e -> { color(e.color); Fill.circle(e.x, e.y, e.rotation * e.fout()); - }), + }).layer(Layer.bullet - 0.001f), //below bullets absorb = new Effect(12, e -> { color(Pal.accent); @@ -757,6 +839,8 @@ public class Fx{ randLenVectors(e.id + 1, 4, 1f + 23f * e.finpow(), (x, y) -> { lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * 3f); }); + + Drawf.light(e.x, e.y, 60f, Pal.bulletYellowBack, 0.7f * e.fout()); }), burning = new Effect(35f, e -> { @@ -767,6 +851,12 @@ public class Fx{ }); }), + fireRemove = new Effect(70f, e -> { + if(Fire.regions[0] == null) return; + alpha(e.fout()); + rect(Fire.regions[((int)(e.rotation + e.fin() * Fire.frames)) % Fire.frames], e.x, e.y); + }), + fire = new Effect(50f, e -> { color(Pal.lightFlame, Pal.darkFlame, e.fin()); @@ -849,6 +939,14 @@ public class Fx{ }); }), + electrified = new Effect(40f, e -> { + color(Pal.heal); + + randLenVectors(e.id, 2, 1f + e.fin() * 2f, (x, y) -> { + Fill.square(e.x + x, e.y + y, e.fslope() * 1.1f, 45f); + }); + }), + sporeSlowed = new Effect(40f, e -> { color(Pal.spore); @@ -881,7 +979,7 @@ public class Fx{ float length = 20f * e.finpow(); float size = 7f * e.fout(); - rect(((Item)e.data).icon(Cicon.medium), e.x + trnsx(e.rotation, length), e.y + trnsy(e.rotation, length), size, size); + rect(((Item)e.data).fullIcon, e.x + trnsx(e.rotation, length), e.y + trnsy(e.rotation, length), size, size); }), shockwave = new Effect(10f, 80f, e -> { @@ -935,26 +1033,82 @@ public class Fx{ }); }), - dynamicExplosion = new Effect(30, 100f, e -> { - float intensity = e.rotation; - - e.scaled(5 + intensity * 2, i -> { - stroke(3.1f * i.fout()); - Lines.circle(e.x, e.y, (3f + i.fin() * 14f) * intensity); - }); + dynamicExplosion = new Effect(30, 500f, b -> { + float intensity = b.rotation; + float baseLifetime = 26f + intensity * 15f; + b.lifetime = 43f + intensity * 35f; color(Color.gray); + //TODO awful borders with linear filtering here + alpha(0.9f); + for(int i = 0; i < 4; i++){ + rand.setSeed(b.id*2 + i); + float lenScl = rand.random(0.4f, 1f); + int fi = i; + b.scaled(b.lifetime * lenScl, e -> { + randLenVectors(e.id + fi - 1, e.fin(Interp.pow10Out), (int)(3f * intensity), 14f * intensity, (x, y, in, out) -> { + float fout = e.fout(Interp.pow5Out) * rand.random(0.5f, 1f); + Fill.circle(e.x + x, e.y + y, fout * ((2f + intensity) * 1.8f)); + }); + }); + } - randLenVectors(e.id, e.finpow(), (int)(6 * intensity), 21f * intensity, (x, y, in, out) -> { - Fill.circle(e.x + x, e.y + y, out * (2f + intensity) * 3 + 0.5f); - Fill.circle(e.x + x / 2f, e.y + y / 2f, out * (intensity) * 3); + b.scaled(baseLifetime, e -> { + e.scaled(5 + intensity * 2.5f, i -> { + stroke((3.1f + intensity/5f) * i.fout()); + Lines.circle(e.x, e.y, (3f + i.fin() * 14f) * intensity); + Drawf.light(e.x, e.y, i.fin() * 14f * 2f * intensity, Color.white, 0.9f * e.fout()); + }); + + color(Pal.lighterOrange, Pal.lightOrange, Color.gray, e.fin()); + stroke((1.7f * e.fout()) * (1f + (intensity - 1f) / 2f)); + + Draw.z(Layer.effect + 0.001f); + randLenVectors(e.id + 1, e.finpow() + 0.001f, (int)(9 * intensity), 40f * intensity, (x, y, in, out) -> { + lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + out * 4 * (3f + intensity)); + Drawf.light(e.x + x, e.y + y, (out * 4 * (3f + intensity)) * 3.5f, Draw.getColor(), 0.8f); + }); }); + }), - color(Pal.lighterOrange, Pal.lightOrange, Color.gray, e.fin()); - stroke((1.7f * e.fout()) * (1f + (intensity - 1f) / 2f)); + reactorExplosion = new Effect(30, 500f, b -> { + float intensity = 6.8f; + float baseLifetime = 25f + intensity * 11f; + b.lifetime = 50f + intensity * 65f; - randLenVectors(e.id + 1, e.finpow(), (int)(9 * intensity), 40f * intensity, (x, y, in, out) -> { - lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + out * 4 * (3f + intensity)); + color(Pal.reactorPurple2); + alpha(0.7f); + for(int i = 0; i < 4; i++){ + rand.setSeed(b.id*2 + i); + float lenScl = rand.random(0.4f, 1f); + int fi = i; + b.scaled(b.lifetime * lenScl, e -> { + randLenVectors(e.id + fi - 1, e.fin(Interp.pow10Out), (int)(2.9f * intensity), 22f * intensity, (x, y, in, out) -> { + float fout = e.fout(Interp.pow5Out) * rand.random(0.5f, 1f); + float rad = fout * ((2f + intensity) * 2.35f); + + Fill.circle(e.x + x, e.y + y, rad); + Drawf.light(e.x + x, e.y + y, rad * 2.5f, Pal.reactorPurple, 0.5f); + }); + }); + } + + b.scaled(baseLifetime, e -> { + Draw.color(); + e.scaled(5 + intensity * 2f, i -> { + stroke((3.1f + intensity/5f) * i.fout()); + Lines.circle(e.x, e.y, (3f + i.fin() * 14f) * intensity); + Drawf.light(e.x, e.y, i.fin() * 14f * 2f * intensity, Color.white, 0.9f * e.fout()); + }); + + color(Pal.lighterOrange, Pal.reactorPurple, e.fin()); + stroke((2f * e.fout())); + + Draw.z(Layer.effect + 0.001f); + randLenVectors(e.id + 1, e.finpow() + 0.001f, (int)(8 * intensity), 28f * intensity, (x, y, in, out) -> { + lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + out * 4 * (4f + intensity)); + Drawf.light(e.x + x, e.y + y, (out * 4 * (3f + intensity)) * 3.5f, Draw.getColor(), 0.8f); + }); }); }), @@ -1047,6 +1201,19 @@ public class Fx{ }); }), + shootPayloadDriver = new Effect(30f, e -> { + color(Pal.accent); + Lines.stroke(0.5f + 0.5f*e.fout()); + float spread = 9f; + + rand.setSeed(e.id); + for(int i = 0; i < 20; i++){ + float ang = e.rotation + rand.range(17f); + v.trns(ang, rand.random(e.fin() * 55f)); + Lines.lineAngle(e.x + v.x + rand.range(spread), e.y + v.y + rand.range(spread), ang, e.fout() * 5f * rand.random(1f) + 1f); + } + }), + shootSmallFlame = new Effect(32f, 80f, e -> { color(Pal.lightFlame, Pal.darkFlame, Color.gray, e.fin()); @@ -1188,6 +1355,8 @@ public class Fx{ for(int i : Mathf.signs){ Drawf.tri(e.x, e.y, 10f * e.fout(), 24f, e.rotation + 90 + 90f * i); } + + Drawf.light(e.x, e.y, 60f * e.fout(), Pal.orangeSpark, 0.5f); }), railHit = new Effect(18f, 200f, e -> { @@ -1478,30 +1647,44 @@ public class Fx{ Lines.spikes(e.x, e.y, e.fin() * 5f, 2, 8); }), + mineSmall = new Effect(30, e -> { + color(e.color, Color.lightGray, e.fin()); + randLenVectors(e.id, 3, e.fin() * 5f, (x, y) -> { + Fill.square(e.x + x, e.y + y, e.fout() + 0.5f, 45); + }); + }), + mine = new Effect(20, e -> { + color(e.color, Color.lightGray, e.fin()); randLenVectors(e.id, 6, 3f + e.fin() * 6f, (x, y) -> { - color(e.color, Color.lightGray, e.fin()); Fill.square(e.x + x, e.y + y, e.fout() * 2f, 45); }); }), mineBig = new Effect(30, e -> { + color(e.color, Color.lightGray, e.fin()); randLenVectors(e.id, 6, 4f + e.fin() * 8f, (x, y) -> { - color(e.color, Color.lightGray, e.fin()); Fill.square(e.x + x, e.y + y, e.fout() * 2f + 0.2f, 45); }); }), mineHuge = new Effect(40, e -> { + color(e.color, Color.lightGray, e.fin()); randLenVectors(e.id, 8, 5f + e.fin() * 10f, (x, y) -> { - color(e.color, Color.lightGray, e.fin()); Fill.square(e.x + x, e.y + y, e.fout() * 2f + 0.5f, 45); }); }), + payloadReceive = new Effect(30, e -> { + color(Color.white, Pal.accent, e.fin()); + randLenVectors(e.id, 12, 7f + e.fin() * 13f, (x, y) -> { + Fill.square(e.x + x, e.y + y, e.fout() * 2.1f + 0.5f, 45); + }); + }), + smelt = new Effect(20, e -> { + color(Color.white, e.color, e.fin()); randLenVectors(e.id, 6, 2f + e.fin() * 5f, (x, y) -> { - color(Color.white, e.color, e.fin()); Fill.square(e.x + x, e.y + y, 0.5f + e.fout() * 2f, 45); }); }), @@ -1635,7 +1818,7 @@ public class Fx{ float radius = unit.hitSize() * 1.3f; e.scaled(16f, c -> { - color(Pal.shield); + color(e.color, 0.9f); stroke(c.fout() * 2f + 0.1f); randLenVectors(e.id, (int)(radius * 1.2f), radius/2f + c.finpow() * radius*1.25f, (x, y) -> { @@ -1643,11 +1826,85 @@ public class Fx{ }); }); - color(Pal.shield, e.fout()); + color(e.color, e.fout() * 0.9f); stroke(e.fout()); Lines.circle(e.x, e.y, radius); }), + chainLightning = new Effect(20f, 300f, e -> { + if(!(e.data instanceof Position p)) return; + float tx = p.getX(), ty = p.getY(), dst = Mathf.dst(e.x, e.y, tx, ty); + Tmp.v1.set(p).sub(e.x, e.y).nor(); + + float normx = Tmp.v1.x, normy = Tmp.v1.y; + float range = 6f; + int links = Mathf.ceil(dst / range); + float spacing = dst / links; + + Lines.stroke(2.5f * e.fout()); + Draw.color(Color.white, e.color, e.fin()); + + Lines.beginLine(); + + Lines.linePoint(e.x, e.y); + + rand.setSeed(e.id); + + for(int i = 0; i < links; i++){ + float nx, ny; + if(i == links - 1){ + nx = tx; + ny = ty; + }else{ + float len = (i + 1) * spacing; + Tmp.v1.setToRandomDirection(rand).scl(range/2f); + nx = e.x + normx * len + Tmp.v1.x; + ny = e.y + normy * len + Tmp.v1.y; + } + + Lines.linePoint(nx, ny); + } + + Lines.endLine(); + }).followParent(false), + + chainEmp = new Effect(30f, 300f, e -> { + if(!(e.data instanceof Position p)) return; + float tx = p.getX(), ty = p.getY(), dst = Mathf.dst(e.x, e.y, tx, ty); + Tmp.v1.set(p).sub(e.x, e.y).nor(); + + float normx = Tmp.v1.x, normy = Tmp.v1.y; + float range = 6f; + int links = Mathf.ceil(dst / range); + float spacing = dst / links; + + Lines.stroke(4f * e.fout()); + Draw.color(Color.white, e.color, e.fin()); + + Lines.beginLine(); + + Lines.linePoint(e.x, e.y); + + rand.setSeed(e.id); + + for(int i = 0; i < links; i++){ + float nx, ny; + if(i == links - 1){ + nx = tx; + ny = ty; + }else{ + float len = (i + 1) * spacing; + Tmp.v1.setToRandomDirection(rand).scl(range/2f); + nx = e.x + normx * len + Tmp.v1.x; + ny = e.y + normy * len + Tmp.v1.y; + } + + Lines.linePoint(nx, ny); + } + + Lines.endLine(); + }).followParent(false), + coreLand = new Effect(120f, e -> { }); } diff --git a/core/src/mindustry/content/Items.java b/core/src/mindustry/content/Items.java index bd883c3ce9..360a965c45 100644 --- a/core/src/mindustry/content/Items.java +++ b/core/src/mindustry/content/Items.java @@ -5,8 +5,9 @@ import mindustry.ctype.*; import mindustry.type.*; public class Items implements ContentList{ - public static Item scrap, copper, lead, graphite, coal, titanium, thorium, silicon, plastanium, phaseFabric, surgeAlloy, - sporePod, sand, blastCompound, pyratite, metaglass; + public static Item + scrap, copper, lead, graphite, coal, titanium, thorium, silicon, plastanium, + phaseFabric, surgeAlloy, sporePod, sand, blastCompound, pyratite, metaglass; @Override public void load(){ diff --git a/core/src/mindustry/content/Liquids.java b/core/src/mindustry/content/Liquids.java index 7ff39522f1..664a9362ba 100644 --- a/core/src/mindustry/content/Liquids.java +++ b/core/src/mindustry/content/Liquids.java @@ -24,7 +24,7 @@ public class Liquids implements ContentList{ }}; oil = new Liquid("oil", Color.valueOf("313131")){{ - viscosity = 0.7f; + viscosity = 0.75f; flammability = 1.2f; explosiveness = 1.2f; heatCapacity = 0.7f; diff --git a/core/src/mindustry/content/Planets.java b/core/src/mindustry/content/Planets.java index 9450c441da..7134a6ef28 100644 --- a/core/src/mindustry/content/Planets.java +++ b/core/src/mindustry/content/Planets.java @@ -9,7 +9,6 @@ import mindustry.type.*; public class Planets implements ContentList{ public static Planet sun, - //tantros, serpulo; @Override @@ -18,8 +17,6 @@ public class Planets implements ContentList{ bloom = true; accessible = false; - //lightColor = Color.valueOf("f4ee8e"); - meshLoader = () -> new SunMesh( this, 4, 5, 0.3, 1.7, 1.2, 1, @@ -33,15 +30,6 @@ public class Planets implements ContentList{ ); }}; - /*tantros = new Planet("tantros", sun, 2, 0.8f){{ - generator = new TantrosPlanetGenerator(); - meshLoader = () -> new HexMesh(this, 4); - atmosphereColor = Color.valueOf("3db899"); - startSector = 10; - atmosphereRadIn = -0.01f; - atmosphereRadOut = 0.3f; - }};*/ - serpulo = new Planet("serpulo", sun, 3, 1){{ generator = new SerpuloPlanetGenerator(); meshLoader = () -> new HexMesh(this, 6); @@ -49,6 +37,7 @@ public class Planets implements ContentList{ atmosphereRadIn = 0.02f; atmosphereRadOut = 0.3f; startSector = 15; + alwaysUnlocked = true; }}; } } diff --git a/core/src/mindustry/content/SectorPresets.java b/core/src/mindustry/content/SectorPresets.java index e84547dd5f..e66908e247 100644 --- a/core/src/mindustry/content/SectorPresets.java +++ b/core/src/mindustry/content/SectorPresets.java @@ -14,12 +14,14 @@ public class SectorPresets implements ContentList{ @Override public void load(){ + //region serpulo groundZero = new SectorPreset("groundZero", serpulo, 15){{ alwaysUnlocked = true; addStartingItems = true; captureWave = 10; difficulty = 1; + startWaveTimeMultiplier = 3f; }}; saltFlats = new SectorPreset("saltFlats", serpulo, 101){{ @@ -95,5 +97,7 @@ public class SectorPresets implements ContentList{ planetaryTerminal = new SectorPreset("planetaryTerminal", serpulo, 93){{ difficulty = 10; }}; + + //endregion } } diff --git a/core/src/mindustry/content/StatusEffects.java b/core/src/mindustry/content/StatusEffects.java index 1fef859f8c..2ea8af6c9d 100644 --- a/core/src/mindustry/content/StatusEffects.java +++ b/core/src/mindustry/content/StatusEffects.java @@ -12,7 +12,7 @@ import mindustry.graphics.*; import static mindustry.Vars.*; public class StatusEffects implements ContentList{ - public static StatusEffect none, burning, freezing, unmoving, slow, wet, muddy, melting, sapped, tarred, overdrive, overclock, shielded, shocked, blasted, corroded, boss, sporeSlowed, disarmed; + public static StatusEffect none, burning, freezing, unmoving, slow, wet, muddy, melting, sapped, tarred, overdrive, overclock, shielded, shocked, blasted, corroded, boss, sporeSlowed, disarmed, electrified; @Override public void load(){ @@ -27,10 +27,10 @@ public class StatusEffects implements ContentList{ init(() -> { opposite(wet, freezing); - affinity(tarred, ((unit, time, newTime, result) -> { + affinity(tarred, ((unit, result, time) -> { unit.damagePierce(transitionDamage); Fx.burning.at(unit.x + Mathf.range(unit.bounds() / 2f), unit.y + Mathf.range(unit.bounds() / 2f)); - result.set(burning, Math.min(time + newTime, 300f)); + result.set(burning, Math.min(time + result.time, 300f)); })); }); }}; @@ -45,7 +45,7 @@ public class StatusEffects implements ContentList{ init(() -> { opposite(melting, burning); - affinity(blasted, ((unit, time, newTime, result) -> { + affinity(blasted, ((unit, result, time) -> { unit.damagePierce(transitionDamage); result.set(freezing, time); })); @@ -70,14 +70,14 @@ public class StatusEffects implements ContentList{ transitionDamage = 14; init(() -> { - affinity(shocked, ((unit, time, newTime, result) -> { + affinity(shocked, ((unit, result, time) -> { unit.damagePierce(transitionDamage); if(unit.team == state.rules.waveTeam){ Events.fire(Trigger.shock); } result.set(wet, time); })); - opposite(burning); + opposite(burning, melting); }); }}; @@ -86,6 +86,7 @@ public class StatusEffects implements ContentList{ speedMultiplier = 0.94f; effect = Fx.muddy; effectChance = 0.09f; + show = false; }}; melting = new StatusEffect("melting"){{ @@ -97,10 +98,10 @@ public class StatusEffects implements ContentList{ init(() -> { opposite(wet, freezing); - affinity(tarred, ((unit, time, newTime, result) -> { + affinity(tarred, ((unit, result, time) -> { unit.damagePierce(8f); Fx.burning.at(unit.x + Mathf.range(unit.bounds() / 2f), unit.y + Mathf.range(unit.bounds() / 2f)); - result.set(melting, Math.min(time + newTime, 200f)); + result.set(melting, Math.min(time + result.time, 200f)); })); }); }}; @@ -113,6 +114,14 @@ public class StatusEffects implements ContentList{ effectChance = 0.1f; }}; + electrified = new StatusEffect("electrified"){{ + color = Pal.heal; + speedMultiplier = 0.7f; + reloadMultiplier = 0.6f; + effect = Fx.electrified; + effectChance = 0.1f; + }}; + sporeSlowed = new StatusEffect("spore-slowed"){{ color = Pal.spore; speedMultiplier = 0.8f; @@ -126,8 +135,8 @@ public class StatusEffects implements ContentList{ effect = Fx.oily; init(() -> { - affinity(melting, ((unit, time, newTime, result) -> result.set(melting, newTime + time))); - affinity(burning, ((unit, time, newTime, result) -> result.set(burning, newTime + time))); + affinity(melting, ((unit, result, time) -> result.set(melting, result.time + time))); + affinity(burning, ((unit, result, time) -> result.set(burning, result.time + time))); }); }}; diff --git a/core/src/mindustry/content/TechTree.java b/core/src/mindustry/content/TechTree.java index 2e0a0f3646..d642808c22 100644 --- a/core/src/mindustry/content/TechTree.java +++ b/core/src/mindustry/content/TechTree.java @@ -445,7 +445,6 @@ public class TechTree implements ContentList{ node(ruinousShores, Seq.with( new SectorComplete(craters), new Research(graphitePress), - new Research(combustionGenerator), new Research(kiln), new Research(mechanicalPump) ), () -> { @@ -549,8 +548,7 @@ public class TechTree implements ContentList{ node(fungalPass, Seq.with( new SectorComplete(stainedMountains), new Research(groundFactory), - new Research(door), - new Research(siliconSmelter) + new Research(door) ), () -> { node(nuclearComplex, Seq.with( new SectorComplete(fungalPass), diff --git a/core/src/mindustry/content/UnitTypes.java b/core/src/mindustry/content/UnitTypes.java index 3677050654..d54f7cf46f 100644 --- a/core/src/mindustry/content/UnitTypes.java +++ b/core/src/mindustry/content/UnitTypes.java @@ -1,62 +1,65 @@ package mindustry.content; import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; import arc.struct.*; import mindustry.ai.types.*; import mindustry.annotations.Annotations.*; import mindustry.ctype.*; +import mindustry.entities.*; import mindustry.entities.abilities.*; import mindustry.entities.bullet.*; +import mindustry.entities.effect.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.type.*; +import mindustry.type.weapons.*; import mindustry.world.meta.*; +import static arc.graphics.g2d.Draw.*; +import static arc.graphics.g2d.Lines.*; +import static arc.math.Angles.*; import static mindustry.Vars.*; public class UnitTypes implements ContentList{ //region definitions - //(the wall of shame - should fix the legacy stuff eventually...) - //mech - public static @EntityDef({Unitc.class, Mechc.class}) UnitType mace, dagger, crawler, fortress, scepter, reign; + public static @EntityDef({Unitc.class, Mechc.class}) UnitType mace, dagger, crawler, fortress, scepter, reign, vela; - //mech + //mech, legacy public static @EntityDef(value = {Unitc.class, Mechc.class}, legacy = true) UnitType nova, pulsar, quasar; - //mech - public static @EntityDef({Unitc.class, Mechc.class}) UnitType vela; - //legs public static @EntityDef({Unitc.class, Legsc.class}) UnitType corvus, atrax; - //legs + //legs, legacy public static @EntityDef(value = {Unitc.class, Legsc.class}, legacy = true) UnitType spiroct, arkyid, toxopid; //air public static @EntityDef({Unitc.class}) UnitType flare, eclipse, horizon, zenith, antumbra; - //air + //air, legacy public static @EntityDef(value = {Unitc.class}, legacy = true) UnitType mono; - //air + //air, legacy public static @EntityDef(value = {Unitc.class}, legacy = true) UnitType poly; //air + payload public static @EntityDef({Unitc.class, Payloadc.class}) UnitType mega; - //air + payload + //air + payload, legacy public static @EntityDef(value = {Unitc.class, Payloadc.class}, legacy = true) UnitType quad; //air + payload + ammo distribution public static @EntityDef({Unitc.class, Payloadc.class, AmmoDistributec.class}) UnitType oct; - //air + //air, legacy public static @EntityDef(value = {Unitc.class}, legacy = true) UnitType alpha, beta, gamma; - //water - public static @EntityDef({Unitc.class, WaterMovec.class}) UnitType risso, minke, bryde, sei, omura; + //naval + public static @EntityDef({Unitc.class, WaterMovec.class}) UnitType risso, minke, bryde, sei, omura, retusa, oxynoe, cyerce, aegires, navanax; //special block unit type public static @EntityDef({Unitc.class, BlockUnitc.class}) UnitType block; @@ -70,9 +73,9 @@ public class UnitTypes implements ContentList{ dagger = new UnitType("dagger"){{ speed = 0.5f; hitSize = 8f; - health = 140; + health = 150; weapons.add(new Weapon("large-weapon"){{ - reload = 14f; + reload = 13f; x = 4f; y = 2f; top = false; @@ -82,9 +85,9 @@ public class UnitTypes implements ContentList{ }}; mace = new UnitType("mace"){{ - speed = 0.4f; - hitSize = 9f; - health = 500; + speed = 0.45f; + hitSize = 10f; + health = 530; armor = 4f; immunities.add(StatusEffects.burning); @@ -93,10 +96,10 @@ public class UnitTypes implements ContentList{ top = false; shootSound = Sounds.flame; shootY = 2f; - reload = 14f; + reload = 12f; recoil = 1f; ejectEffect = Fx.none; - bullet = new BulletType(3.9f, 30f){{ + bullet = new BulletType(4.1f, 32f){{ ammoMultiplier = 3f; hitSize = 7f; lifetime = 12f; @@ -113,11 +116,11 @@ public class UnitTypes implements ContentList{ }}; fortress = new UnitType("fortress"){{ - speed = 0.39f; + speed = 0.43f; hitSize = 13f; rotateSpeed = 3f; targetAir = false; - health = 800; + health = 820; armor = 9f; mechFrontSway = 0.55f; @@ -133,12 +136,12 @@ public class UnitTypes implements ContentList{ bullet = new ArtilleryBulletType(2f, 8, "shell"){{ hitEffect = Fx.blastExplosion; knockback = 0.8f; - lifetime = 110f; + lifetime = 120f; width = height = 14f; collides = true; collidesTiles = true; - splashDamageRadius = 28f; - splashDamage = 54f; + splashDamageRadius = 30f; + splashDamage = 60f; backColor = Pal.bulletYellowBack; frontColor = Pal.bulletYellow; }}; @@ -147,7 +150,7 @@ public class UnitTypes implements ContentList{ scepter = new UnitType("scepter"){{ speed = 0.35f; - hitSize = 20f; + hitSize = 22f; rotateSpeed = 2.1f; health = 9000; armor = 10f; @@ -243,7 +246,7 @@ public class UnitTypes implements ContentList{ splashDamage = 16f; splashDamageRadius = 13f; - fragBullets = 2; + fragBullets = 3; fragLifeMin = 0f; fragCone = 30f; @@ -291,8 +294,8 @@ public class UnitTypes implements ContentList{ recoil = 2f; shootSound = Sounds.lasershoot; - bullet = new LaserBoltBulletType(5.2f, 14){{ - lifetime = 32f; + bullet = new LaserBoltBulletType(5.2f, 13){{ + lifetime = 30f; healPercent = 5f; collidesTeam = true; backColor = Pal.heal; @@ -305,7 +308,7 @@ public class UnitTypes implements ContentList{ canBoost = true; boostMultiplier = 1.6f; speed = 0.7f; - hitSize = 10f; + hitSize = 11f; health = 320f; buildSpeed = 0.9f; armor = 4f; @@ -325,7 +328,7 @@ public class UnitTypes implements ContentList{ y = 0.5f; shootY = 2.5f; - reload = 38f; + reload = 36f; shots = 3; inaccuracy = 35; shotDelay = 0.5f; @@ -360,7 +363,7 @@ public class UnitTypes implements ContentList{ quasar = new UnitType("quasar"){{ mineTier = 3; boostMultiplier = 2f; - health = 650f; + health = 640f; buildSpeed = 1.7f; canBoost = true; armor = 9f; @@ -372,7 +375,7 @@ public class UnitTypes implements ContentList{ ammoType = AmmoTypes.power; speed = 0.4f; - hitSize = 10f; + hitSize = 13f; mineSpeed = 6f; drawShields = false; @@ -384,7 +387,7 @@ public class UnitTypes implements ContentList{ shake = 2f; shootY = 4f; x = 6.5f; - reload = 50f; + reload = 55f; recoil = 4f; shootSound = Sounds.laser; @@ -396,13 +399,14 @@ public class UnitTypes implements ContentList{ sideLength = 70f; healPercent = 10f; collidesTeam = true; + length = 135f; colors = new Color[]{Pal.heal.cpy().a(0.4f), Pal.heal, Color.white}; }}; }}); }}; vela = new UnitType("vela"){{ - hitSize = 23f; + hitSize = 24f; rotateSpeed = 1.6f; canDrown = false; @@ -494,8 +498,6 @@ public class UnitTypes implements ContentList{ speed = 0.3f; - mineTier = 2; - mineSpeed = 7f; drawShields = false; weapons.add(new Weapon("corvus-weapon"){{ @@ -553,7 +555,7 @@ public class UnitTypes implements ContentList{ speed = 1f; hitSize = 8f; - health = 180; + health = 200; mechSideSway = 0.25f; range = 40f; @@ -568,9 +570,9 @@ public class UnitTypes implements ContentList{ hitEffect = Fx.pulverize; lifetime = 10f; speed = 1f; - splashDamageRadius = 58f; + splashDamageRadius = 60f; instantDisappear = true; - splashDamage = 85f; + splashDamage = 88f; killShooter = true; hittable = false; collidesAir = true; @@ -581,7 +583,7 @@ public class UnitTypes implements ContentList{ atrax = new UnitType("atrax"){{ speed = 0.5f; drag = 0.4f; - hitSize = 10f; + hitSize = 13f; rotateSpeed = 3f; targetAir = false; health = 600; @@ -601,7 +603,7 @@ public class UnitTypes implements ContentList{ weapons.add(new Weapon("eruption"){{ top = false; shootY = 3f; - reload = 10f; + reload = 9f; ejectEffect = Fx.none; recoil = 1f; x = 7f; @@ -619,11 +621,11 @@ public class UnitTypes implements ContentList{ }}; spiroct = new UnitType("spiroct"){{ - speed = 0.4f; + speed = 0.45f; drag = 0.4f; - hitSize = 12f; + hitSize = 15f; rotateSpeed = 3f; - health = 900; + health = 910; immunities = ObjectSet.with(StatusEffects.burning, StatusEffects.melting); legCount = 6; legLength = 13f; @@ -642,7 +644,7 @@ public class UnitTypes implements ContentList{ weapons.add(new Weapon("spiroct-weapon"){{ shootY = 4f; - reload = 15f; + reload = 14f; ejectEffect = Fx.none; recoil = 2f; rotate = true; @@ -652,7 +654,7 @@ public class UnitTypes implements ContentList{ y = -1.5f; bullet = new SapBulletType(){{ - sapStrength = 0.4f; + sapStrength = 0.5f; length = 75f; damage = 20; shootEffect = Fx.shootSmall; @@ -665,7 +667,7 @@ public class UnitTypes implements ContentList{ }}); weapons.add(new Weapon("mount-purple-weapon"){{ - reload = 20f; + reload = 18f; rotate = true; x = 4f; y = 3f; @@ -687,8 +689,8 @@ public class UnitTypes implements ContentList{ arkyid = new UnitType("arkyid"){{ drag = 0.1f; - speed = 0.5f; - hitSize = 21f; + speed = 0.6f; + hitSize = 23f; health = 8000; armor = 6f; @@ -737,7 +739,7 @@ public class UnitTypes implements ContentList{ shootSound = Sounds.sap; }}, new Weapon("spiroct-weapon"){{ - reload = 15f; + reload = 14f; x = 9f; y = 6f; rotate = true; @@ -745,7 +747,7 @@ public class UnitTypes implements ContentList{ shootSound = Sounds.sap; }}, new Weapon("spiroct-weapon"){{ - reload = 23f; + reload = 22f; x = 14f; y = 0f; rotate = true; @@ -790,9 +792,10 @@ public class UnitTypes implements ContentList{ toxopid = new UnitType("toxopid"){{ drag = 0.1f; speed = 0.5f; - hitSize = 21f; + hitSize = 26f; health = 22000; armor = 13f; + lightRadius = 140f; rotateSpeed = 1.9f; @@ -878,6 +881,9 @@ public class UnitTypes implements ContentList{ lightningLength = 20; smokeEffect = Fx.shootBigSmoke2; hitShake = 10f; + lightRadius = 40f; + lightColor = Pal.sap; + lightOpacity = 0.6f; status = StatusEffects.sapped; statusDuration = 60f * 10; @@ -899,6 +905,9 @@ public class UnitTypes implements ContentList{ lightningLength = 5; smokeEffect = Fx.shootBigSmoke2; hitShake = 5f; + lightRadius = 30f; + lightColor = Pal.sap; + lightOpacity = 0.5f; status = StatusEffects.sapped; statusDuration = 60f * 10; @@ -921,6 +930,7 @@ public class UnitTypes implements ContentList{ targetAir = false; commandLimit = 4; circleTarget = true; + hitSize = 7; weapons.add(new Weapon(){{ y = 0f; @@ -941,7 +951,7 @@ public class UnitTypes implements ContentList{ horizon = new UnitType("horizon"){{ health = 340; - speed = 1.7f; + speed = 1.65f; accel = 0.08f; drag = 0.016f; flying = true; @@ -980,7 +990,7 @@ public class UnitTypes implements ContentList{ zenith = new UnitType("zenith"){{ health = 700; - speed = 1.8f; + speed = 1.7f; accel = 0.04f; drag = 0.016f; flying = true; @@ -1010,8 +1020,8 @@ public class UnitTypes implements ContentList{ homingRange = 60f; keepVelocity = false; splashDamageRadius = 25f; - splashDamage = 16f; - lifetime = 60f; + splashDamage = 15f; + lifetime = 50f; trailColor = Pal.unitBack; backColor = Pal.unitBack; frontColor = Pal.unitFront; @@ -1034,16 +1044,16 @@ public class UnitTypes implements ContentList{ armor = 9f; engineOffset = 21; engineSize = 5.3f; - hitSize = 56f; + hitSize = 46f; targetFlag = BlockFlag.battery; - BulletType missiles = new MissileBulletType(2.7f, 10){{ + BulletType missiles = new MissileBulletType(2.7f, 14){{ width = 8f; height = 8f; shrinkY = 0f; drag = -0.01f; splashDamageRadius = 20f; - splashDamage = 30f; + splashDamage = 34f; ammoMultiplier = 4f; lifetime = 50f; hitEffect = Fx.blastExplosion; @@ -1087,7 +1097,7 @@ public class UnitTypes implements ContentList{ shootSound = Sounds.shootBig; rotate = true; shadow = 8f; - bullet = new BasicBulletType(7f, 50){{ + bullet = new BasicBulletType(7f, 55){{ width = 12f; height = 18f; lifetime = 25f; @@ -1104,7 +1114,7 @@ public class UnitTypes implements ContentList{ rotateSpeed = 1f; flying = true; lowAltitude = true; - health = 20000; + health = 21000; engineOffset = 38; engineSize = 7.3f; hitSize = 58f; @@ -1115,7 +1125,7 @@ public class UnitTypes implements ContentList{ BulletType fragBullet = new FlakBulletType(4f, 5){{ shootEffect = Fx.shootBig; ammoMultiplier = 4f; - splashDamage = 42f; + splashDamage = 50f; splashDamageRadius = 25f; collidesGround = true; lifetime = 38f; @@ -1138,7 +1148,7 @@ public class UnitTypes implements ContentList{ rotate = true; bullet = new LaserBulletType(){{ - damage = 90f; + damage = 100f; sideAngle = 20f; sideWidth = 1.5f; sideLength = 80f; @@ -1208,7 +1218,7 @@ public class UnitTypes implements ContentList{ health = 400; buildSpeed = 0.5f; engineOffset = 6.5f; - hitSize = 8f; + hitSize = 9f; lowAltitude = true; ammoType = AmmoTypes.power; @@ -1216,7 +1226,7 @@ public class UnitTypes implements ContentList{ mineTier = 2; mineSpeed = 3.5f; - abilities.add(new RepairFieldAbility(5f, 60f * 5, 50f)); + abilities.add(new RepairFieldAbility(5f, 60f * 8, 50f)); weapons.add(new Weapon("heal-weapon-mount"){{ top = false; @@ -1235,7 +1245,7 @@ public class UnitTypes implements ContentList{ homingPower = 0.08f; weaveMag = 4; weaveScale = 4; - lifetime = 56f; + lifetime = 50f; keepVelocity = false; shootEffect = Fx.shootHeal; smokeEffect = Fx.hitLaser; @@ -1265,7 +1275,7 @@ public class UnitTypes implements ContentList{ flying = true; engineOffset = 10.5f; rotateShooting = false; - hitSize = 15f; + hitSize = 16.05f; engineSize = 3f; payloadCapacity = (2 * 2) * tilePayload; buildSpeed = 2.6f; @@ -1276,7 +1286,7 @@ public class UnitTypes implements ContentList{ weapons.add( new Weapon("heal-weapon-mount"){{ shootSound = Sounds.lasershoot; - reload = 25f; + reload = 24f; x = 8f; y = -6f; rotate = true; @@ -1317,7 +1327,7 @@ public class UnitTypes implements ContentList{ engineOffset = 12f; engineSize = 6f; rotateShooting = false; - hitSize = 32f; + hitSize = 36f; payloadCapacity = (3 * 3) * tilePayload; buildSpeed = 2.5f; buildBeamOffset = 23; @@ -1352,7 +1362,7 @@ public class UnitTypes implements ContentList{ shootCone = 180f; ejectEffect = Fx.none; - despawnShake = 4f; + hitShake = 4f; collidesAir = false; @@ -1388,7 +1398,7 @@ public class UnitTypes implements ContentList{ engineOffset = 46f; engineSize = 7.8f; rotateShooting = false; - hitSize = 60f; + hitSize = 66f; payloadCapacity = (5.3f * 5.3f) * tilePayload; buildSpeed = 4f; drawShields = false; @@ -1408,7 +1418,7 @@ public class UnitTypes implements ContentList{ risso = new UnitType("risso"){{ speed = 1.1f; drag = 0.13f; - hitSize = 9f; + hitSize = 10f; health = 280; accel = 0.4f; rotateSpeed = 3.3f; @@ -1429,7 +1439,7 @@ public class UnitTypes implements ContentList{ weapons.add(new Weapon("missiles-mount"){{ mirror = false; - reload = 20f; + reload = 23f; x = 0f; y = -5f; rotate = true; @@ -1444,7 +1454,7 @@ public class UnitTypes implements ContentList{ homingRange = 60f; splashDamageRadius = 25f; splashDamage = 10f; - lifetime = 80f; + lifetime = 65f; trailColor = Color.gray; backColor = Pal.bulletYellowBack; frontColor = Pal.bulletYellow; @@ -1460,7 +1470,7 @@ public class UnitTypes implements ContentList{ health = 600; speed = 0.9f; drag = 0.15f; - hitSize = 11f; + hitSize = 13f; armor = 4f; accel = 0.3f; rotateSpeed = 2.6f; @@ -1500,12 +1510,12 @@ public class UnitTypes implements ContentList{ }}; bryde = new UnitType("bryde"){{ - health = 900; + health = 910; speed = 0.85f; accel = 0.2f; rotateSpeed = 1.8f; drag = 0.17f; - hitSize = 16f; + hitSize = 20f; armor = 7f; rotateShooting = false; @@ -1537,13 +1547,13 @@ public class UnitTypes implements ContentList{ trailMult = 0.8f; hitEffect = Fx.massiveExplosion; knockback = 1.5f; - lifetime = 100f; + lifetime = 80f; height = 15.5f; width = 15f; collidesTiles = false; ammoMultiplier = 4f; splashDamageRadius = 40f; - splashDamage = 80f; + splashDamage = 70f; backColor = Pal.missileYellowBack; frontColor = Pal.missileYellow; trailEffect = Fx.artilleryTrail; @@ -1582,7 +1592,7 @@ public class UnitTypes implements ContentList{ keepVelocity = false; splashDamageRadius = 25f; splashDamage = 10f; - lifetime = 80f; + lifetime = 70f; trailColor = Color.gray; backColor = Pal.bulletYellowBack; frontColor = Pal.bulletYellow; @@ -1595,7 +1605,7 @@ public class UnitTypes implements ContentList{ }}; sei = new UnitType("sei"){{ - health = 10500; + health = 11000; armor = 12f; speed = 0.73f; @@ -1684,7 +1694,7 @@ public class UnitTypes implements ContentList{ health = 22000; speed = 0.62f; drag = 0.18f; - hitSize = 50f; + hitSize = 58f; armor = 16f; accel = 0.19f; rotateSpeed = 0.9f; @@ -1730,6 +1740,483 @@ public class UnitTypes implements ContentList{ }}); }}; + //endregion + //region naval support + retusa = new UnitType("retusa"){{ + defaultController = HugAI::new; + speed = 0.9f; + targetAir = false; + drag = 0.14f; + hitSize = 11f; + health = 270; + accel = 0.4f; + rotateSpeed = 5f; + trailLength = 20; + trailX = 5f; + trailScl = 1.3f; + rotateShooting = false; + range = 100f; + + armor = 3f; + + buildSpeed = 2f; + + weapons.add(new RepairBeamWeapon("repair-beam-weapon-center"){{ + x = 0f; + y = -5.5f; + shootY = 6f; + beamWidth = 0.8f; + mirror = false; + repairSpeed = 0.7f; + + bullet = new BulletType(){{ + maxRange = 120f; + }}; + }}); + + weapons.add(new Weapon(){{ + mirror = false; + reload = 80f; + shots = 3; + shotDelay = 7f; + x = y = shootX = shootY = 0f; + + bullet = new BasicBulletType(){{ + sprite = "mine-bullet"; + width = height = 11f; + layer = Layer.scorch; + shootEffect = smokeEffect = Fx.none; + + maxRange = 50f; + ignoreRotation = true; + healPercent = 4f; + + backColor = Pal.heal; + frontColor = Color.white; + mixColorTo = Color.white; + + hitSound = Sounds.plasmaboom; + + shootCone = 360f; + ejectEffect = Fx.none; + hitSize = 22f; + + collidesAir = false; + + lifetime = 500f; + + hitEffect = new MultiEffect(Fx.blastExplosion, Fx.greenCloud); + keepVelocity = false; + + shrinkX = shrinkY = 0f; + + speed = 0.001f; + + splashDamage = 50f; + splashDamageRadius = 40f; + }}; + }}); + }}; + + oxynoe = new UnitType("oxynoe"){{ + health = 560; + speed = 0.83f; + drag = 0.14f; + hitSize = 14f; + armor = 4f; + accel = 0.4f; + rotateSpeed = 4f; + rotateShooting = false; + + trailLength = 22; + trailX = 5.5f; + trailY = -4f; + trailScl = 1.9f; + + buildSpeed = 2.5f; + + weapons.add(new Weapon("plasma-mount-weapon"){{ + + reload = 5f; + x = 4.5f; + y = 6.5f; + rotate = true; + rotateSpeed = 5f; + inaccuracy = 10f; + ejectEffect = Fx.casing1; + shootSound = Sounds.flame; + shootCone = 30f; + + bullet = new BulletType(3.4f, 23f){{ + healPercent = 1.5f; + collidesTeam = true; + ammoMultiplier = 3f; + hitSize = 7f; + lifetime = 18f; + pierce = true; + collidesAir = false; + statusDuration = 60f * 4; + hitEffect = Fx.hitFlamePlasma; + ejectEffect = Fx.none; + despawnEffect = Fx.none; + status = StatusEffects.burning; + keepVelocity = false; + hittable = false; + shootEffect = new Effect(32f, 80f, e -> { + color(Color.white, Pal.heal, Color.gray, e.fin()); + + randLenVectors(e.id, 8, e.finpow() * 60f, e.rotation, 10f, (x, y) -> { + Fill.circle(e.x + x, e.y + y, 0.65f + e.fout() * 1.5f); + Drawf.light(e.x + x, e.y + y, 16f * e.fout(), Pal.heal, 0.6f); + }); + }); + }}; + }}); + + weapons.add(new PointDefenseWeapon("point-defense-mount"){{ + mirror = false; + x = 0f; + y = 1f; + reload = 9f; + targetInterval = 10f; + targetSwitchInterval = 15f; + + bullet = new BulletType(){{ + shootEffect = Fx.sparkShoot; + hitEffect = Fx.pointHit; + maxRange = 100f; + damage = 15f; + }}; + }}); + + }}; + + cyerce = new UnitType("cyerce"){{ + health = 870; + speed = 0.86f; + accel = 0.22f; + rotateSpeed = 2.6f; + drag = 0.16f; + hitSize = 20f; + armor = 6f; + rotateShooting = false; + + trailLength = 23; + trailX = 9f; + trailY = -9f; + trailScl = 2f; + + buildSpeed = 3f; + + weapons.add(new RepairBeamWeapon("repair-beam-weapon-center"){{ + x = 11f; + y = -10f; + shootY = 6f; + beamWidth = 0.8f; + repairSpeed = 0.7f; + + bullet = new BulletType(){{ + maxRange = 130f; + }}; + }}); + + weapons.add(new Weapon("plasma-missile-mount"){{ + reload = 60f; + x = 9f; + y = 3f; + + shadow = 5f; + + rotateSpeed = 4f; + rotate = true; + inaccuracy = 1f; + velocityRnd = 0.1f; + shootSound = Sounds.missile; + + ejectEffect = Fx.none; + bullet = new FlakBulletType(2.5f, 25){{ + sprite = "missile-large"; + collides = false; + //for targeting + collidesGround = collidesAir = true; + explodeRange = 40f; + width = height = 12f; + shrinkY = 0f; + drag = -0.003f; + homingRange = 60f; + keepVelocity = false; + lightRadius = 60f; + lightOpacity = 0.7f; + lightColor = Pal.heal; + + splashDamageRadius = 30f; + splashDamage = 28f; + + lifetime = 90f; + backColor = Pal.heal; + frontColor = Color.white; + + hitEffect = new ExplosionEffect(){{ + lifetime = 28f; + waveStroke = 6f; + waveLife = 10f; + waveRadBase = 7f; + waveColor = Pal.heal; + waveRad = 30f; + smokes = 6; + smokeColor = Color.white; + sparkColor = Pal.heal; + sparks = 6; + sparkRad = 35f; + sparkStroke = 1.5f; + sparkLen = 4f; + }}; + + weaveScale = 8f; + weaveMag = 1f; + + trailColor = Pal.heal; + trailParam = 5f; + trailInterval = 3f; + + fragBullets = 7; + fragVelocityMin = 0.3f; + + fragBullet = new MissileBulletType(3.9f, 12){{ + homingPower = 0.2f; + weaveMag = 4; + weaveScale = 4; + lifetime = 70f; + shootEffect = Fx.shootHeal; + smokeEffect = Fx.hitLaser; + splashDamage = 13f; + splashDamageRadius = 20f; + frontColor = Color.white; + hitSound = Sounds.none; + + lightColor = Pal.heal; + lightRadius = 40f; + lightOpacity = 0.7f; + + trailInterval = 2f; + trailParam = 3f; + healPercent = 2.8f; + collidesTeam = true; + backColor = Pal.heal; + trailColor = Pal.heal; + + despawnEffect = Fx.none; + hitEffect = new ExplosionEffect(){{ + lifetime = 20f; + waveStroke = 2f; + waveColor = Pal.heal; + waveRad = 12f; + smokeSize = 0f; + smokeSizeBase = 0f; + sparkColor = Pal.heal; + sparks = 9; + sparkRad = 35f; + sparkLen = 4f; + sparkStroke = 1.5f; + }}; + }}; + }}; + }}); + }}; + + aegires = new UnitType("aegires"){{ + health = 12000; + armor = 12f; + + speed = 0.7f; + drag = 0.17f; + hitSize = 44f; + accel = 0.2f; + rotateSpeed = 1.4f; + rotateShooting = false; + + //clip size is massive due to energy field + clipSize = 250f; + + trailLength = 50; + trailX = 18f; + trailY = -17f; + trailScl = 3.2f; + + buildSpeed = 3.5f; + + abilities.add(new EnergyFieldAbility(35f, 65f, 180f){{ + repair = 35f; + statusDuration = 60f * 6f; + maxTargets = 25; + }}); + + for(float mountY : new float[]{-18f, 14}){ + weapons.add(new PointDefenseWeapon("point-defense-mount"){{ + x = 12.5f; + y = mountY; + reload = 6f; + targetInterval = 8f; + targetSwitchInterval = 8f; + + bullet = new BulletType(){{ + shootEffect = Fx.sparkShoot; + hitEffect = Fx.pointHit; + maxRange = 180f; + damage = 24f; + }}; + }}); + } + }}; + + navanax = new UnitType("navanax"){{ + health = 20000; + speed = 0.65f; + drag = 0.17f; + hitSize = 58f; + armor = 16f; + accel = 0.2f; + rotateSpeed = 1.1f; + rotateShooting = false; + + trailLength = 70; + trailX = 23f; + trailY = -32f; + trailScl = 3.5f; + + buildSpeed = 3.8f; + + for(float mountY : new float[]{-117/4f, 50/4f}){ + for(float sign : Mathf.signs){ + weapons.add(new Weapon("plasma-laser-mount"){{ + shadow = 20f; + controllable = false; + autoTarget = true; + mirror = false; + shake = 3f; + shootY = 7f; + rotate = true; + x = 84f/4f * sign; + y = mountY; + + targetInterval = 20f; + targetSwitchInterval = 35f; + + rotateSpeed = 3.5f; + reload = 170f; + recoil = 1f; + shootSound = Sounds.beam; + continuous = true; + cooldownTime = reload; + immunities.add(StatusEffects.burning); + + bullet = new ContinuousLaserBulletType(){{ + maxRange = 90f; + damage = 25f; + length = 90f; + hitEffect = Fx.hitMeltHeal; + drawSize = 200f; + lifetime = 155f; + shake = 1f; + + shootEffect = Fx.shootHeal; + smokeEffect = Fx.none; + width = 4f; + largeHit = false; + + incendChance = 0.03f; + incendSpread = 5f; + incendAmount = 1; + + healPercent = 0.4f; + collidesTeam = true; + + colors = new Color[]{Pal.heal.cpy().a(.2f), Pal.heal.cpy().a(.5f), Pal.heal.cpy().mul(1.2f), Color.white}; + }}; + }}); + } + } + + weapons.add(new Weapon("emp-cannon-mount"){{ + rotate = true; + + x = 70f/4f; + y = -26f/4f; + + reload = 70f; + shake = 3f; + rotateSpeed = 2f; + shadow = 30f; + shootY = 7f; + recoil = 4f; + cooldownTime = reload - 10f; + + bullet = new EmpBulletType(){{ + float rad = 100f; + + lightOpacity = 0.7f; + unitDamageScl = 0.8f; + healPercent = 20f; + timeIncrease = 3f; + timeDuration = 60f * 20f; + powerDamageScl = 3f; + damage = 40; + hitColor = lightColor = Pal.heal; + lightRadius = 70f; + clipSize = 250f; + shootEffect = Fx.hitEmpSpark; + smokeEffect = Fx.shootBigSmoke2; + lifetime = 60f; + sprite = "circle-bullet"; + backColor = Pal.heal; + frontColor = Color.white; + width = height = 12f; + speed = 5f; + trailLength = 20; + trailWidth = 6f; + trailColor = Pal.heal; + trailInterval = 3f; + splashDamage = 40f; + splashDamageRadius = rad; + hitShake = 4f; + trailRotation = true; + status = StatusEffects.electrified; + + trailEffect = new Effect(16f, e -> { + color(Pal.heal); + for(int s : Mathf.signs){ + Drawf.tri(e.x, e.y, 4f, 30f * e.fslope(), e.rotation + 90f*s); + } + }); + + hitEffect = new Effect(50f, 100f, e -> { + e.scaled(7f, b -> { + color(Pal.heal, b.fout()); + Fill.circle(e.x, e.y, rad); + }); + + color(Pal.heal); + stroke(e.fout() * 3f); + Lines.circle(e.x, e.y, rad); + + int points = 10; + float offset = Mathf.randomSeed(e.id, 360f); + for(int i = 0; i < points; i++){ + float angle = i* 360f / points + offset; + //for(int s : Mathf.zeroOne){ + Drawf.tri(e.x + Angles.trnsx(angle, rad), e.y + Angles.trnsy(angle, rad), 6f, 50f * e.fout(), angle/* + s*180f*/); + //} + } + + Fill.circle(e.x, e.y, 12f * e.fout()); + color(); + Fill.circle(e.x, e.y, 6f * e.fout()); + Drawf.light(e.x, e.y, rad * 1.6f, Pal.heal, e.fout()); + }); + }}; + }}); + }}; + //endregion //region core diff --git a/core/src/mindustry/content/Weathers.java b/core/src/mindustry/content/Weathers.java index 8e3ea38a92..ee89e4f82c 100644 --- a/core/src/mindustry/content/Weathers.java +++ b/core/src/mindustry/content/Weathers.java @@ -14,7 +14,8 @@ public class Weathers implements ContentList{ snow, sandstorm, sporestorm, - fog; + fog, + suspendParticles; @Override public void load(){ @@ -102,5 +103,19 @@ public class Weathers implements ContentList{ attrs.set(Attribute.water, 0.05f); opacityMultiplier = 0.47f; }}; + + suspendParticles = new ParticleWeather("suspend-particles"){{ + color = noiseColor = Color.valueOf("a7c1fa"); + particleRegion = "particle"; + statusGround = false; + useWindVector = true; + sizeMax = 4f; + sizeMin = 1.4f; + minAlpha = 0.5f; + maxAlpha = 1f; + density = 10000f; + baseSpeed = 0.03f; + }}; } + } diff --git a/core/src/mindustry/core/ContentLoader.java b/core/src/mindustry/core/ContentLoader.java index 582bbd8bf8..e00fec3061 100644 --- a/core/src/mindustry/core/ContentLoader.java +++ b/core/src/mindustry/core/ContentLoader.java @@ -104,6 +104,7 @@ public class ContentLoader{ /** Calls Content#load() on everything. Use only after all modules have been created on the client.*/ public void load(){ + initialize(Content::loadIcon); initialize(Content::load); } @@ -132,9 +133,9 @@ public class ContentLoader{ /** Loads block colors. */ public void loadColors(){ Pixmap pixmap = new Pixmap(files.internal("sprites/block_colors.png")); - for(int i = 0; i < pixmap.getWidth(); i++){ + for(int i = 0; i < pixmap.width; i++){ if(blocks().size > i){ - int color = pixmap.getPixel(i, 0); + int color = pixmap.get(i, 0); if(color == 0 || color == 255) continue; @@ -290,6 +291,10 @@ public class ContentLoader{ return getBy(ContentType.unit); } + public UnitType unit(int id){ + return getByID(ContentType.unit, id); + } + public Seq planets(){ return getBy(ContentType.planet); } diff --git a/core/src/mindustry/core/Control.java b/core/src/mindustry/core/Control.java index 1799d7674c..bb4c1082b1 100644 --- a/core/src/mindustry/core/Control.java +++ b/core/src/mindustry/core/Control.java @@ -28,7 +28,6 @@ import mindustry.maps.Map; import mindustry.maps.*; import mindustry.net.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.ui.dialogs.*; import mindustry.world.*; @@ -123,6 +122,10 @@ public class Control implements ApplicationListener, Loadable{ //add player when world loads regardless Events.on(WorldLoadEvent.class, e -> { player.add(); + //make player admin on any load when hosting + if(net.active() && net.server()){ + player.admin = true; + } }); //autohost for pvp maps @@ -130,7 +133,7 @@ public class Control implements ApplicationListener, Loadable{ if(state.rules.pvp && !net.active()){ try{ net.host(port); - player.admin(true); + player.admin = true; }catch(IOException e){ ui.showException("@server.error", e); state.set(State.menu); @@ -148,7 +151,7 @@ public class Control implements ApplicationListener, Loadable{ if(e.content instanceof SectorPreset){ for(TechNode node : TechTree.all){ if(!node.content.unlocked() && node.objectives.contains(o -> o instanceof SectorComplete sec && sec.preset == e.content) && !node.objectives.contains(o -> !o.complete())){ - ui.hudfrag.showToast(new TextureRegionDrawable(node.content.icon(Cicon.large)), bundle.get("available")); + ui.hudfrag.showToast(new TextureRegionDrawable(node.content.uiIcon), iconLarge, bundle.get("available")); } } } @@ -192,33 +195,36 @@ public class Control implements ApplicationListener, Loadable{ }); Events.run(Trigger.newGame, () -> { - Building core = player.closestCore(); + Building core = player.bestCore(); if(core == null) return; - //TODO this sounds pretty bad due to conflict - if(settings.getInt("musicvol") > 0){ - Musics.land.stop(); - Musics.land.play(); - Musics.land.setVolume(settings.getInt("musicvol") / 100f); - } - - app.post(() -> ui.hudfrag.showLand()); - renderer.zoomIn(Fx.coreLand.lifetime); - app.post(() -> Fx.coreLand.at(core.getX(), core.getY(), 0, core.block)); camera.position.set(core); player.set(core); - Time.run(Fx.coreLand.lifetime, () -> { - Fx.launch.at(core); - Effect.shake(5f, 5f, core); - - if(state.isCampaign()){ - ui.announce("[accent]" + state.rules.sector.name() + "\n" + - (state.rules.sector.info.resources.any() ? "[lightgray]" + bundle.get("sectors.resources") + "[white] " + - state.rules.sector.info.resources.toString(" ", u -> u.emoji()) : ""), 5); + if(showLandAnimation){ + //TODO this sounds pretty bad due to conflict + if(settings.getInt("musicvol") > 0){ + Musics.land.stop(); + Musics.land.play(); + Musics.land.setVolume(settings.getInt("musicvol") / 100f); } - }); + + app.post(() -> ui.hudfrag.showLand()); + renderer.zoomIn(Fx.coreLand.lifetime); + app.post(() -> Fx.coreLand.at(core.getX(), core.getY(), 0, core.block)); + + Time.run(Fx.coreLand.lifetime, () -> { + Fx.launch.at(core); + Effect.shake(5f, 5f, core); + + if(state.isCampaign()){ + ui.announce("[accent]" + state.rules.sector.name() + "\n" + + (state.rules.sector.info.resources.any() ? "[lightgray]" + bundle.get("sectors.resources") + "[white] " + + state.rules.sector.info.resources.toString(" ", u -> u.emoji()) : ""), 5); + } + }); + } }); } @@ -345,7 +351,7 @@ public class Control implements ApplicationListener, Loadable{ //reset wave so things are more fair state.wave = 1; //set up default wave time - state.wavetime = state.rules.waveSpacing * 2f; + state.wavetime = state.rules.waveSpacing * (sector.preset == null ? 2f : sector.preset.startWaveTimeMultiplier); //reset captured state sector.info.wasCaptured = false; //re-enable waves diff --git a/core/src/mindustry/core/GameState.java b/core/src/mindustry/core/GameState.java index de7cf6f60c..33050b996e 100644 --- a/core/src/mindustry/core/GameState.java +++ b/core/src/mindustry/core/GameState.java @@ -17,7 +17,9 @@ public class GameState{ /** Wave countdown in ticks. */ public float wavetime; /** Whether the game is in game over state. */ - public boolean gameOver = false, serverPaused = false, wasTimeout; + public boolean gameOver = false, serverPaused = false; + /** Server ticks/second. Only valid in multiplayer. */ + public int serverTps = -1; /** Map that is currently being played on. */ public Map map = emptyMap; /** The current game rules. */ @@ -33,8 +35,9 @@ public class GameState{ /** Current game state. */ private State state = State.menu; + @Nullable public Unit boss(){ - return teams.boss; + return teams.bosses.firstOpt(); } public void set(State astate){ diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 5f39ad8017..cd9c86810b 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -14,6 +14,7 @@ import mindustry.maps.*; import mindustry.type.*; import mindustry.type.Weather.*; import mindustry.world.*; +import mindustry.world.blocks.storage.CoreBlock.*; import java.util.*; @@ -34,8 +35,8 @@ public class Logic implements ApplicationListener{ Events.on(BlockDestroyEvent.class, event -> { //blocks that get broken are appended to the team's broken block queue Tile tile = event.tile; - //skip null entities or un-rebuildables, for obvious reasons; also skip client since they can't modify these requests - if(tile.build == null || !tile.block().rebuildable || net.client()) return; + //skip null entities or un-rebuildables, for obvious reasons + if(tile.build == null || !tile.block().rebuildable) return; tile.build.addPlan(true); }); @@ -174,10 +175,11 @@ public class Logic implements ApplicationListener{ if(!state.isCampaign()){ for(TeamData team : state.teams.getActive()){ if(team.hasCore()){ - Building entity = team.core(); + CoreBuild entity = team.core(); entity.items.clear(); for(ItemStack stack : state.rules.loadout){ - entity.items.add(stack.item, stack.amount); + //make sure to cap storage + entity.items.add(stack.item, Math.min(stack.amount, entity.storageCapacity - entity.items.get(stack.item))); } } } @@ -406,6 +408,7 @@ public class Logic implements ApplicationListener{ //apply weather attributes state.envAttrs.clear(); + state.envAttrs.add(state.rules.attributes); Groups.weather.each(w -> state.envAttrs.add(w.weather.attrs, w.opacity)); Groups.update(); diff --git a/core/src/mindustry/core/NetClient.java b/core/src/mindustry/core/NetClient.java index be8fb108a0..e1ac7f97f1 100644 --- a/core/src/mindustry/core/NetClient.java +++ b/core/src/mindustry/core/NetClient.java @@ -16,9 +16,9 @@ import mindustry.entities.*; import mindustry.entities.units.*; import mindustry.game.EventType.*; import mindustry.game.*; +import mindustry.game.Teams.*; import mindustry.gen.*; import mindustry.net.Administration.*; -import mindustry.net.Net.*; import mindustry.net.*; import mindustry.net.Packets.*; import mindustry.ui.*; @@ -32,9 +32,9 @@ import java.util.zip.*; import static mindustry.Vars.*; public class NetClient implements ApplicationListener{ - private static final float dataTimeout = 60 * 18; - private static final float playerSyncTime = 2; - public static final float viewScale = 2f; + private static final float dataTimeout = 60 * 20; + private static final float playerSyncTime = 5; + private static final Reads dataReads = new Reads(null); private long ping; private Interval timer = new Interval(5); @@ -62,7 +62,7 @@ public class NetClient implements ApplicationListener{ net.handleClient(Connect.class, packet -> { Log.info("Connecting to server: @", packet.addressTCP); - player.admin(false); + player.admin = false; reset(); @@ -92,7 +92,7 @@ public class NetClient implements ApplicationListener{ c.mods = mods.getModStrings(); c.mobile = mobile; c.versionType = Version.type; - c.color = player.color().rgba(); + c.color = player.color.rgba(); c.usid = getUsid(packet.addressTCP); c.uuid = platform.getUUID(); @@ -103,7 +103,7 @@ public class NetClient implements ApplicationListener{ return; } - net.send(c, SendMode.tcp); + net.send(c, true); }); net.handleClient(Disconnect.class, packet -> { @@ -112,19 +112,19 @@ public class NetClient implements ApplicationListener{ connecting = false; logic.reset(); platform.updateRPC(); - player.name(Core.settings.getString("name")); - player.color().set(Core.settings.getInt("color-0")); + player.name = Core.settings.getString("name"); + player.color.set(Core.settings.getInt("color-0")); if(quiet) return; Time.runTask(3f, ui.loadfrag::hide); if(packet.reason != null){ - switch(packet.reason){ - case "closed" -> ui.showSmall("@disconnect", "@disconnect.closed"); - case "timeout" -> ui.showSmall("@disconnect", "@disconnect.timeout"); - case "error" -> ui.showSmall("@disconnect", "@disconnect.error"); - } + ui.showSmall(switch(packet.reason){ + case "closed" -> "@disconnect.closed"; + case "timeout" -> "@disconnect.timeout"; + default -> "@disconnect.error"; + }, "@disconnect.closed"); }else{ ui.showErrorMessage("@disconnect"); } @@ -136,10 +136,6 @@ public class NetClient implements ApplicationListener{ finishConnecting(); }); - - net.handleClient(InvokePacket.class, packet -> { - RemoteReadClient.readPacket(packet.reader(), packet.type); - }); } public void addPacketHandler(String type, Cons handler){ @@ -428,9 +424,9 @@ public class NetClient implements ApplicationListener{ } @Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true) - public static void entitySnapshot(short amount, short dataLen, byte[] data){ + public static void entitySnapshot(short amount, byte[] data){ try{ - netClient.byteStream.setBytes(net.decompressSnapshot(data, dataLen)); + netClient.byteStream.setBytes(data); DataInputStream input = netClient.dataStream; for(int j = 0; j < amount; j++){ @@ -474,9 +470,9 @@ public class NetClient implements ApplicationListener{ } @Remote(variants = Variant.both, priority = PacketPriority.low, unreliable = true) - public static void blockSnapshot(short amount, short dataLen, byte[] data){ + public static void blockSnapshot(short amount, byte[] data){ try{ - netClient.byteStream.setBytes(net.decompressSnapshot(data, dataLen)); + netClient.byteStream.setBytes(data); DataInputStream input = netClient.dataStream; for(int i = 0; i < amount; i++){ @@ -499,7 +495,7 @@ public class NetClient implements ApplicationListener{ } @Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true) - public static void stateSnapshot(float waveTime, int wave, int enemies, boolean paused, boolean gameOver, int timeData, short coreDataLen, byte[] coreData){ + public static void stateSnapshot(float waveTime, int wave, int enemies, boolean paused, boolean gameOver, int timeData, byte tps, byte[] coreData){ try{ if(wave > state.wave){ state.wave = wave; @@ -511,21 +507,22 @@ public class NetClient implements ApplicationListener{ state.wave = wave; state.enemies = enemies; state.serverPaused = paused; + state.serverTps = tps & 0xff; universe.updateNetSeconds(timeData); - netClient.byteStream.setBytes(net.decompressSnapshot(coreData, coreDataLen)); + netClient.byteStream.setBytes(coreData); DataInputStream input = netClient.dataStream; + dataReads.input = input; - int cores = input.readInt(); - for(int i = 0; i < cores; i++){ - int pos = input.readInt(); - Tile tile = world.tile(pos); - - if(tile != null && tile.build != null){ - tile.build.items.read(Reads.get(input)); + int teams = input.readUnsignedByte(); + for(int i = 0; i < teams; i++){ + int team = input.readUnsignedByte(); + TeamData data = state.teams.get(Team.all[team]); + if(data.cores.any()){ + data.cores.first().items.read(dataReads); }else{ - new ItemModule().read(Reads.get(input)); + new ItemModule().read(dataReads); } } @@ -665,7 +662,7 @@ public class NetClient implements ApplicationListener{ player.boosting, player.shooting, ui.chatfrag.shown(), control.input.isBuilding, requests, Core.camera.position.x, Core.camera.position.y, - Core.camera.width * viewScale, Core.camera.height * viewScale + Core.camera.width, Core.camera.height ); } diff --git a/core/src/mindustry/core/NetServer.java b/core/src/mindustry/core/NetServer.java index 1e26c88c11..dbdb804e75 100644 --- a/core/src/mindustry/core/NetServer.java +++ b/core/src/mindustry/core/NetServer.java @@ -23,7 +23,6 @@ import mindustry.net.*; import mindustry.net.Administration.*; import mindustry.net.Packets.*; import mindustry.world.*; -import mindustry.world.blocks.storage.CoreBlock.*; import java.io.*; import java.net.*; @@ -38,8 +37,8 @@ public class NetServer implements ApplicationListener{ private static final int maxSnapshotSize = 800, timerBlockSync = 0, serverSyncTime = 200; private static final float blockSyncTime = 60 * 6; private static final FloatBuffer fbuffer = FloatBuffer.allocate(20); + private static final Writes dataWrites = new Writes(null); private static final Vec2 vector = new Vec2(); - private static final Rect viewport = new Rect(); /** If a player goes away of their server-side coordinates by this distance, they get teleported back. */ private static final float correctDist = tilesize * 14f; @@ -101,6 +100,8 @@ public class NetServer implements ApplicationListener{ packet.uuid = con.address.substring("steam:".length()); } + Events.fire(new ConnectPacketEvent(con, packet)); + con.connectTime = Time.millis(); String uuid = packet.uuid; @@ -109,8 +110,7 @@ public class NetServer implements ApplicationListener{ crc.update(buuid, 0, 8); ByteBuffer buff = ByteBuffer.allocate(8); buff.put(buuid, 8, 8); - buff.position(0); - if(crc.getValue() != buff.getLong()){ + if(crc.getValue() != buff.getLong(0)){ con.kick(KickReason.clientOutdated); return; } @@ -254,22 +254,6 @@ public class NetServer implements ApplicationListener{ Events.fire(new PlayerConnect(player)); }); - net.handleServer(InvokePacket.class, (con, packet) -> { - if(con.player == null || con.kicked) return; - - try{ - RemoteReadServer.readPacket(packet.reader(), packet.type, con.player); - }catch(ValidateException e){ - debug("Validation failed for '@': @", e.player, e.getMessage()); - }catch(RuntimeException e){ - if(e.getCause() instanceof ValidateException v){ - debug("Validation failed for '@': @", v.player, v.getMessage()); - }else{ - throw e; - } - } - }); - registerCommands(); } @@ -659,7 +643,7 @@ public class NetServer implements ApplicationListener{ if(!player.dead()){ Unit unit = player.unit(); - long elapsed = Time.timeSinceMillis(con.lastReceivedClientTime); + long elapsed = Math.min(Time.timeSinceMillis(con.lastReceivedClientTime), 1500); float maxSpeed = unit.realSpeed(); float maxMove = elapsed / 1000f * 60f * maxSpeed * 1.2f; @@ -847,8 +831,7 @@ public class NetServer implements ApplicationListener{ if(syncStream.size() > maxSnapshotSize){ dataStream.close(); - byte[] stateBytes = syncStream.toByteArray(); - Call.blockSnapshot(sent, (short)stateBytes.length, net.compressSnapshot(stateBytes)); + Call.blockSnapshot(sent, syncStream.toByteArray()); sent = 0; syncStream.reset(); } @@ -856,31 +839,30 @@ public class NetServer implements ApplicationListener{ if(sent > 0){ dataStream.close(); - byte[] stateBytes = syncStream.toByteArray(); - Call.blockSnapshot(sent, (short)stateBytes.length, net.compressSnapshot(stateBytes)); + Call.blockSnapshot(sent, syncStream.toByteArray()); } } public void writeEntitySnapshot(Player player) throws IOException{ + byte tps = (byte)Math.min(Core.graphics.getFramesPerSecond(), 255); syncStream.reset(); - int sum = state.teams.present.sum(t -> t.cores.size); + int activeTeams = (byte)state.teams.present.count(t -> t.cores.size > 0); - dataStream.writeInt(sum); + dataStream.writeByte(activeTeams); + dataWrites.output = dataStream; + //block data isn't important, just send the items for each team, they're synced across cores for(TeamData data : state.teams.present){ - for(CoreBuild entity : data.cores){ - dataStream.writeInt(entity.tile.pos()); - entity.items.write(Writes.get(dataStream)); + if(data.cores.size > 0){ + dataStream.writeByte(data.team.id); + data.cores.first().items.write(dataWrites); } } dataStream.close(); - byte[] stateBytes = syncStream.toByteArray(); //write basic state data. - Call.stateSnapshot(player.con, state.wavetime, state.wave, state.enemies, state.serverPaused, state.gameOver, universe.seconds(), (short)stateBytes.length, net.compressSnapshot(stateBytes)); - - viewport.setSize(player.con.viewWidth, player.con.viewHeight).setCenter(player.con.viewX, player.con.viewY); + Call.stateSnapshot(player.con, state.wavetime, state.wave, state.enemies, state.serverPaused, state.gameOver, universe.seconds(), tps, syncStream.toByteArray()); syncStream.reset(); @@ -896,8 +878,7 @@ public class NetServer implements ApplicationListener{ if(syncStream.size() > maxSnapshotSize){ dataStream.close(); - byte[] syncBytes = syncStream.toByteArray(); - Call.entitySnapshot(player.con, (short)sent, (short)syncBytes.length, net.compressSnapshot(syncBytes)); + Call.entitySnapshot(player.con, (short)sent, syncStream.toByteArray()); sent = 0; syncStream.reset(); } @@ -906,8 +887,7 @@ public class NetServer implements ApplicationListener{ if(sent > 0){ dataStream.close(); - byte[] syncBytes = syncStream.toByteArray(); - Call.entitySnapshot(player.con, (short)sent, (short)syncBytes.length, net.compressSnapshot(syncBytes)); + Call.entitySnapshot(player.con, (short)sent, syncStream.toByteArray()); } } diff --git a/core/src/mindustry/core/Platform.java b/core/src/mindustry/core/Platform.java index 911583f822..2adf5dc790 100644 --- a/core/src/mindustry/core/Platform.java +++ b/core/src/mindustry/core/Platform.java @@ -21,8 +21,8 @@ import static mindustry.Vars.*; public interface Platform{ /** Dynamically creates a class loader for a jar file. */ - default ClassLoader loadJar(Fi jar, String mainClass) throws Exception{ - return new URLClassLoader(new URL[]{jar.file().toURI().toURL()}, getClass().getClassLoader()); + default ClassLoader loadJar(Fi jar, ClassLoader parent) throws Exception{ + return new URLClassLoader(new URL[]{jar.file().toURI().toURL()}, parent); } /** Steam: Update lobby visibility.*/ @@ -59,6 +59,15 @@ public interface Platform{ } default Context getScriptContext(){ + ContextFactory.getGlobalSetter().setContextFactoryGlobal(new ContextFactory(){ + @Override + protected Context makeContext(){ + Context ctx = super.makeContext(); + ctx.setClassShutter(Scripts::allowClass); + return ctx; + } + }); + Context c = Context.enter(); c.setOptimizationLevel(9); return c; diff --git a/core/src/mindustry/core/Renderer.java b/core/src/mindustry/core/Renderer.java index 37953f0f9c..83f32408a8 100644 --- a/core/src/mindustry/core/Renderer.java +++ b/core/src/mindustry/core/Renderer.java @@ -3,17 +3,19 @@ package mindustry.core; import arc.*; import arc.files.*; import arc.graphics.*; +import arc.graphics.Texture.*; import arc.graphics.g2d.*; import arc.graphics.gl.*; import arc.math.*; import arc.scene.ui.layout.*; +import arc.struct.*; import arc.util.*; +import arc.util.async.*; import mindustry.content.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.graphics.g3d.*; -import mindustry.ui.*; import mindustry.world.blocks.storage.CoreBlock.*; import static arc.Core.*; @@ -35,6 +37,8 @@ public class Renderer implements ApplicationListener{ public boolean animateShields, drawWeather = true, drawStatus; /** minZoom = zooming out, maxZoom = zooming in */ public float minZoom = 1.5f, maxZoom = 6f; + public Seq envRenderers = new Seq<>(); + public TextureRegion[] bubbles = new TextureRegion[16], splashes = new TextureRegion[12]; private @Nullable CoreBuild landCore; private Color clearColor = new Color(0f, 0f, 0f, 1f); @@ -51,6 +55,10 @@ public class Renderer implements ApplicationListener{ shaketime = Math.max(shaketime, duration); } + public void addEnvRenderer(int mask, Runnable render){ + envRenderers.add(new EnvRenderer(mask, render)); + } + @Override public void init(){ planets = new PlanetRenderer(); @@ -59,9 +67,18 @@ public class Renderer implements ApplicationListener{ setupBloom(); } - Events.on(WorldLoadEvent.class, e -> { + Events.run(Trigger.newGame, () -> { landCore = player.bestCore(); }); + + EnvRenderers.init(); + for(int i = 0; i < bubbles.length; i++) bubbles[i] = atlas.find("bubble-" + i); + for(int i = 0; i < splashes.length; i++) splashes[i] = atlas.find("splash-" + i); + + assets.load("sprites/clouds.png", Texture.class).loaded = t -> { + ((Texture)t).setWrap(TextureWrap.repeat); + ((Texture)t).setFilter(TextureFilter.linear); + }; } @Override @@ -77,7 +94,9 @@ public class Renderer implements ApplicationListener{ drawStatus = Core.settings.getBool("blockstatus"); if(landTime > 0){ - landTime -= Time.delta; + if(!state.isPaused()){ + landTime -= Time.delta; + } landscale = Interp.pow5In.apply(minZoomScl, Scl.scl(4f), 1f - landTime / Fx.coreLand.lifetime); camerascale = landscale; weatherAlpha = 0f; @@ -204,6 +223,13 @@ public class Renderer implements ApplicationListener{ Draw.drawRange(Layer.blockBuilding, () -> Draw.shader(Shaders.blockbuild, true), Draw::shader); + //render all matching environments + for(var renderer : envRenderers){ + if((renderer.env & state.rules.environment) == renderer.env){ + renderer.renderer.run(); + } + } + if(state.rules.lighting){ Draw.draw(Layer.light, lights::draw); } @@ -214,8 +240,8 @@ public class Renderer implements ApplicationListener{ if(bloom != null){ bloom.resize(graphics.getWidth() / 4, graphics.getHeight() / 4); - Draw.draw(Layer.bullet - 0.01f, bloom::capture); - Draw.draw(Layer.effect + 0.01f, bloom::render); + Draw.draw(Layer.bullet - 0.02f, bloom::capture); + Draw.draw(Layer.effect + 0.02f, bloom::render); } Draw.draw(Layer.plans, overlays::drawBottom); @@ -252,24 +278,38 @@ public class Renderer implements ApplicationListener{ private void drawLanding(){ CoreBuild entity = landCore == null ? player.bestCore() : landCore; + //var clouds = assets.get("sprites/clouds.png", Texture.class); if(landTime > 0 && entity != null){ - float fract = landTime / Fx.coreLand.lifetime; + float fout = landTime / Fx.coreLand.lifetime; - TextureRegion reg = entity.block.icon(Cicon.full); + //TODO clouds + /* + float scaling = 10000f; + float sscl = 1f + fout*1.5f; + float offset = -0.38f; + + Tmp.tr1.set(clouds); + Tmp.tr1.set((camera.position.x - camera.width/2f * sscl) / scaling, (camera.position.y - camera.height/2f * sscl) / scaling, (camera.position.x + camera.width/2f * sscl) / scaling, (camera.position.y + camera.height/2f * sscl) / scaling); + Draw.alpha(Mathf.slope(Mathf.clamp(((1f - fout) + offset)/(1f + offset)))); + Draw.mixcol(Pal.spore, 0.5f); + Draw.rect(Tmp.tr1, camera.position.x, camera.position.y, camera.width, camera.height); + Draw.reset();*/ + + TextureRegion reg = entity.block.fullIcon; float scl = Scl.scl(4f) / camerascale; - float s = reg.width * Draw.scl * scl * 4f * fract; + float s = reg.width * Draw.scl * scl * 4f * fout; Draw.color(Pal.lightTrail); Draw.rect("circle-shadow", entity.x, entity.y, s, s); - Angles.randLenVectors(1, (1f- fract), 100, 1000f * scl * (1f-fract), (x, y, fin, fout) -> { - Lines.stroke(scl * fin); - Lines.lineAngle(entity.x + x, entity.y + y, Mathf.angle(x, y), (fin * 20 + 1f) * scl); + Angles.randLenVectors(1, (1f- fout), 100, 1000f * scl * (1f-fout), (x, y, ffin, ffout) -> { + Lines.stroke(scl * ffin); + Lines.lineAngle(entity.x + x, entity.y + y, Mathf.angle(x, y), (ffin * 20 + 1f) * scl); }); Draw.color(); - Draw.mixcol(Color.white, fract); - Draw.rect(reg, entity.x, entity.y, reg.width * Draw.scl * scl, reg.height * Draw.scl * scl, fract * 135f); + Draw.mixcol(Color.white, fout); + Draw.rect(reg, entity.x, entity.y, reg.width * Draw.scl * scl, reg.height * Draw.scl * scl, fout * 135f); Draw.reset(); } @@ -330,25 +370,39 @@ public class Renderer implements ApplicationListener{ camera.position.y = h / 2f + tilesize / 2f; buffer.begin(); draw(); + Draw.flush(); + byte[] lines = ScreenUtils.getFrameBufferPixels(0, 0, w, h, true); buffer.end(); disableUI = false; camera.width = vpW; camera.height = vpH; camera.position.set(px, py); - buffer.begin(); - byte[] lines = ScreenUtils.getFrameBufferPixels(0, 0, w, h, true); - for(int i = 0; i < lines.length; i += 4){ - lines[i + 3] = (byte)255; - } - buffer.end(); - Pixmap fullPixmap = new Pixmap(w, h, Pixmap.Format.rgba8888); - Buffers.copy(lines, 0, fullPixmap.getPixels(), lines.length); - Fi file = screenshotDirectory.child("screenshot-" + Time.millis() + ".png"); - PixmapIO.writePNG(file, fullPixmap); - fullPixmap.dispose(); - ui.showInfoFade(Core.bundle.format("screenshot", file.toString())); drawWeather = true; - buffer.dispose(); + + Threads.thread(() -> { + for(int i = 0; i < lines.length; i += 4){ + lines[i + 3] = (byte)255; + } + Pixmap fullPixmap = new Pixmap(w, h); + Buffers.copy(lines, 0, fullPixmap.getPixels(), lines.length); + Fi file = screenshotDirectory.child("screenshot-" + Time.millis() + ".png"); + PixmapIO.writePng(file, fullPixmap); + fullPixmap.dispose(); + app.post(() -> ui.showInfoFade(Core.bundle.format("screenshot", file.toString()))); + }); } + + public static class EnvRenderer{ + /** Environment bitmask; must match env exactly when and-ed. */ + public final int env; + /** Rendering callback. */ + public final Runnable renderer; + + public EnvRenderer(int env, Runnable renderer){ + this.env = env; + this.renderer = renderer; + } + } + } diff --git a/core/src/mindustry/core/UI.java b/core/src/mindustry/core/UI.java index 279427f6f4..44d4008dba 100644 --- a/core/src/mindustry/core/UI.java +++ b/core/src/mindustry/core/UI.java @@ -33,6 +33,8 @@ import static arc.scene.actions.Actions.*; import static mindustry.Vars.*; public class UI implements ApplicationListener, Loadable{ + private static String billions, millions, thousands; + public static PixmapPacker packer; public MenuFragment menufrag; @@ -56,7 +58,7 @@ public class UI implements ApplicationListener, Loadable{ public HostDialog host; public PausedDialog paused; public SettingsMenuDialog settings; - public ControlsDialog controls; + public KeybindDialog controls; public MapEditorDialog editor; public LanguageDialog language; public BansDialog bans; @@ -152,6 +154,10 @@ public class UI implements ApplicationListener, Loadable{ @Override public void init(){ + billions = Core.bundle.get("unit.billions"); + millions = Core.bundle.get("unit.millions"); + thousands = Core.bundle.get("unit.thousands"); + menuGroup = new WidgetGroup(); hudGroup = new WidgetGroup(); @@ -166,7 +172,7 @@ public class UI implements ApplicationListener, Loadable{ picker = new ColorPicker(); editor = new MapEditorDialog(); - controls = new ControlsDialog(); + controls = new KeybindDialog(); restart = new GameOverDialog(); join = new JoinDialog(); discord = new DiscordDialog(); @@ -347,17 +353,11 @@ public class UI implements ApplicationListener, Loadable{ } public void showInfo(String info){ - showInfo(info, () -> {}); - } - - public void showInfo(String info, Runnable listener){ new Dialog(""){{ getCell(cont).growX(); cont.margin(15).add(info).width(400f).wrap().get().setAlignment(Align.center, Align.center); - buttons.button("@ok", () -> { - hide(); - listener.run(); - }).size(110, 50).pad(4); + buttons.button("@ok", this::hide).size(110, 50).pad(4); + keyDown(KeyCode.enter, this::hide); closeOnBack(); }}.show(); } @@ -458,6 +458,10 @@ public class UI implements ApplicationListener, Loadable{ }}.show(); } + public void showConfirm(String text, Runnable confirmed){ + showConfirm("@confirm", text, null, confirmed); + } + public void showConfirm(String title, String text, Runnable confirmed){ showConfirm(title, text, null, confirmed); } @@ -535,6 +539,13 @@ public class UI implements ApplicationListener, Loadable{ dialog.show(); } + public static String formatTime(float ticks){ + int time = (int)(ticks / 60); + if(time < 60) return "0:" + (time < 10 ? "0" : "") + time; + int mod = time % 60; + return (time / 60) + ":" + (mod < 10 ? "0" : "") + mod; + } + public static String formatAmount(long number){ //prevent overflow if(number == Long.MIN_VALUE) number ++; @@ -542,13 +553,13 @@ public class UI implements ApplicationListener, Loadable{ long mag = Math.abs(number); String sign = number < 0 ? "-" : ""; if(mag >= 1_000_000_000){ - return sign + Strings.fixed(mag / 1_000_000_000f, 1) + "[gray]" + Core.bundle.get("unit.billions") + "[]"; + return sign + Strings.fixed(mag / 1_000_000_000f, 1) + "[gray]" + billions+ "[]"; }else if(mag >= 1_000_000){ - return sign + Strings.fixed(mag / 1_000_000f, 1) + "[gray]" + Core.bundle.get("unit.millions") + "[]"; + return sign + Strings.fixed(mag / 1_000_000f, 1) + "[gray]" +millions + "[]"; }else if(mag >= 10_000){ - return number / 1000 + "[gray]" + Core.bundle.get("unit.thousands") + "[]"; + return number / 1000 + "[gray]" + thousands + "[]"; }else if(mag >= 1000){ - return sign + Strings.fixed(mag / 1000f, 1) + "[gray]" + Core.bundle.get("unit.thousands") + "[]"; + return sign + Strings.fixed(mag / 1000f, 1) + "[gray]" + thousands + "[]"; }else{ return number + ""; } diff --git a/core/src/mindustry/core/World.java b/core/src/mindustry/core/World.java index dcb53437c3..38a4954792 100644 --- a/core/src/mindustry/core/World.java +++ b/core/src/mindustry/core/World.java @@ -271,13 +271,13 @@ public class World{ private void setSectorRules(Sector sector){ state.map = new Map(StringMap.of("name", sector.preset == null ? sector.planet.localizedName + "; Sector " + sector.id : sector.preset.localizedName)); state.rules.sector = sector; - state.rules.weather.clear(); - //apply weather based on terrain - ObjectIntMap floorc = new ObjectIntMap<>(); + sector.planet.generator.addWeather(sector, state.rules); + ObjectSet content = new ObjectSet<>(); + //TODO duplicate code? for(Tile tile : world.tiles){ if(world.getDarkness(tile.x, tile.y) >= 3){ continue; @@ -287,47 +287,6 @@ public class World{ if(tile.floor().itemDrop != null) content.add(tile.floor().itemDrop); if(tile.overlay().itemDrop != null) content.add(tile.overlay().itemDrop); if(liquid != null) content.add(liquid); - - if(!tile.block().isStatic()){ - floorc.increment(tile.floor()); - if(tile.overlay() != Blocks.air){ - floorc.increment(tile.overlay()); - } - } - } - - //sort counts in descending order - Seq> entries = floorc.entries().toArray(); - entries.sort(e -> -e.value); - //remove all blocks occuring < 30 times - unimportant - entries.removeAll(e -> e.value < 30); - - Block[] floors = new Block[entries.size]; - for(int i = 0; i < entries.size; i++){ - floors[i] = entries.get(i).key; - } - - //TODO bad code - boolean hasSnow = floors[0].name.contains("ice") || floors[0].name.contains("snow"); - boolean hasRain = !hasSnow && content.contains(Liquids.water) && !floors[0].name.contains("sand"); - boolean hasDesert = !hasSnow && !hasRain && floors[0] == Blocks.sand; - boolean hasSpores = floors[0].name.contains("spore") || floors[0].name.contains("moss") || floors[0].name.contains("tainted"); - - if(hasSnow){ - state.rules.weather.add(new WeatherEntry(Weathers.snow)); - } - - if(hasRain){ - state.rules.weather.add(new WeatherEntry(Weathers.rain)); - state.rules.weather.add(new WeatherEntry(Weathers.fog)); - } - - if(hasDesert){ - state.rules.weather.add(new WeatherEntry(Weathers.sandstorm)); - } - - if(hasSpores){ - state.rules.weather.add(new WeatherEntry(Weathers.sporestorm)); } sector.info.resources = content.asArray(); @@ -393,12 +352,6 @@ public class World{ if(invalidMap) Core.app.post(() -> state.set(State.menu)); } - public void notifyChanged(Tile tile){ - if(!generating){ - Core.app.post(() -> Events.fire(new TileChangeEvent(tile))); - } - } - public void raycastEachWorld(float x0, float y0, float x1, float y1, Raycaster cons){ raycastEach(toTile(x0), toTile(y0), toTile(x1), toTile(y1), cons); } @@ -464,7 +417,7 @@ public class World{ byte[] dark = new byte[tiles.width * tiles.height]; byte[] writeBuffer = new byte[tiles.width * tiles.height]; - byte darkIterations = 4; + byte darkIterations = darkRadius; for(int i = 0; i < dark.length; i++){ Tile tile = tiles.geti(i); @@ -498,7 +451,7 @@ public class World{ tile.data = dark[idx]; } - if(dark[idx] == 4){ + if(dark[idx] == darkRadius){ boolean full = true; for(Point2 p : Geometry.d4){ int px = p.x + tile.x, py = p.y + tile.y; @@ -509,11 +462,28 @@ public class World{ } } - if(full) tile.data = 5; + if(full) tile.data = darkRadius + 1; } } } + public byte getWallDarkness(Tile tile){ + if(tile.isDarkened()){ + int minDst = darkRadius + 1; + for(int cx = tile.x - darkRadius; cx <= tile.x + darkRadius; cx++){ + for(int cy = tile.y - darkRadius; cy <= tile.y + darkRadius; cy++){ + if(tiles.in(cx, cy) && !rawTile(cx, cy).isDarkened()){ + minDst = Math.min(minDst, Math.abs(cx - tile.x) + Math.abs(cy - tile.y)); + } + } + } + + return (byte)Math.max((minDst - 1), 0); + } + return 0; + } + + //TODO optimize; this is very slow and called too often! public float getDarkness(int x, int y){ int edgeBlend = 2; diff --git a/core/src/mindustry/ctype/Content.java b/core/src/mindustry/ctype/Content.java index 71bf965e3f..4103f4899e 100644 --- a/core/src/mindustry/ctype/Content.java +++ b/core/src/mindustry/ctype/Content.java @@ -7,7 +7,7 @@ import mindustry.mod.Mods.*; /** Base class for a content type that is loaded in {@link mindustry.core.ContentLoader}. */ public abstract class Content implements Comparable, Disposable{ - public final short id; + public short id; /** Info on which mod this content was loaded from. */ public ModContentInfo minfo = new ModContentInfo(); @@ -31,6 +31,9 @@ public abstract class Content implements Comparable, Disposable{ */ public void load(){} + /** Called right after load(). */ + public void loadIcon(){} + /** @return whether an error occurred during mod loading. */ public boolean hasErrored(){ return minfo.error != null; diff --git a/core/src/mindustry/ctype/UnlockableContent.java b/core/src/mindustry/ctype/UnlockableContent.java index 85b29469ae..c28496bb81 100644 --- a/core/src/mindustry/ctype/UnlockableContent.java +++ b/core/src/mindustry/ctype/UnlockableContent.java @@ -3,7 +3,6 @@ package mindustry.ctype; import arc.*; import arc.func.*; import arc.graphics.g2d.*; -import arc.scene.ui.layout.*; import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; @@ -30,8 +29,10 @@ public abstract class UnlockableContent extends MappableContent{ public boolean inlineDescription = true; /** Special logic icon ID. */ public int iconId = 0; - /** Icons by Cicon ID.*/ - protected TextureRegion[] cicons = new TextureRegion[Cicon.all.length]; + /** Icon of the content to use in UI. */ + public TextureRegion uiIcon; + /** Icon of the full content. Unscaled.*/ + public TextureRegion fullIcon; /** Unlock state. Loaded from settings. Do not modify outside of the constructor. */ protected boolean unlocked; @@ -44,11 +45,29 @@ public abstract class UnlockableContent extends MappableContent{ this.unlocked = Core.settings != null && Core.settings.getBool(this.name + "-unlocked", false); } + @Override + public void loadIcon(){ + fullIcon = + Core.atlas.find(getContentType().name() + "-" + name + "-full", + Core.atlas.find(name + "-full", + Core.atlas.find(name, + Core.atlas.find(getContentType().name() + "-" + name, + Core.atlas.find(name + "1"))))); + + uiIcon = Core.atlas.find(getContentType().name() + "-" + name + "-ui", fullIcon); + } + /** @return the tech node for this content. may be null. */ public @Nullable TechNode node(){ return TechTree.get(this); } + /** Use fullIcon / uiIcon instead! This will be removed. */ + @Deprecated + public TextureRegion icon(Cicon icon){ + return icon == Cicon.full ? fullIcon : uiIcon; + } + public String displayDescription(){ return minfo.mod == null ? description : description + "\n" + Core.bundle.format("mod.display", minfo.mod.meta.displayName()); } @@ -65,7 +84,10 @@ public abstract class UnlockableContent extends MappableContent{ public void setStats(){ } - /** Generate any special icons for this content. Called asynchronously.*/ + /** + * Generate any special icons for this content. Called synchronously. + * No regions are loaded at this point; grab pixmaps from the packer. + * */ @CallSuper public void createIcons(MultiPacker packer){ @@ -80,23 +102,8 @@ public abstract class UnlockableContent extends MappableContent{ return Fonts.getUnicodeStr(name); } - /** Returns a specific content icon, or the region {contentType}-{name} if not found.*/ - public TextureRegion icon(Cicon icon){ - if(cicons[icon.ordinal()] == null){ - cicons[icon.ordinal()] = - Core.atlas.find(getContentType().name() + "-" + name + "-" + icon.name(), - Core.atlas.find(getContentType().name() + "-" + name + "-full", - Core.atlas.find(name + "-" + icon.name(), - Core.atlas.find(name + "-full", - Core.atlas.find(name, - Core.atlas.find(getContentType().name() + "-" + name, - Core.atlas.find(name + "1"))))))); - } - return cicons[icon.ordinal()]; - } - - public Cicon prefDatabaseIcon(){ - return Cicon.xlarge; + public boolean hasEmoji(){ + return Fonts.hasUnicodeStr(name); } /** Iterates through any implicit dependencies of this content. @@ -105,11 +112,6 @@ public abstract class UnlockableContent extends MappableContent{ } - /** This should show all necessary info about this content in the specified table. */ - public void display(Table table){ - - } - /** Called when this content is unlocked. Use this to unlock other related content. */ public void onUnlock(){ } @@ -144,7 +146,7 @@ public abstract class UnlockableContent extends MappableContent{ } public boolean unlocked(){ - if(net != null && net.client()) return unlocked || alwaysUnlocked || state.rules.researched.contains(name); + if(net != null && net.client()) return alwaysUnlocked || state.rules.researched.contains(name); return unlocked || alwaysUnlocked; } diff --git a/core/src/mindustry/editor/DrawOperation.java b/core/src/mindustry/editor/DrawOperation.java index 53134663ea..8681c679ff 100755 --- a/core/src/mindustry/editor/DrawOperation.java +++ b/core/src/mindustry/editor/DrawOperation.java @@ -10,13 +10,8 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; public class DrawOperation{ - private MapEditor editor; private LongSeq array = new LongSeq(); - public DrawOperation(MapEditor editor){ - this.editor = editor; - } - public boolean isEmpty(){ return array.isEmpty(); } diff --git a/core/src/mindustry/editor/EditorTile.java b/core/src/mindustry/editor/EditorTile.java index e4be334888..69b00f3495 100644 --- a/core/src/mindustry/editor/EditorTile.java +++ b/core/src/mindustry/editor/EditorTile.java @@ -77,7 +77,7 @@ public class EditorTile extends Tile{ op(OpType.team, (byte)getTeamID()); super.setTeam(team); - getLinkedTiles(t -> ui.editor.editor.renderer.updatePoint(t.x, t.y)); + getLinkedTiles(t -> editor.renderer.updatePoint(t.x, t.y)); } @Override @@ -140,14 +140,14 @@ public class EditorTile extends Tile{ } private void update(){ - ui.editor.editor.renderer.updatePoint(x, y); + editor.renderer.updatePoint(x, y); } private boolean skip(){ - return state.isGame() || ui.editor.editor.isLoading(); + return state.isGame() || editor.isLoading(); } private void op(OpType type, short value){ - ui.editor.editor.addTileOp(TileOp.get(x, y, (byte)type.ordinal(), value)); + editor.addTileOp(TileOp.get(x, y, (byte)type.ordinal(), value)); } } diff --git a/core/src/mindustry/editor/EditorTool.java b/core/src/mindustry/editor/EditorTool.java index cd4b63935e..b560d56e5b 100644 --- a/core/src/mindustry/editor/EditorTool.java +++ b/core/src/mindustry/editor/EditorTool.java @@ -9,11 +9,12 @@ import arc.util.*; import mindustry.content.*; import mindustry.game.*; import mindustry.world.*; +import static mindustry.Vars.*; public enum EditorTool{ zoom(KeyCode.v), pick(KeyCode.i){ - public void touched(MapEditor editor, int x, int y){ + public void touched(int x, int y){ if(!Structs.inBounds(x, y, editor.width(), editor.height())) return; Tile tile = editor.tile(x, y); @@ -23,7 +24,7 @@ public enum EditorTool{ line(KeyCode.l, "replace", "orthogonal"){ @Override - public void touchedLine(MapEditor editor, int x1, int y1, int x2, int y2){ + public void touchedLine(int x1, int y1, int x2, int y2){ //straight if(mode == 1){ if(Math.abs(x2 - x1) > Math.abs(y2 - y1)){ @@ -51,7 +52,7 @@ public enum EditorTool{ } @Override - public void touched(MapEditor editor, int x, int y){ + public void touched(int x, int y){ if(mode == -1){ //normal mode editor.drawBlocks(x, y); @@ -75,7 +76,7 @@ public enum EditorTool{ } @Override - public void touched(MapEditor editor, int x, int y){ + public void touched(int x, int y){ editor.drawCircle(x, y, tile -> { if(mode == -1){ //erase block @@ -95,13 +96,13 @@ public enum EditorTool{ IntSeq stack = new IntSeq(); @Override - public void touched(MapEditor editor, int x, int y){ + public void touched(int x, int y){ if(!Structs.inBounds(x, y, editor.width(), editor.height())) return; Tile tile = editor.tile(x, y); if(editor.drawBlock.isMultiblock()){ //don't fill multiblocks, thanks - pencil.touched(editor, x, y); + pencil.touched(x, y); return; } @@ -133,19 +134,19 @@ public enum EditorTool{ } //replace only when the mode is 0 using the specified functions - fill(editor, x, y, mode == 0, tester, setter); + fill(x, y, mode == 0, tester, setter); }else if(mode == 1){ //mode 1 is team fill //only fill synthetic blocks, it's meaningless otherwise if(tile.synthetic()){ Team dest = tile.team(); if(dest == editor.drawTeam) return; - fill(editor, x, y, false, t -> t.getTeamID() == dest.id && t.synthetic(), t -> t.setTeam(editor.drawTeam)); + fill(x, y, false, t -> t.getTeamID() == dest.id && t.synthetic(), t -> t.setTeam(editor.drawTeam)); } } } - void fill(MapEditor editor, int x, int y, boolean replace, Boolf tester, Cons filler){ + void fill(int x, int y, boolean replace, Boolf tester, Cons filler){ int width = editor.width(), height = editor.height(); if(replace){ @@ -215,7 +216,7 @@ public enum EditorTool{ } @Override - public void touched(MapEditor editor, int x, int y){ + public void touched(int x, int y){ //floor spray if(editor.drawBlock.isFloor()){ @@ -263,7 +264,7 @@ public enum EditorTool{ this.key = code; } - public void touched(MapEditor editor, int x, int y){} + public void touched(int x, int y){} - public void touchedLine(MapEditor editor, int x1, int y1, int x2, int y2){} + public void touchedLine(int x1, int y1, int x2, int y2){} } diff --git a/core/src/mindustry/editor/MapEditor.java b/core/src/mindustry/editor/MapEditor.java index 9b62ae40a6..b603182e43 100644 --- a/core/src/mindustry/editor/MapEditor.java +++ b/core/src/mindustry/editor/MapEditor.java @@ -21,7 +21,7 @@ public class MapEditor{ public static final int[] brushSizes = {1, 2, 3, 4, 5, 9, 15, 20}; public StringMap tags = new StringMap(); - public MapRenderer renderer = new MapRenderer(this); + public MapRenderer renderer = new MapRenderer(); private final Context context = new Context(); private OperationStack stack = new OperationStack(); @@ -62,7 +62,7 @@ public class MapEditor{ public void beginEdit(Pixmap pixmap){ reset(); - createTiles(pixmap.getWidth(), pixmap.getHeight()); + createTiles(pixmap.width, pixmap.height); load(() -> MapIO.readImage(pixmap, tiles())); renderer.resize(width(), height()); } @@ -330,7 +330,7 @@ public class MapEditor{ public void addTileOp(long data){ if(loading) return; - if(currentOp == null) currentOp = new DrawOperation(this); + if(currentOp == null) currentOp = new DrawOperation(); currentOp.addOperation(data); renderer.updatePoint(TileOp.x(data), TileOp.y(data)); diff --git a/core/src/mindustry/editor/MapEditorDialog.java b/core/src/mindustry/editor/MapEditorDialog.java index f862256995..ac7631cff3 100644 --- a/core/src/mindustry/editor/MapEditorDialog.java +++ b/core/src/mindustry/editor/MapEditorDialog.java @@ -33,8 +33,6 @@ import mindustry.world.meta.*; import static mindustry.Vars.*; public class MapEditorDialog extends Dialog implements Disposable{ - public final MapEditor editor; - private MapView view; private MapInfoDialog infoDialog; private MapLoadDialog loadDialog; @@ -53,10 +51,9 @@ public class MapEditorDialog extends Dialog implements Disposable{ background(Styles.black); - editor = new MapEditor(); - view = new MapView(editor); - infoDialog = new MapInfoDialog(editor); - generateDialog = new MapGenerateDialog(editor, true); + view = new MapView(); + infoDialog = new MapInfoDialog(); + generateDialog = new MapGenerateDialog(true); menu = new BaseDialog("@menu"); menu.addCloseButton(); @@ -120,7 +117,7 @@ public class MapEditorDialog extends Dialog implements Disposable{ "@editor.exportimage", "@editor.exportimage.description", Icon.fileImage, (Runnable)() -> platform.export(editor.tags.get("name", "unknown"), "png", file -> { Pixmap out = MapIO.writeImage(editor.tiles()); - file.writePNG(out); + file.writePng(out); out.dispose(); }))); }); @@ -173,7 +170,7 @@ public class MapEditorDialog extends Dialog implements Disposable{ menu.hide(); }).size(swidth * 2f + 10, 60f); - resizeDialog = new MapResizeDialog(editor, (x, y) -> { + resizeDialog = new MapResizeDialog((x, y) -> { if(!(editor.width() == x && editor.height() == y)){ ui.loadAnd(() -> { editor.resize(x, y); @@ -639,7 +636,7 @@ public class MapEditorDialog extends Dialog implements Disposable{ for(int x = 0; x < editor.width(); x++){ for(int y = 0; y < editor.height(); y++){ Tile tile = editor.tile(x, y); - if(tile.block().breakable && tile.block() instanceof Boulder){ + if(tile.block().breakable && tile.block() instanceof Prop){ tile.setBlock(Blocks.air); editor.renderer.updatePoint(x, y); } @@ -714,7 +711,7 @@ public class MapEditorDialog extends Dialog implements Disposable{ int i = 0; for(Block block : blocksOut){ - TextureRegion region = block.icon(Cicon.medium); + TextureRegion region = block.uiIcon; if(!Core.atlas.isFound(region) || !block.inEditor || block.buildVisibility == BuildVisibility.debugOnly diff --git a/core/src/mindustry/editor/MapGenerateDialog.java b/core/src/mindustry/editor/MapGenerateDialog.java index 025dbeedaf..71f4f60264 100644 --- a/core/src/mindustry/editor/MapGenerateDialog.java +++ b/core/src/mindustry/editor/MapGenerateDialog.java @@ -26,28 +26,27 @@ import static mindustry.Vars.*; @SuppressWarnings("unchecked") public class MapGenerateDialog extends BaseDialog{ - private final Prov[] filterTypes = new Prov[]{ + final Prov[] filterTypes = new Prov[]{ NoiseFilter::new, ScatterFilter::new, TerrainFilter::new, DistortFilter::new, RiverNoiseFilter::new, OreFilter::new, OreMedianFilter::new, MedianFilter::new, BlendFilter::new, MirrorFilter::new, ClearFilter::new, CoreSpawnFilter::new, EnemySpawnFilter::new, SpawnPathFilter::new }; - private final MapEditor editor; - private final boolean applied; + final boolean applied; - private Pixmap pixmap; - private Texture texture; - private GenerateInput input = new GenerateInput(); + Pixmap pixmap; + Texture texture; + GenerateInput input = new GenerateInput(); Seq filters = new Seq<>(); - private int scaling = mobile ? 3 : 1; - private Table filterTable; + int scaling = mobile ? 3 : 1; + Table filterTable; - private AsyncExecutor executor = new AsyncExecutor(1); - private AsyncResult result; + AsyncExecutor executor = new AsyncExecutor(1); + AsyncResult result; boolean generating; - private long[] buffer1, buffer2; - private Cons> applier; + long[] buffer1, buffer2; + Cons> applier; CachedTile ctile = new CachedTile(){ //nothing. @Override @@ -62,35 +61,79 @@ public class MapGenerateDialog extends BaseDialog{ }; /** @param applied whether or not to use the applied in-game mode. */ - public MapGenerateDialog(MapEditor editor, boolean applied){ + public MapGenerateDialog(boolean applied){ super("@editor.generate"); - this.editor = editor; this.applied = applied; shown(this::setup); - addCloseButton(); + addCloseListener(); + + var style = Styles.cleart; + + buttons.defaults().size(180f, 64f).pad(2f); + buttons.button("@back", Icon.left, this::hide); + if(applied){ buttons.button("@editor.apply", Icon.ok, () -> { ui.loadAnd(() -> { apply(); hide(); }); - }).size(160f, 64f); - }else{ - buttons.button("@settings.reset", () -> { - filters.set(maps.readFilters("")); - rebuildFilters(); - update(); - }).size(160f, 64f); + }); } + buttons.button("@editor.randomize", Icon.refresh, () -> { for(GenerateFilter filter : filters){ filter.randomize(); } update(); - }).size(160f, 64f); + }); - buttons.button("@add", Icon.add, this::showAdd).height(64f).width(150f); + buttons.button("@edit", Icon.edit, () -> { + BaseDialog dialog = new BaseDialog("@editor.export"); + dialog.cont.pane(p -> { + p.margin(10f); + p.table(Tex.button, in -> { + in.defaults().size(280f, 60f).left(); + + in.button("@waves.copy", Icon.copy, style, () -> { + dialog.hide(); + + Core.app.setClipboardText(JsonIO.write(filters)); + }).marginLeft(12f).row(); + in.button("@waves.load", Icon.download, style, () -> { + dialog.hide(); + try{ + filters.set(JsonIO.read(Seq.class, Core.app.getClipboardText())); + + rebuildFilters(); + update(); + }catch(Throwable e){ + ui.showException(e); + } + }).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null).row(); + in.button("@clear", Icon.none, style, () -> { + dialog.hide(); + filters.clear(); + rebuildFilters(); + update(); + }).marginLeft(12f).row(); + if(!applied){ + in.button("@settings.reset", Icon.refresh, style, () -> { + dialog.hide(); + filters.set(maps.readFilters("")); + rebuildFilters(); + update(); + }).marginLeft(12f).row(); + } + }); + }); + + dialog.addCloseButton(); + dialog.show(); + }); + + buttons.button("@add", Icon.add, this::showAdd); if(!applied){ hidden(this::apply); @@ -173,7 +216,7 @@ public class MapGenerateDialog extends BaseDialog{ @Override public void draw(){ super.draw(); - for(GenerateFilter filter : filters){ + for(var filter : filters){ filter.draw(this); } } @@ -214,7 +257,7 @@ public class MapGenerateDialog extends BaseDialog{ filterTable.top().left(); int i = 0; - for(GenerateFilter filter : filters){ + for(var filter : filters){ //main container filterTable.table(Tex.pane, c -> { @@ -284,31 +327,33 @@ public class MapGenerateDialog extends BaseDialog{ } void showAdd(){ - BaseDialog selection = new BaseDialog("@add"); + var selection = new BaseDialog("@add"); selection.cont.pane(p -> { + p.background(Tex.button); p.marginRight(14); - p.defaults().size(210f, 60f); + p.defaults().size(195f, 56f); int i = 0; - for(Prov gen : filterTypes){ - GenerateFilter filter = gen.get(); + for(var gen : filterTypes){ + var filter = gen.get(); + var icon = filter.icon(); - if((filter.isPost() && applied)) continue; + if(filter.isPost() && applied) continue; - p.button(filter.name(), () -> { + p.button((icon == '\0' ? "" : icon + " ") + filter.name(), Styles.cleart, () -> { filters.add(filter); rebuildFilters(); update(); selection.hide(); - }); - if(++i % 2 == 0) p.row(); + }).with(Table::left).get().getLabelCell().growX().left().padLeft(5).labelAlign(Align.left); + if(++i % 3 == 0) p.row(); } - p.button("@filter.defaultores", () -> { + p.button(Iconc.refresh + " " + Core.bundle.get("filter.defaultores"), Styles.cleart, () -> { maps.addDefaultOres(filters); rebuildFilters(); update(); selection.hide(); - }); + }).with(Table::left).get().getLabelCell().growX().left().padLeft(5).labelAlign(Align.left); }).get().setScrollingDisabled(true, false); selection.addCloseButton(); @@ -350,25 +395,25 @@ public class MapGenerateDialog extends BaseDialog{ return; } - Seq copy = new Seq<>(filters); + var copy = filters.copy(); result = executor.submit(() -> { try{ - int w = pixmap.getWidth(); + int w = pixmap.width; world.setGenerating(true); generating = true; if(!filters.isEmpty()){ //write to buffer1 for reading - for(int px = 0; px < pixmap.getWidth(); px++){ - for(int py = 0; py < pixmap.getHeight(); py++){ + for(int px = 0; px < pixmap.width; px++){ + for(int py = 0; py < pixmap.height; py++){ buffer1[px + py*w] = pack(editor.tile(px * scaling, py * scaling)); } } } - for(GenerateFilter filter : copy){ - input.begin(filter, editor.width(), editor.height(), (x, y) -> unpack(buffer1[Mathf.clamp(x / scaling, 0, pixmap.getWidth()-1) + w* Mathf.clamp(y / scaling, 0, pixmap.getHeight()-1)])); + for(var filter : copy){ + input.begin(filter, editor.width(), editor.height(), (x, y) -> unpack(buffer1[Mathf.clamp(x / scaling, 0, pixmap.width -1) + w* Mathf.clamp(y / scaling, 0, pixmap.height -1)])); //read from buffer1 and write to buffer2 pixmap.each((px, py) -> { @@ -382,8 +427,8 @@ public class MapGenerateDialog extends BaseDialog{ pixmap.each((px, py) -> buffer1[px + py*w] = buffer2[px + py*w]); } - for(int px = 0; px < pixmap.getWidth(); px++){ - for(int py = 0; py < pixmap.getHeight(); py++){ + for(int px = 0; px < pixmap.width; px++){ + for(int py = 0; py < pixmap.height; py++){ int color; //get result from buffer1 if there's filters left, otherwise get from editor directly if(filters.isEmpty()){ @@ -393,7 +438,7 @@ public class MapGenerateDialog extends BaseDialog{ long tile = buffer1[px + py*w]; color = MapIO.colorFor(content.block(PackTile.block(tile)), content.block(PackTile.floor(tile)), content.block(PackTile.overlay(tile)), Team.derelict); } - pixmap.draw(px, pixmap.getHeight() - 1 - py, color); + pixmap.set(px, pixmap.height - 1 - py, color); } } diff --git a/core/src/mindustry/editor/MapInfoDialog.java b/core/src/mindustry/editor/MapInfoDialog.java index dd9959b199..a18828f1f0 100644 --- a/core/src/mindustry/editor/MapInfoDialog.java +++ b/core/src/mindustry/editor/MapInfoDialog.java @@ -9,17 +9,17 @@ import mindustry.io.*; import mindustry.ui.*; import mindustry.ui.dialogs.*; +import static mindustry.Vars.*; + public class MapInfoDialog extends BaseDialog{ - private final MapEditor editor; private final WaveInfoDialog waveInfo; private final MapGenerateDialog generate; private final CustomRulesDialog ruleInfo = new CustomRulesDialog(); - public MapInfoDialog(MapEditor editor){ + public MapInfoDialog(){ super("@editor.mapinfo"); - this.editor = editor; - this.waveInfo = new WaveInfoDialog(editor); - this.generate = new MapGenerateDialog(editor, false); + this.waveInfo = new WaveInfoDialog(); + this.generate = new MapGenerateDialog(false); addCloseButton(); diff --git a/core/src/mindustry/editor/MapRenderer.java b/core/src/mindustry/editor/MapRenderer.java index 919c534128..abbd4baa1a 100644 --- a/core/src/mindustry/editor/MapRenderer.java +++ b/core/src/mindustry/editor/MapRenderer.java @@ -18,14 +18,7 @@ public class MapRenderer implements Disposable{ private IndexedRenderer[][] chunks; private IntSet updates = new IntSet(); private IntSet delayedUpdates = new IntSet(); - private MapEditor editor; private int width, height; - private Texture texture; - - public MapRenderer(MapEditor editor){ - this.editor = editor; - this.texture = Core.atlas.find("clear-editor").texture; - } public void resize(int width, int height){ updates.clear(); @@ -64,6 +57,8 @@ public class MapRenderer implements Disposable{ return; } + var texture = Core.atlas.find("clear-editor").texture; + for(int x = 0; x < chunks.length; x++){ for(int y = 0; y < chunks[0].length; y++){ IndexedRenderer mesh = chunks[x][y]; diff --git a/core/src/mindustry/editor/MapResizeDialog.java b/core/src/mindustry/editor/MapResizeDialog.java index 9d961a4246..f95edff43f 100644 --- a/core/src/mindustry/editor/MapResizeDialog.java +++ b/core/src/mindustry/editor/MapResizeDialog.java @@ -6,13 +6,14 @@ import arc.scene.ui.TextField.*; import arc.scene.ui.layout.*; import arc.util.*; import mindustry.ui.dialogs.*; +import static mindustry.Vars.*; public class MapResizeDialog extends BaseDialog{ public static int minSize = 50, maxSize = 500, increment = 50; int width, height; - public MapResizeDialog(MapEditor editor, Intc2 cons){ + public MapResizeDialog(Intc2 cons){ super("@editor.resizemap"); shown(() -> { cont.clear(); diff --git a/core/src/mindustry/editor/MapView.java b/core/src/mindustry/editor/MapView.java index 69ae67d7c9..fce417ecad 100644 --- a/core/src/mindustry/editor/MapView.java +++ b/core/src/mindustry/editor/MapView.java @@ -19,7 +19,6 @@ import mindustry.ui.*; import static mindustry.Vars.*; public class MapView extends Element implements GestureListener{ - private MapEditor editor; EditorTool tool = EditorTool.pencil; private float offsetx, offsety; private float zoom = 1f; @@ -35,8 +34,7 @@ public class MapView extends Element implements GestureListener{ float mousex, mousey; EditorTool lastTool; - public MapView(MapEditor editor){ - this.editor = editor; + public MapView(){ for(int i = 0; i < MapEditor.brushSizes.length; i++){ float size = MapEditor.brushSizes[i]; @@ -92,7 +90,7 @@ public class MapView extends Element implements GestureListener{ lasty = p.y; startx = p.x; starty = p.y; - tool.touched(editor, p.x, p.y); + tool.touched(p.x, p.y); firstTouch.set(p); if(tool.edit){ @@ -115,7 +113,7 @@ public class MapView extends Element implements GestureListener{ if(tool == EditorTool.line){ ui.editor.resetSaved(); - tool.touchedLine(editor, startx, starty, p.x, p.y); + tool.touchedLine(startx, starty, p.x, p.y); } editor.flushOp(); @@ -136,7 +134,7 @@ public class MapView extends Element implements GestureListener{ if(drawing && tool.draggable && !(p.x == lastx && p.y == lasty)){ ui.editor.resetSaved(); - Bresenham2.line(lastx, lasty, p.x, p.y, (cx, cy) -> tool.touched(editor, cx, cy)); + Bresenham2.line(lastx, lasty, p.x, p.y, (cx, cy) -> tool.touched(cx, cy)); } if(tool == EditorTool.line && tool.mode == 1){ diff --git a/core/src/mindustry/editor/WaveGraph.java b/core/src/mindustry/editor/WaveGraph.java index fd0b05bfb9..cc36f22620 100644 --- a/core/src/mindustry/editor/WaveGraph.java +++ b/core/src/mindustry/editor/WaveGraph.java @@ -177,7 +177,7 @@ public class WaveGraph extends Table{ t.button(b -> { Color tcolor = color(type).cpy(); b.image().size(32f).update(i -> i.setColor(b.isChecked() ? Tmp.c1.set(tcolor).mul(0.5f) : tcolor)).get().act(1); - b.image(type.icon(Cicon.medium)).size(32f).padRight(20).update(i -> i.setColor(b.isChecked() ? Color.gray : Color.white)).get().act(1); + b.image(type.uiIcon).size(32f).padRight(20).update(i -> i.setColor(b.isChecked() ? Color.gray : Color.white)).get().act(1); b.margin(0f); }, Styles.fullTogglet, () -> { if(!hidden.add(type)){ diff --git a/core/src/mindustry/editor/WaveInfoDialog.java b/core/src/mindustry/editor/WaveInfoDialog.java index a71d0a2d8f..d83fa11f3e 100644 --- a/core/src/mindustry/editor/WaveInfoDialog.java +++ b/core/src/mindustry/editor/WaveInfoDialog.java @@ -30,7 +30,7 @@ public class WaveInfoDialog extends BaseDialog{ private float updateTimer, updatePeriod = 1f; private WaveGraph graph = new WaveGraph(); - public WaveInfoDialog(MapEditor editor){ + public WaveInfoDialog(){ super("@waves.title"); shown(this::setup); @@ -160,7 +160,7 @@ public class WaveInfoDialog extends BaseDialog{ t.margin(0).defaults().pad(3).padLeft(5f).growX().left(); t.button(b -> { b.left(); - b.image(group.type.icon(Cicon.medium)).size(32f).padRight(3).scaling(Scaling.fit); + b.image(group.type.uiIcon).size(32f).padRight(3).scaling(Scaling.fit); b.add(group.type.localizedName).color(Pal.accent); b.add().growX(); @@ -263,7 +263,7 @@ public class WaveInfoDialog extends BaseDialog{ if(type.isHidden()) continue; p.button(t -> { t.left(); - t.image(type.icon(Cicon.medium)).size(8 * 4).scaling(Scaling.fit).padRight(2f); + t.image(type.uiIcon).size(8 * 4).scaling(Scaling.fit).padRight(2f); t.add(type.localizedName); }, () -> { lastType = type; diff --git a/core/src/mindustry/entities/Damage.java b/core/src/mindustry/entities/Damage.java index 1e66934577..beb1e2b6c3 100644 --- a/core/src/mindustry/entities/Damage.java +++ b/core/src/mindustry/entities/Damage.java @@ -83,8 +83,17 @@ public class Damage{ } } + public static @Nullable Building findAbsorber(Team team, float x1, float y1, float x2, float y2){ + tmpBuilding = null; + + boolean found = world.raycast(World.toTile(x1), World.toTile(y1), World.toTile(x2), World.toTile(y2), + (x, y) -> (tmpBuilding = world.build(x, y)) != null && tmpBuilding.team != team && tmpBuilding.block.absorbLasers); + + return found ? tmpBuilding : null; + } + public static float findLaserLength(Bullet b, float length){ - Tmp.v1.trns(b.rotation(), length); + Tmp.v1.trnsExact(b.rotation(), length); furthest = null; @@ -125,7 +134,7 @@ public class Damage{ if(laser) length = findLaserLength(hitter, length); collidedBlocks.clear(); - tr.trns(angle, length); + tr.trnsExact(angle, length); Intc2 collider = (cx, cy) -> { Building tile = world.build(cx, cy); @@ -378,7 +387,7 @@ public class Damage{ //this needs to be compensated if(in != null && in.team != team && in.block.size > 1 && in.health > damage){ //deal the damage of an entire side, to be equivalent with maximum 'standard' damage - in.damage(damage * Math.min((in.block.size), baseRadius * 0.45f)); + in.damage(team, damage * Math.min((in.block.size), baseRadius * 0.4f)); //no need to continue with the explosion return; } @@ -435,7 +444,7 @@ public class Damage{ int cx = Point2.x(e.key), cy = Point2.y(e.key); var build = world.build(cx, cy); if(build != null){ - build.damage(e.value); + build.damage(team, e.value); } } }); diff --git a/core/src/mindustry/entities/Effect.java b/core/src/mindustry/entities/Effect.java index 6fa163b074..7b72ecdc01 100644 --- a/core/src/mindustry/entities/Effect.java +++ b/core/src/mindustry/entities/Effect.java @@ -29,6 +29,8 @@ public class Effect{ public float lifetime = 50f; /** Clip size. */ public float clip; + /** If true, parent unit is data are followed. */ + public boolean followParent; public float layer = Layer.effect; public float layerDuration; @@ -53,6 +55,11 @@ public class Effect{ public void init(){} + public Effect followParent(boolean follow){ + followParent = follow; + return this; + } + public Effect layer(float l){ layer = l; return this; @@ -148,11 +155,11 @@ public class Effect{ EffectState entity = EffectState.create(); entity.effect = effect; entity.rotation = rotation; - entity.data = (data); - entity.lifetime = (effect.lifetime); + entity.data = data; + entity.lifetime = effect.lifetime; entity.set(x, y); entity.color.set(color); - if(data instanceof Posc) entity.parent = ((Posc)data); + if(effect.followParent && data instanceof Posc) entity.parent = ((Posc)data); entity.add(); } } diff --git a/core/src/mindustry/entities/Sized.java b/core/src/mindustry/entities/Sized.java index 7039f0b2a5..d33778e3aa 100644 --- a/core/src/mindustry/entities/Sized.java +++ b/core/src/mindustry/entities/Sized.java @@ -1,5 +1,7 @@ package mindustry.entities; -public interface Sized{ +import arc.math.geom.*; + +public interface Sized extends Position{ float hitSize(); } diff --git a/core/src/mindustry/entities/Units.java b/core/src/mindustry/entities/Units.java index cb7d59ea41..d0fd94a63c 100644 --- a/core/src/mindustry/entities/Units.java +++ b/core/src/mindustry/entities/Units.java @@ -4,6 +4,7 @@ import arc.*; import arc.func.*; import arc.math.geom.*; import arc.struct.*; +import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.game.*; @@ -20,6 +21,7 @@ public class Units{ private static Unit result; private static float cdist; private static boolean boolResult; + private static int intResult; @Remote(called = Loc.server) public static void unitCapDeath(Unit unit){ @@ -75,7 +77,7 @@ public class Units{ if((team == state.rules.waveTeam && !state.rules.pvp) || (state.isCampaign() && team == state.rules.waveTeam)){ return Integer.MAX_VALUE; } - return Math.max(0, state.rules.unitCapVariable ? state.rules.unitCap + indexer.getExtraUnits(team) : state.rules.unitCap); + return Math.max(0, state.rules.unitCapVariable ? state.rules.unitCap + team.data().unitCap : state.rules.unitCap); } /** @return whether this player can interact with a specific tile. if either of these are null, returns true.*/ @@ -138,23 +140,28 @@ public class Units{ return boolResult; } - /** Returns the neareset damaged tile. */ + /** Returns the nearest damaged tile. */ public static Building findDamagedTile(Team team, float x, float y){ return Geometry.findClosest(x, y, indexer.getDamaged(team)); } - /** Returns the neareset ally tile in a range. */ + /** Returns the nearest ally tile in a range. */ public static Building findAllyTile(Team team, float x, float y, float range, Boolf pred){ return indexer.findTile(team, x, y, range, pred); } - /** Returns the neareset enemy tile in a range. */ + /** Returns the nearest enemy tile in a range. */ public static Building findEnemyTile(Team team, float x, float y, float range, Boolf pred){ if(team == Team.derelict) return null; return indexer.findEnemyTile(team, x, y, range, pred); } + /** Iterates through all buildings in a range. */ + public static void nearbyBuildings(float x, float y, float range, Cons cons){ + indexer.allBuildings(x, y, range, cons); + } + /** Returns the closest target enemy. First, units are checked, then tile entities. */ public static Teamc closestTarget(Team team, float x, float y, float range){ return closestTarget(team, x, y, range, Unit::isValid); @@ -199,7 +206,7 @@ public class Units{ nearbyEnemies(team, x - range, y - range, range*2f, range*2f, e -> { if(e.dead() || !predicate.get(e) || e.team == Team.derelict) return; - float dst2 = e.dst2(x, y); + float dst2 = e.dst2(x, y) - (e.hitSize * e.hitSize); if(dst2 < range*range && (result == null || dst2 < cdist)){ result = e; cdist = dst2; @@ -302,13 +309,40 @@ public class Units{ return result; } + /** @return whether any units exist in this square (centered) */ + public static int count(float x, float y, float size, Boolf filter){ + return count(x - size/2f, y - size/2f, size, size, filter); + } + + /** @return whether any units exist in this rectangle */ + public static int count(float x, float y, float width, float height, Boolf filter){ + intResult = 0; + Groups.unit.intersect(x, y, width, height, v -> { + if(filter.get(v)){ + intResult ++; + } + }); + return intResult; + } + + /** @return whether any units exist in this rectangle */ + public static boolean any(float x, float y, float width, float height, Boolf filter){ + return count(x, y, width, height, filter) > 0; + } + /** Iterates over all units in a rectangle. */ - public static void nearby(Team team, float x, float y, float width, float height, Cons cons){ - team.data().tree().intersect(x, y, width, height, cons); + public static void nearby(@Nullable Team team, float x, float y, float width, float height, Cons cons){ + if(team != null){ + team.data().tree().intersect(x, y, width, height, cons); + }else{ + for(var other : state.teams.getActive()){ + other.tree().intersect(x, y, width, height, cons); + } + } } /** Iterates over all units in a circle around this position. */ - public static void nearby(Team team, float x, float y, float radius, Cons cons){ + public static void nearby(@Nullable Team team, float x, float y, float radius, Cons cons){ nearby(team, x - radius, y - radius, radius*2f, radius*2f, unit -> { if(unit.within(x, y, radius + unit.hitSize/2f)){ cons.get(unit); @@ -336,6 +370,15 @@ public class Units{ } } + /** Iterates over all units that are enemies of this team. */ + public static void nearbyEnemies(Team team, float x, float y, float radius, Cons cons){ + nearbyEnemies(team, x - radius, y - radius, radius * 2f, radius * 2f, u -> { + if(u.within(x, y, radius + u.hitSize/2f)){ + cons.get(u); + } + }); + } + /** Iterates over all units that are enemies of this team. */ public static void nearbyEnemies(Team team, Rect rect, Cons cons){ nearbyEnemies(team, rect.x, rect.y, rect.width, rect.height, cons); diff --git a/core/src/mindustry/entities/abilities/EnergyFieldAbility.java b/core/src/mindustry/entities/abilities/EnergyFieldAbility.java new file mode 100644 index 0000000000..587fff2bdc --- /dev/null +++ b/core/src/mindustry/entities/abilities/EnergyFieldAbility.java @@ -0,0 +1,145 @@ +package mindustry.entities.abilities; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.struct.*; +import arc.util.*; +import mindustry.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.type.*; + +public class EnergyFieldAbility extends Ability{ + private static final Seq all = new Seq<>(); + + public float damage = 1, repair = 20f, reload = 100, range = 60; + public Effect healEffect = Fx.heal, hitEffect = Fx.hitLaserBlast, damageEffect = Fx.chainLightning; + public StatusEffect status = StatusEffects.electrified; + public float statusDuration = 60f * 6f; + public float x, y; + public boolean hitBuildings = true; + public int maxTargets = 25; + public float healPercent = 3f; + + public float layer = Layer.bullet - 0.001f, blinkScl = 20f; + public float effectRadius = 5f, sectorRad = 0.14f, rotateSpeed = 0.5f; + public int sectors = 5; + public Color color = Pal.heal; + + protected float timer, curStroke; + protected boolean anyNearby = false; + + EnergyFieldAbility(){} + + public EnergyFieldAbility(float damage, float reload, float range){ + this.damage = damage; + this.reload = reload; + this.range = range; + } + + @Override + public String localized(){ + return Core.bundle.format("ability.energyfield", damage, range / Vars.tilesize, maxTargets); + } + + @Override + public void draw(Unit unit){ + super.draw(unit); + + Draw.z(layer); + Draw.color(color); + Tmp.v1.trns(unit.rotation - 90, x, y).add(unit.x, unit.y); + float rx = Tmp.v1.x, ry = Tmp.v1.y; + float orbRadius = effectRadius * (1f + Mathf.absin(blinkScl, 0.1f)); + + Fill.circle(rx, ry, orbRadius); + Draw.color(); + Fill.circle(rx, ry, orbRadius / 2f); + + Lines.stroke((0.7f + Mathf.absin(blinkScl, 0.7f)), color); + + for(int i = 0; i < sectors; i++){ + float rot = unit.rotation + i * 360f/sectors - Time.time * rotateSpeed; + Lines.swirl(rx, ry, orbRadius + 3f, sectorRad, rot); + } + + Lines.stroke(Lines.getStroke() * curStroke); + + if(curStroke > 0){ + for(int i = 0; i < sectors; i++){ + float rot = unit.rotation + i * 360f/sectors + Time.time * rotateSpeed; + Lines.swirl(rx, ry, range, sectorRad, rot); + } + } + + Drawf.light(rx, ry, range * 1.5f, color, curStroke * 0.8f); + + Draw.reset(); + } + + @Override + public void update(Unit unit){ + + curStroke = Mathf.lerpDelta(curStroke, anyNearby ? 1 : 0, 0.09f); + + if((timer += Time.delta) >= reload){ + + Tmp.v1.trns(unit.rotation - 90, x, y).add(unit.x, unit.y); + float rx = Tmp.v1.x, ry = Tmp.v1.y; + anyNearby = false; + + all.clear(); + + Units.nearby(null, rx, ry, range, other -> { + if(other != unit){ + all.add(other); + } + }); + + if(hitBuildings){ + Units.nearbyBuildings(rx, ry, range, all::add); + } + + all.sort(h -> h.dst2(rx, ry)); + int len = Math.min(all.size, maxTargets); + for(int i = 0; i < len; i++){ + Healthc other = all.get(i); + + //lightning gets absorbed by plastanium + var absorber = Damage.findAbsorber(unit.team, rx, ry, other.getX(), other.getY()); + if(absorber != null){ + other = absorber; + } + + if(((Teamc)other).team() == unit.team){ + if(other.damaged()){ + anyNearby = true; + other.heal(healPercent / 100f * other.maxHealth()); + healEffect.at(other); + damageEffect.at(rx, ry, 0f, color, other); + hitEffect.at(rx, ry, unit.angleTo(other), color); + + if(other instanceof Building b){ + Fx.healBlockFull.at(b.x, b.y, b.block.size, color); + } + } + }else{ + anyNearby = true; + other.damage(damage); + if(other instanceof Statusc s){ + s.apply(status, statusDuration); + } + hitEffect.at(other.x(), other.y(), unit.angleTo(other), color); + damageEffect.at(rx, ry, 0f, color, other); + hitEffect.at(rx, ry, unit.angleTo(other), color); + } + } + + timer = 0f; + } + } +} diff --git a/core/src/mindustry/entities/abilities/ShieldRegenFieldAbility.java b/core/src/mindustry/entities/abilities/ShieldRegenFieldAbility.java index 8af8d2390a..7eea17b8d1 100644 --- a/core/src/mindustry/entities/abilities/ShieldRegenFieldAbility.java +++ b/core/src/mindustry/entities/abilities/ShieldRegenFieldAbility.java @@ -33,13 +33,13 @@ public class ShieldRegenFieldAbility extends Ability{ if(other.shield < max){ other.shield = Math.max(other.shield + amount, max); other.shieldAlpha = 1f; //TODO may not be necessary - applyEffect.at(unit); + applyEffect.at(unit.x, unit.y, unit.team.color); applied = true; } }); if(applied){ - activeEffect.at(unit); + activeEffect.at(unit.x, unit.y, unit.team.color); } timer = 0f; diff --git a/core/src/mindustry/entities/abilities/StatusFieldAbility.java b/core/src/mindustry/entities/abilities/StatusFieldAbility.java index a1bf47d408..b41df1b851 100644 --- a/core/src/mindustry/entities/abilities/StatusFieldAbility.java +++ b/core/src/mindustry/entities/abilities/StatusFieldAbility.java @@ -1,5 +1,6 @@ package mindustry.entities.abilities; +import arc.*; import arc.util.*; import mindustry.content.*; import mindustry.entities.*; @@ -23,6 +24,11 @@ public class StatusFieldAbility extends Ability{ this.effect = effect; } + @Override + public String localized(){ + return Core.bundle.format("ability.statusfield", effect.emoji()); + } + @Override public void update(Unit unit){ timer += Time.delta; diff --git a/core/src/mindustry/entities/abilities/UnitSpawnAbility.java b/core/src/mindustry/entities/abilities/UnitSpawnAbility.java index ac4218946b..92d26e59cc 100644 --- a/core/src/mindustry/entities/abilities/UnitSpawnAbility.java +++ b/core/src/mindustry/entities/abilities/UnitSpawnAbility.java @@ -54,7 +54,7 @@ public class UnitSpawnAbility extends Ability{ if(Units.canCreate(unit.team, this.unit)){ Draw.draw(Draw.z(), () -> { float x = unit.x + Angles.trnsx(unit.rotation, spawnY, spawnX), y = unit.y + Angles.trnsy(unit.rotation, spawnY, spawnX); - Drawf.construct(x, y, this.unit.icon(Cicon.full), unit.rotation - 90, timer / spawnTime, 1f, timer); + Drawf.construct(x, y, this.unit.fullIcon, unit.rotation - 90, timer / spawnTime, 1f, timer); }); } } diff --git a/core/src/mindustry/entities/bullet/ArtilleryBulletType.java b/core/src/mindustry/entities/bullet/ArtilleryBulletType.java index a35c670316..1b9c76a7f4 100644 --- a/core/src/mindustry/entities/bullet/ArtilleryBulletType.java +++ b/core/src/mindustry/entities/bullet/ArtilleryBulletType.java @@ -18,6 +18,24 @@ public class ArtilleryBulletType extends BasicBulletType{ hitSound = Sounds.explosion; shootEffect = Fx.shootBig; trailEffect = Fx.artilleryTrail; + + //default settings: + shrinkX = 0.15f; + shrinkY = 0.63f; + + //for trail: + + /* + trailLength = 27; + trailWidth = 3.5f; + trailEffect = Fx.none; + trailColor = Pal.bulletYellowBack; + + trailInterp = Interp.slope; + + shrinkX = 0.8f; + shrinkY = 0.3f; + */ } public ArtilleryBulletType(float speed, float damage){ @@ -39,15 +57,13 @@ public class ArtilleryBulletType extends BasicBulletType{ @Override public void draw(Bullet b){ - float baseScale = 0.7f; - float scale = (baseScale + b.fslope() * (1f - baseScale)); - - float height = this.height * ((1f - shrinkY) + shrinkY * b.fout()); + drawTrail(b); + float xscale = (1f - shrinkX + b.fslope() * (shrinkX)), yscale = (1f - shrinkY + b.fslope() * (shrinkY)), rot = b.rotation(); Draw.color(backColor); - Draw.rect(backRegion, b.x, b.y, width * scale, height * scale, b.rotation() - 90); + Draw.rect(backRegion, b.x, b.y, width * xscale, height * yscale, rot - 90); Draw.color(frontColor); - Draw.rect(frontRegion, b.x, b.y, width * scale, height * scale, b.rotation() - 90); + Draw.rect(frontRegion, b.x, b.y, width * xscale, height * yscale, rot - 90); Draw.color(); } } diff --git a/core/src/mindustry/entities/bullet/BasicBulletType.java b/core/src/mindustry/entities/bullet/BasicBulletType.java index ac33c28868..84cbca0e0a 100644 --- a/core/src/mindustry/entities/bullet/BasicBulletType.java +++ b/core/src/mindustry/entities/bullet/BasicBulletType.java @@ -42,6 +42,7 @@ public class BasicBulletType extends BulletType{ @Override public void draw(Bullet b){ + super.draw(b); float height = this.height * ((1f - shrinkY) + shrinkY * b.fout()); float width = this.width * ((1f - shrinkX) + shrinkX * b.fout()); float offset = -90 + (spin != 0 ? Mathf.randomSeed(b.id, 360f) + b.time * spin : 0f); diff --git a/core/src/mindustry/entities/bullet/BulletType.java b/core/src/mindustry/entities/bullet/BulletType.java index 7c3a4414af..40a09a37f3 100644 --- a/core/src/mindustry/entities/bullet/BulletType.java +++ b/core/src/mindustry/entities/bullet/BulletType.java @@ -1,39 +1,60 @@ package mindustry.entities.bullet; +import arc.*; import arc.audio.*; import arc.graphics.*; +import arc.graphics.g2d.*; import arc.math.*; import arc.util.*; +import mindustry.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.ctype.*; import mindustry.entities.*; +import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.*; +import mindustry.world.blocks.defense.Wall.*; import static mindustry.Vars.*; -public abstract class BulletType extends Content{ +public class BulletType extends Content implements Cloneable{ + /** Lifetime in ticks. */ public float lifetime = 40f; - public float speed; - public float damage; + /** Speed in units/tick. */ + public float speed = 1f; + /** Direct damage dealt on hit. */ + public float damage = 1f; + /** Hitbox size. */ public float hitSize = 4; + /** Clipping hitbox. */ public float drawSize = 40f; + /** Drag as fraction of velocity. */ public float drag = 0f; - public boolean pierce, pierceBuilding; + /** Whether to pierce units. */ + public boolean pierce; + /** Whether to pierce buildings. */ + public boolean pierceBuilding; + /** Maximum # of pierced objects. */ public int pierceCap = -1; - public Effect hitEffect, despawnEffect; - + /** Z layer to drawn on. */ + public float layer = Layer.bullet; + /** Effect shown on direct hit. */ + public Effect hitEffect = Fx.hitBulletSmall; + /** Effect shown when bullet despawns. */ + public Effect despawnEffect = Fx.hitBulletSmall; /** Effect created when shooting. */ public Effect shootEffect = Fx.shootSmall; /** Extra smoke effect created when shooting. */ public Effect smokeEffect = Fx.shootSmallSmoke; /** Sound made when hitting something or getting removed.*/ public Sound hitSound = Sounds.none; + /** Sound made when hitting something or getting removed.*/ + public Sound despawnSound = Sounds.none; /** Pitch of the sound made when hitting something*/ public float hitSoundPitch = 1; /** Volume of the sound made when hitting something*/ @@ -80,15 +101,17 @@ public abstract class BulletType extends Content{ public boolean reflectable = true; /** Whether this projectile can be absorbed by shields. */ public boolean absorbable = true; - /** Whether to move the bullet back depending on delta to fix some delta-time realted issues. + /** Whether to move the bullet back depending on delta to fix some delta-time related issues. * Do not change unless you know what you're doing. */ public boolean backMove = true; /** Bullet range override. */ public float maxRange = -1f; /** % of block health healed **/ public float healPercent = 0f; - /** whether to make fire on impact */ + /** Whether to make fire on impact */ public boolean makeFire = false; + /** Whether to create hit effects on despawn. Forced to true if this bullet has any special effects like splash damage. */ + public boolean despawnHit = false; //additional effects @@ -101,8 +124,14 @@ public abstract class BulletType extends Content{ public Color trailColor = Pal.missileYellowBack; public float trailChance = -0.0001f; + public float trailInterval = 0f; public Effect trailEffect = Fx.missileTrail; public float trailParam = 2f; + public boolean trailRotation = false; + public Interp trailInterp = Interp.one; + /** Any value <= 0 disables the trail. */ + public int trailLength = -1; + public float trailWidth = 2f; /** Use a negative value to disable splash damage. */ public float splashDamageRadius = -1f; @@ -134,19 +163,27 @@ public abstract class BulletType extends Content{ public float puddleAmount = 5f; public Liquid puddleLiquid = Liquids.water; - public float lightRadius = 16f; + public float lightRadius = -1f; public float lightOpacity = 0.3f; public Color lightColor = Pal.powerLight; public BulletType(float speed, float damage){ this.speed = speed; this.damage = damage; - hitEffect = Fx.hitBulletSmall; - despawnEffect = Fx.hitBulletSmall; } public BulletType(){ - this(1f, 1f); + } + + public BulletType copy(){ + try{ + BulletType copy = (BulletType)clone(); + copy.id = (short)Vars.content.getBy(getContentType()).size; + Vars.content.handleContent(copy); + return copy; + }catch(Exception e){ + throw new RuntimeException("death to checked exceptions", e); + } } /** @return estimated damage per shot. this can be very inaccurate. */ @@ -181,13 +218,13 @@ public abstract class BulletType extends Content{ if(healPercent > 0f && build.team == b.team && !(build.block instanceof ConstructBlock)){ Fx.healBlockFull.at(build.x, build.y, build.block.size, Pal.heal); - build.heal(healPercent / 100f * build.maxHealth()); + build.heal(healPercent / 100f * build.maxHealth); }else if(build.team != b.team && direct){ hit(b); } } - public void hitEntity(Bullet b, Hitboxc entity, float initialHealth){ + public void hitEntity(Bullet b, Hitboxc entity, float health){ if(entity instanceof Healthc h){ h.damage(b.damage); } @@ -198,6 +235,11 @@ public abstract class BulletType extends Content{ unit.impulse(Tmp.v3); unit.apply(status, statusDuration); } + + //for achievements + if(b.owner instanceof WallBuild && player != null && b.team == player.team() && entity instanceof Unit unit && unit.dead){ + Events.fire(Trigger.phaseDeflectHit); + } } public void hit(Bullet b){ @@ -205,7 +247,6 @@ public abstract class BulletType extends Content{ } public void hit(Bullet b, float x, float y){ - b.hit = true; hitEffect.at(x, y, b.rotation(), hitColor); hitSound.at(x, y, hitSoundPitch, hitSoundVolume); @@ -226,7 +267,7 @@ public abstract class BulletType extends Content{ } } - if(Mathf.chance(incendChance)){ + if(incendChance > 0 && Mathf.chance(incendChance)){ Damage.createIncend(x, y, incendSpread, incendAmount); } @@ -245,9 +286,7 @@ public abstract class BulletType extends Content{ } if(makeFire){ - indexer.eachBlock(null, x, y, splashDamageRadius, other -> other.team != b.team, other -> { - Fires.create(other.tile); - }); + indexer.eachBlock(null, x, y, splashDamageRadius, other -> other.team != b.team, other -> Fires.create(other.tile)); } } @@ -256,21 +295,40 @@ public abstract class BulletType extends Content{ } } + /** Called when the bullet reaches the end of its lifetime of is destroyed by something external. */ public void despawned(Bullet b){ + if(despawnHit){ + hit(b); + } despawnEffect.at(b.x, b.y, b.rotation(), hitColor); - hitSound.at(b); + despawnSound.at(b); Effect.shake(despawnShake, despawnShake, b); + } - if(!b.hit && (fragBullet != null || splashDamageRadius > 0 || lightning > 0)){ - hit(b); + /** Called when the bullet is removed for any reason. */ + public void removed(Bullet b){ + if(trailLength > 0 && b.trail != null && b.trail.size() > 0){ + Fx.trailFade.at(b.x, b.y, trailWidth, trailColor, b.trail.copy()); } } public void draw(Bullet b){ + drawTrail(b); + } + + public void drawTrail(Bullet b){ + if(trailLength > 0 && b.trail != null){ + //draw below bullets? TODO + float z = Draw.z(); + Draw.z(z - 0.0001f); + b.trail.draw(trailColor, trailWidth); + Draw.z(z); + } } public void drawLight(Bullet b){ + if(lightOpacity <= 0f || lightRadius <= 0f) return; Drawf.light(b.team, b, lightRadius, lightColor, lightOpacity); } @@ -283,11 +341,37 @@ public abstract class BulletType extends Content{ if(instantDisappear){ b.time = lifetime; } + + if(fragBullet != null || splashDamageRadius > 0 || lightning > 0){ + despawnHit = true; + } + + if(lightRadius == -1){ + lightRadius = Math.max(18, hitSize * 5f); + } + drawSize = Math.max(drawSize, trailLength * speed * 2f); } public void update(Bullet b){ + if(!headless && trailLength > 0){ + if(b.trail == null){ + b.trail = new Trail(trailLength); + } + b.trail.length = trailLength; + b.trail.update(b.x, b.y, trailInterp.apply(b.fin())); + } + if(homingPower > 0.0001f && b.time >= homingDelay){ - Teamc target = Units.closestTarget(b.team, b.x, b.y, homingRange, e -> e.checkTarget(collidesAir, collidesGround), t -> collidesGround); + Teamc target; + //home in on allies if possible + if(healPercent > 0){ + target = Units.closestTarget(null, b.x, b.y, homingRange, + e -> e.checkTarget(collidesAir, collidesGround) && e.team != b.team, + t -> collidesGround && (t.team != b.team || t.damaged())); + }else{ + target = Units.closestTarget(b.team, b.x, b.y, homingRange, e -> e.checkTarget(collidesAir, collidesGround), t -> collidesGround); + } + if(target != null){ b.vel.setAngle(Angles.moveToward(b.rotation(), b.angleTo(target), homingPower * Time.delta * 50f)); } @@ -299,7 +383,13 @@ public abstract class BulletType extends Content{ if(trailChance > 0){ if(Mathf.chanceDelta(trailChance)){ - trailEffect.at(b.x, b.y, trailParam, trailColor); + trailEffect.at(b.x, b.y, trailRotation ? b.rotation() : trailParam, trailColor); + } + } + + if(trailInterval > 0f){ + if(b.timer(0, trailInterval)){ + trailEffect.at(b.x, b.y, trailRotation ? b.rotation() : trailParam, trailColor); } } } @@ -366,6 +456,10 @@ public abstract class BulletType extends Content{ bullet.drag = drag; bullet.hitSize = hitSize; bullet.damage = (damage < 0 ? this.damage : damage) * bullet.damageMultiplier(); + //reset trail + if(bullet.trail != null){ + bullet.trail.clear(); + } bullet.add(); if(keepVelocity && owner instanceof Velc v) bullet.vel.add(v.vel().x, v.vel().y); diff --git a/core/src/mindustry/entities/bullet/EmpBulletType.java b/core/src/mindustry/entities/bullet/EmpBulletType.java new file mode 100644 index 0000000000..7e281e3b08 --- /dev/null +++ b/core/src/mindustry/entities/bullet/EmpBulletType.java @@ -0,0 +1,70 @@ +package mindustry.entities.bullet; + +import mindustry.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.gen.*; + +public class EmpBulletType extends BasicBulletType{ + public float radius = 100f; + public float timeIncrease = 2.5f, timeDuration = 60f * 10f; + public float powerDamageScl = 2f, powerSclDecrease = 0.2f; + public Effect hitPowerEffect = Fx.hitEmpSpark, chainEffect = Fx.chainEmp, applyEffect = Fx.heal; + public boolean hitUnits = true; + public float unitDamageScl = 0.7f; + + @Override + public void hit(Bullet b, float x, float y){ + super.hit(b, x, y); + + if(!b.absorbed){ + Vars.indexer.allBuildings(x, y, radius, other -> { + if(other.team == b.team){ + if(other.block.hasPower && other.block.canOverdrive && other.timeScale < timeIncrease){ + if(timeIncrease >= other.timeScale){ + other.timeScale = Math.max(other.timeScale, timeIncrease); + } + other.timeScaleDuration = Math.max(other.timeScaleDuration, timeDuration); + chainEffect.at(x, y, 0, hitColor, other); + applyEffect.at(other, other.block.size * 7f); + } + + if(other.block.hasPower && other.damaged()){ + other.heal(healPercent / 100f * other.maxHealth()); + Fx.healBlockFull.at(other.x, other.y, other.block.size, hitColor); + applyEffect.at(other, other.block.size * 7f); + } + }else if(other.power != null){ + var absorber = Damage.findAbsorber(b.team, x, y, other.x, other.y); + if(absorber != null){ + other = absorber; + } + + if(other.power != null && other.power.graph.getLastPowerProduced() > 0f){ + other.timeScale = Math.min(other.timeScale, powerSclDecrease); + other.timeScaleDuration = timeDuration; + other.damage(damage * powerDamageScl); + hitPowerEffect.at(other.x, other.y, b.angleTo(other), hitColor); + chainEffect.at(x, y, 0, hitColor, other); + } + } + }); + + if(hitUnits){ + Units.nearbyEnemies(b.team, x, y, radius, other -> { + if(other.team != b.team){ + var absorber = Damage.findAbsorber(b.team, x, y, other.x, other.y); + if(absorber != null){ + return; + } + + hitPowerEffect.at(other.x, other.y, b.angleTo(other), hitColor); + chainEffect.at(x, y, 0, hitColor, other); + other.damage(damage * unitDamageScl); + other.apply(status, statusDuration); + } + }); + } + } + } +} diff --git a/core/src/mindustry/entities/bullet/FlakBulletType.java b/core/src/mindustry/entities/bullet/FlakBulletType.java index ad5f8be7fa..a1ae9c2ee8 100644 --- a/core/src/mindustry/entities/bullet/FlakBulletType.java +++ b/core/src/mindustry/entities/bullet/FlakBulletType.java @@ -6,7 +6,7 @@ import mindustry.entities.*; import mindustry.gen.*; public class FlakBulletType extends BasicBulletType{ - public float explodeRange = 30f; + public float explodeRange = 30f, explodeDelay = 5f; public FlakBulletType(float speed, float damage){ super(speed, damage, "shell"); @@ -25,17 +25,21 @@ public class FlakBulletType extends BasicBulletType{ @Override public void update(Bullet b){ super.update(b); - if(b.data() instanceof Integer) return; + //don't check for targets if primed to explode + if(b.fdata < 0f) return; if(b.timer(2, 6)){ Units.nearbyEnemies(b.team, Tmp.r1.setSize(explodeRange * 2f).setCenter(b.x, b.y), unit -> { - if(b.data() instanceof Float || !unit.checkTarget(collidesAir, collidesGround)) return; + //fadata < 0 means it's primed to explode + if(b.fdata < 0f || !unit.checkTarget(collidesAir, collidesGround)) return; - if(unit.dst(b) < explodeRange){ - b.data(0); - Time.run(5f, () -> { - if(b.data() instanceof Integer){ - b.time(b.lifetime()); + if(unit.within(b, explodeRange)){ + //mark as primed + b.fdata = -1f; + Time.run(explodeDelay, () -> { + //explode + if(b.fdata < 0){ + b.time = b.lifetime; } }); } diff --git a/core/src/mindustry/entities/bullet/LaserBoltBulletType.java b/core/src/mindustry/entities/bullet/LaserBoltBulletType.java index cd5d89d5da..beff118b7a 100644 --- a/core/src/mindustry/entities/bullet/LaserBoltBulletType.java +++ b/core/src/mindustry/entities/bullet/LaserBoltBulletType.java @@ -3,6 +3,7 @@ package mindustry.entities.bullet; import arc.graphics.g2d.*; import mindustry.gen.*; import mindustry.content.*; +import mindustry.graphics.*; public class LaserBoltBulletType extends BasicBulletType{ public float width = 2f, height = 7f; @@ -15,6 +16,8 @@ public class LaserBoltBulletType extends BasicBulletType{ despawnEffect = Fx.hitLaser; hittable = false; reflectable = false; + lightColor = Pal.heal; + lightOpacity = 0.6f; } public LaserBoltBulletType(){ @@ -23,6 +26,7 @@ public class LaserBoltBulletType extends BasicBulletType{ @Override public void draw(Bullet b){ + super.draw(b); Draw.color(backColor); Lines.stroke(width); Lines.lineAngleCenter(b.x, b.y, b.rotation(), height); diff --git a/core/src/mindustry/entities/bullet/LiquidBulletType.java b/core/src/mindustry/entities/bullet/LiquidBulletType.java index e99b7afefc..24c91fd209 100644 --- a/core/src/mindustry/entities/bullet/LiquidBulletType.java +++ b/core/src/mindustry/entities/bullet/LiquidBulletType.java @@ -63,9 +63,10 @@ public class LiquidBulletType extends BulletType{ @Override public void draw(Bullet b){ + super.draw(b); Draw.color(liquid.color, Color.white, b.fout() / 100f); - Fill.circle(b.x, b.y, orbSize); + Draw.reset(); } @Override diff --git a/core/src/mindustry/entities/bullet/RailBulletType.java b/core/src/mindustry/entities/bullet/RailBulletType.java index bb833669ac..d141c69998 100644 --- a/core/src/mindustry/entities/bullet/RailBulletType.java +++ b/core/src/mindustry/entities/bullet/RailBulletType.java @@ -69,9 +69,9 @@ public class RailBulletType extends BulletType{ } @Override - public void hitEntity(Bullet b, Hitboxc entity, float initialHealth){ - handle(b, entity, initialHealth); - super.hitEntity(b, entity, initialHealth); + public void hitEntity(Bullet b, Hitboxc entity, float health){ + handle(b, entity, health); + super.hitEntity(b, entity, health); } @Override diff --git a/core/src/mindustry/entities/bullet/SapBulletType.java b/core/src/mindustry/entities/bullet/SapBulletType.java index 969e17dcb0..ee3cd6699c 100644 --- a/core/src/mindustry/entities/bullet/SapBulletType.java +++ b/core/src/mindustry/entities/bullet/SapBulletType.java @@ -25,6 +25,8 @@ public class SapBulletType extends BulletType{ hittable = false; hitEffect = Fx.hitLiquid; status = StatusEffects.sapped; + lightColor = Pal.sap; + lightOpacity = 0.6f; statusDuration = 60f * 3f; impact = true; } @@ -40,7 +42,7 @@ public class SapBulletType extends BulletType{ Draw.reset(); - Drawf.light(b.team, b.x, b.y, Tmp.v1.x, Tmp.v1.y, 15f * b.fout(), lightColor, 0.6f); + Drawf.light(b.team, b.x, b.y, Tmp.v1.x, Tmp.v1.y, 15f * b.fout(), lightColor, lightOpacity); } } diff --git a/core/src/mindustry/entities/bullet/ShrapnelBulletType.java b/core/src/mindustry/entities/bullet/ShrapnelBulletType.java index 387c7164ff..ccc5532e7d 100644 --- a/core/src/mindustry/entities/bullet/ShrapnelBulletType.java +++ b/core/src/mindustry/entities/bullet/ShrapnelBulletType.java @@ -29,6 +29,7 @@ public class ShrapnelBulletType extends BulletType{ pierce = true; hittable = false; absorbable = false; + lightOpacity = 0.6f; } @Override @@ -52,11 +53,11 @@ public class ShrapnelBulletType extends BulletType{ @Override public void draw(Bullet b){ - float realLength = b.fdata; + float realLength = b.fdata, rot = b.rotation(); Draw.color(fromColor, toColor, b.fin()); for(int i = 0; i < (int)(serrations * realLength / length); i++){ - Tmp.v1.trns(b.rotation(), i * serrationSpacing); + Tmp.v1.trns(rot, i * serrationSpacing); float sl = Mathf.clamp(b.fout() - serrationFadeOffset) * (serrationSpaceOffset - i * serrationLenScl); Drawf.tri(b.x + Tmp.v1.x, b.y + Tmp.v1.y, serrationWidth, sl, b.rotation() + 90); Drawf.tri(b.x + Tmp.v1.x, b.y + Tmp.v1.y, serrationWidth, sl, b.rotation() - 90); @@ -64,5 +65,7 @@ public class ShrapnelBulletType extends BulletType{ Drawf.tri(b.x, b.y, width * b.fout(), (realLength + 50), b.rotation()); Drawf.tri(b.x, b.y, width * b.fout(), 10f, b.rotation() + 180f); Draw.reset(); + + Drawf.light(b.team, b.x, b.y, b.x + Angles.trnsx(rot, realLength), b.y + Angles.trnsy(rot, realLength), width * 2.5f * b.fout(), toColor, lightOpacity); } } diff --git a/core/src/mindustry/entities/comp/BlockUnitComp.java b/core/src/mindustry/entities/comp/BlockUnitComp.java index d0ec5b9a32..a0b1fdebcd 100644 --- a/core/src/mindustry/entities/comp/BlockUnitComp.java +++ b/core/src/mindustry/entities/comp/BlockUnitComp.java @@ -4,7 +4,6 @@ import arc.graphics.g2d.*; import mindustry.annotations.Annotations.*; import mindustry.game.*; import mindustry.gen.*; -import mindustry.ui.*; import static mindustry.Vars.*; @@ -34,7 +33,7 @@ abstract class BlockUnitComp implements Unitc{ @Replace @Override public TextureRegion icon(){ - return tile.block.icon(Cicon.full); + return tile.block.fullIcon; } @Override diff --git a/core/src/mindustry/entities/comp/BoundedComp.java b/core/src/mindustry/entities/comp/BoundedComp.java index c02d34a760..9976ed31c8 100644 --- a/core/src/mindustry/entities/comp/BoundedComp.java +++ b/core/src/mindustry/entities/comp/BoundedComp.java @@ -9,7 +9,7 @@ import static mindustry.Vars.*; @Component abstract class BoundedComp implements Velc, Posc, Healthc, Flyingc{ - static final float warpDst = 180f; + static final float warpDst = 40f; @Import float x, y; @Import Vec2 vel; @@ -17,11 +17,16 @@ abstract class BoundedComp implements Velc, Posc, Healthc, Flyingc{ @Override public void update(){ if(!net.client() || isLocal()){ + + float dx = 0f, dy = 0f; + //repel unit out of bounds - if(x < 0) vel.x += (-x/warpDst); - if(y < 0) vel.y += (-y/warpDst); - if(x > world.unitWidth()) vel.x -= (x - world.unitWidth())/warpDst; - if(y > world.unitHeight()) vel.y -= (y - world.unitHeight())/warpDst; + if(x < 0) dx += (-x/warpDst); + if(y < 0) dy += (-y/warpDst); + if(x > world.unitWidth()) dx -= (x - world.unitWidth())/warpDst; + if(y > world.unitHeight()) dy -= (y - world.unitHeight())/warpDst; + + velAddNet(dx, dy); } //clamp position if not flying diff --git a/core/src/mindustry/entities/comp/BuilderComp.java b/core/src/mindustry/entities/comp/BuilderComp.java index c9984f8ea4..9b456e5602 100644 --- a/core/src/mindustry/entities/comp/BuilderComp.java +++ b/core/src/mindustry/entities/comp/BuilderComp.java @@ -20,6 +20,7 @@ import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.*; import mindustry.world.blocks.ConstructBlock.*; +import mindustry.world.blocks.storage.CoreBlock.*; import java.util.*; @@ -69,7 +70,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{ } } - Building core = core(); + var core = core(); //nothing to build. if(buildPlan() == null) return; @@ -109,7 +110,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{ plans.removeFirst(); return; } - }else if((tile.team() != team && tile.team() != Team.derelict) || (!current.breaking && (cb.cblock != current.block || cb.tile != current.tile()))){ + }else if((tile.team() != team && tile.team() != Team.derelict) || (!current.breaking && (cb.current != current.block || cb.tile != current.tile()))){ plans.removeFirst(); return; } diff --git a/core/src/mindustry/entities/comp/BuildingComp.java b/core/src/mindustry/entities/comp/BuildingComp.java index 62cd2b9b32..34cb209fc6 100644 --- a/core/src/mindustry/entities/comp/BuildingComp.java +++ b/core/src/mindustry/entities/comp/BuildingComp.java @@ -48,8 +48,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, //region vars and initialization static final float timeToSleep = 60f * 1, timeToUncontrol = 60f * 6; static final ObjectSet tmpTiles = new ObjectSet<>(); - static final Seq tempTileEnts = new Seq<>(); - static final Seq tempTiles = new Seq<>(); + static final Seq tempBuilds = new Seq<>(); static int sleepingEntities = 0; @Import float x, y, health, maxHealth; @@ -57,7 +56,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, transient Tile tile; transient Block block; - transient Seq proximity = new Seq<>(8); + transient Seq proximity = new Seq<>(6); transient boolean updateFlow; transient byte cdump; transient int rotation; @@ -71,6 +70,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, ConsumeModule cons; private transient float timeScale = 1f, timeScaleDuration; + private transient float dumpAccum; private transient @Nullable SoundLoop sound; @@ -213,8 +213,8 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, if(self() instanceof ConstructBuild entity){ //update block to reflect the fact that something was being constructed - if(entity.cblock != null && entity.cblock.synthetic() && entity.wasConstructing){ - block = entity.cblock; + if(entity.current != null && entity.current.synthetic() && entity.wasConstructing){ + block = entity.current; overrideConfig = entity.lastConfig; }else{ //otherwise this was a deconstruction that was interrupted, don't want to rebuild that @@ -401,6 +401,29 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, //endregion //region handler methods + /** @return whether the player can select (but not actually control) this building. */ + public boolean canControlSelect(Player player){ + return false; + } + + /** Called when a player control-selects this building - not called for ControlBlock subclasses. */ + public void onControlSelect(Player player){ + + } + + public void acceptPlayerPayload(Player player, Cons grabber){ + Fx.spawn.at(player); + var unit = player.unit(); + player.clearUnit(); + //player.deathTimer = Player.deathDelay + 1f; //for instant respawn + unit.remove(); + grabber.get(new UnitPayload(unit)); + Fx.unitDrop.at(unit); + if(Vars.net.client()){ + Vars.netClient.clearRemovedEntity(unit.id); + } + } + public boolean canUnload(){ return block.unloadable; } @@ -467,10 +490,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, } - public void onProximityUpdate(){ - noSleep(); - } - public boolean acceptPayload(Building source, Payload payload){ return false; } @@ -688,13 +707,27 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, if(Vars.state.rules.sector != null && team == state.rules.defaultTeam) Vars.state.rules.sector.info.handleProduction(item, amount); } - /** Try dumping any item near the */ + /** Dumps any item with an accumulator. May dump multiple times per frame. Use with care. */ + public void dumpAccumulate(){ + dumpAccumulate(null); + } + + /** Dumps any item with an accumulator. May dump multiple times per frame. Use with care. */ + public void dumpAccumulate(Item item){ + dumpAccum += delta(); + while(dumpAccum >= 1f){ + dump(item); + dumpAccum -=1f; + } + } + + /** Try dumping any item near the building. */ public boolean dump(){ return dump(null); } /** - * Try dumping a specific item near the + * Try dumping a specific item near the building. * @param todump Item to dump. Can be null to dump anything. */ public boolean dump(Item todump){ @@ -753,20 +786,27 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, return false; } + /** Called shortly before this building is removed. */ public void onProximityRemoved(){ if(power != null){ powerGraphRemoved(); } } - /** in overrides, this does the exact same thing as onProximityUpdate, use that instead */ + /** Called after this building is created in the world. May be called multiple times, or when adjacent buildings change. */ public void onProximityAdded(){ - if(block.hasPower) updatePowerGraph(); + if(power != null){ + updatePowerGraph(); + } + } + + /** Called when anything adjacent to this building is placed/removed, including itself. */ + public void onProximityUpdate(){ + noSleep(); } public void updatePowerGraph(){ - - for(Building other : getPowerConnections(tempTileEnts)){ + for(Building other : getPowerConnections(tempBuilds)){ if(other.power != null){ other.power.graph.addGraph(power.graph); } @@ -970,7 +1010,12 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, } - /** Called when the block is destroyed. */ + /** Called *after* the tile has been removed. */ + public void afterDestroyed(){ + + } + + /** Called when the block is destroyed. The tile is still intact at this stage. */ public void onDestroyed(){ float explosiveness = block.baseExplosiveness; float flammability = 0f; @@ -1022,7 +1067,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, } public TextureRegion getDisplayIcon(){ - return block.icon(Cicon.medium); + return block.uiIcon; } @Override @@ -1063,7 +1108,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, l.left(); for(Item item : content.items()){ if(items.hasFlowItem(item)){ - l.image(item.icon(Cicon.small)).padRight(3f); + l.image(item.uiIcon).padRight(3f); l.label(() -> items.getFlowRate(item) < 0 ? "..." : Strings.fixed(items.getFlowRate(item), 1) + ps).color(Color.lightGray); l.row(); } @@ -1090,7 +1135,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, Runnable rebuild = () -> { l.clearChildren(); l.left(); - l.image(() -> liquids.current().icon(Cicon.small)).padRight(3f); + l.image(() -> liquids.current().uiIcon).padRight(3f); l.label(() -> liquids.getFlowRate() < 0 ? "..." : Strings.fixed(liquids.getFlowRate(), 2) + ps).color(Color.lightGray); }; @@ -1189,11 +1234,23 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, /** Handle a bullet collision. * @return whether the bullet should be removed. */ public boolean collision(Bullet other){ - damage(other.damage() * other.type().buildingDamageMultiplier); + damage(other.team, other.damage() * other.type().buildingDamageMultiplier); return true; } + /** Used to handle damage from splash damage for certain types of blocks. */ + public void damage(@Nullable Team source, float damage){ + damage(damage); + } + + /** Changes this building's team in a safe manner. */ + public void changeTeam(Team next){ + indexer.removeIndex(tile); + this.team = next; + indexer.addIndex(tile); + } + public boolean canPickup(){ return true; } @@ -1366,11 +1423,9 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, @Override public void control(LAccess type, Object p1, double p2, double p3, double p4){ //don't execute configure instructions that copy logic building configures; this can cause extreme lag - if(type == LAccess.configure && block.logicConfigurable && !(p1 instanceof LogicBuild)){ + if(type == LAccess.config && block.logicConfigurable && !(p1 instanceof LogicBuild)){ //change config only if it's new - if(senseObject(LAccess.config) != p1){ - configured(null, p1); - } + configured(null, p1); } } @@ -1388,6 +1443,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, onDestroyed(); tile.remove(); remove(); + afterDestroyed(); } @Final @@ -1409,7 +1465,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, } } - if(team == Team.derelict){ + if(team == Team.derelict || !block.supportsEnv(state.rules.environment)){ enabled = false; } diff --git a/core/src/mindustry/entities/comp/BulletComp.java b/core/src/mindustry/entities/comp/BulletComp.java index a61ecc4d39..f9fe116008 100644 --- a/core/src/mindustry/entities/comp/BulletComp.java +++ b/core/src/mindustry/entities/comp/BulletComp.java @@ -1,6 +1,5 @@ package mindustry.entities.comp; -import arc.*; import arc.func.*; import arc.graphics.g2d.*; import arc.math.*; @@ -10,12 +9,10 @@ import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.core.*; import mindustry.entities.bullet.*; -import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.game.Teams.*; import mindustry.gen.*; import mindustry.graphics.*; -import mindustry.world.blocks.defense.Wall.*; import static mindustry.Vars.*; @@ -31,6 +28,7 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw BulletType type; float fdata; transient boolean absorbed, hit; + transient @Nullable Trail trail; @Override public void getCollisions(Cons consumer){ @@ -42,11 +40,6 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw } } - @Override - public void drawBullets(){ - type.draw(self()); - } - @Override public void add(){ type.init(self()); @@ -54,13 +47,17 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw @Override public void remove(){ - type.despawned(self()); + //'despawned' only counts when the bullet is killed externally or reaches the end of life + if(!hit){ + type.despawned(self()); + } + type.removed(self()); collided.clear(); } @Override public float damageMultiplier(){ - if(owner instanceof Unit) return ((Unit)owner).damageMultiplier() * state.rules.unitDamageMultiplier; + if(owner instanceof Unit u) return u.damageMultiplier() * state.rules.unitDamageMultiplier; if(owner instanceof Building) return state.rules.blockDamageMultiplier; return 1f; @@ -80,8 +77,8 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw @Replace @Override public boolean collides(Hitboxc other){ - return type.collides && (other instanceof Teamc && ((Teamc)other).team() != team) - && !(other instanceof Flyingc && !((Flyingc)other).checkTarget(type.collidesAir, type.collidesGround)) + return type.collides && (other instanceof Teamc t && t.team() != team) + && !(other instanceof Flyingc f && !f.checkTarget(type.collidesAir, type.collidesGround)) && !(type.pierce && collided.contains(other.id())); //prevent multiple collisions } @@ -89,24 +86,16 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw @Override public void collision(Hitboxc other, float x, float y){ type.hit(self(), x, y); - float health = 0f; - - if(other instanceof Healthc h){ - health = h.health(); - } //must be last. if(!type.pierce){ + hit = true; remove(); }else{ collided.add(other.id()); } - type.hitEntity(self(), other, health); - - if(owner instanceof WallBuild && player != null && team == player.team() && other instanceof Unit unit && unit.dead){ - Events.fire(Trigger.phaseDeflectHit); - } + type.hitEntity(self(), other, other instanceof Healthc h ? h.health() : 0f); } @Override @@ -130,6 +119,7 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw if(remove || type.collidesTeam){ if(!type.pierceBuilding){ + hit = true; remove(); }else{ collided.add(tile.id); @@ -146,13 +136,14 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw } if(type.pierceCap != -1 && collided.size >= type.pierceCap){ + hit = true; remove(); } } @Override public void draw(){ - Draw.z(Layer.bullet); + Draw.z(type.layer); type.draw(self()); type.drawLight(self()); diff --git a/core/src/mindustry/entities/comp/CommanderComp.java b/core/src/mindustry/entities/comp/CommanderComp.java index a3f0a0c625..4e10af62e1 100644 --- a/core/src/mindustry/entities/comp/CommanderComp.java +++ b/core/src/mindustry/entities/comp/CommanderComp.java @@ -83,7 +83,7 @@ abstract class CommanderComp implements Entityc, Posc{ clearCommand(); units.shuffle(); - float spacing = hitSize * 0.8f; + float spacing = hitSize * 0.9f; minFormationSpeed = type.speed; controlling.addAll(units); diff --git a/core/src/mindustry/entities/comp/EntityComp.java b/core/src/mindustry/entities/comp/EntityComp.java index c2ba271182..d44e5481b4 100644 --- a/core/src/mindustry/entities/comp/EntityComp.java +++ b/core/src/mindustry/entities/comp/EntityComp.java @@ -40,6 +40,7 @@ abstract class EntityComp{ return false; } + /** Replaced with `this` after code generation. */ T self(){ return (T)this; } @@ -48,11 +49,6 @@ abstract class EntityComp{ return (T)this; } - T with(Cons cons){ - cons.get((T)this); - return (T)this; - } - @InternalImpl abstract int classId(); diff --git a/core/src/mindustry/entities/comp/FireComp.java b/core/src/mindustry/entities/comp/FireComp.java index 89710ebdf1..7439af97c9 100644 --- a/core/src/mindustry/entities/comp/FireComp.java +++ b/core/src/mindustry/entities/comp/FireComp.java @@ -1,5 +1,7 @@ package mindustry.entities.comp; +import arc.*; +import arc.graphics.g2d.*; import arc.math.*; import arc.math.geom.*; import arc.util.*; @@ -9,6 +11,7 @@ import mindustry.content.*; import mindustry.entities.*; import mindustry.game.*; import mindustry.gen.*; +import mindustry.graphics.*; import mindustry.world.*; import mindustry.world.meta.*; @@ -16,24 +19,30 @@ import static mindustry.Vars.*; @EntityDef(value = {Firec.class}, pooled = true) @Component(base = true) -abstract class FireComp implements Timedc, Posc, Firec, Syncc{ - private static final float spreadChance = 0.04f, fireballChance = 0.06f; +abstract class FireComp implements Timedc, Posc, Syncc, Drawc{ + public static final int frames = 40, duration = 90; + + private static final float spreadDelay = 22f, fireballDelay = 40f, + ticksPerFrame = (float)duration / frames, warmupDuration = 20f, damageDelay = 40f, tileDamage = 1.8f, unitDamage = 3f; + + public static final TextureRegion[] regions = new TextureRegion[frames]; @Import float time, lifetime, x, y; Tile tile; private transient Block block; - private transient float baseFlammability = -1, puddleFlammability; + private transient float + baseFlammability = -1, puddleFlammability, damageTimer = Mathf.random(40f), + spreadTimer = Mathf.random(spreadDelay), fireballTimer = Mathf.random(fireballDelay), + warmup = 0f, + animation = Mathf.random(frames); @Override public void update(){ - if(Mathf.chance(0.09 * Time.delta)){ - Fx.fire.at(x + Mathf.range(4f), y + Mathf.range(4f)); - } - if(Mathf.chance(0.05 * Time.delta)){ - Fx.fireSmoke.at(x + Mathf.range(4f), y + Mathf.range(4f)); - } + animation += Time.delta / ticksPerFrame; + warmup += Time.delta; + animation %= frames; if(!headless){ control.sound.loop(Sounds.fire, this, 0.07f); @@ -55,46 +64,71 @@ abstract class FireComp implements Timedc, Posc, Firec, Syncc{ Building entity = tile.build; boolean damage = entity != null; + if(baseFlammability < 0 || block != tile.block()){ + baseFlammability = tile.getFlammability(); + block = tile.block(); + } + float flammability = baseFlammability + puddleFlammability; if(!damage && flammability <= 0){ time += Time.delta * 8; } - if(baseFlammability < 0 || block != tile.block()){ - baseFlammability = tile.build == null ? 0 : tile.getFlammability(); - block = tile.block(); - } - if(damage){ lifetime += Mathf.clamp(flammability / 8f, 0f, 0.6f) * Time.delta; } - if(flammability > 1f && Mathf.chance(spreadChance * Time.delta * Mathf.clamp(flammability / 5f, 0.3f, 2f))){ + if(flammability > 1f && (spreadTimer += Time.delta * Mathf.clamp(flammability / 5f, 0.3f, 2f)) >= spreadDelay){ + spreadTimer = 0f; Point2 p = Geometry.d4[Mathf.random(3)]; Tile other = world.tile(tile.x + p.x, tile.y + p.y); Fires.create(other); - - if(Mathf.chance(fireballChance * Time.delta * Mathf.clamp(flammability / 10f))){ - Bullets.fireball.createNet(Team.derelict, x, y, Mathf.random(360f), -1f, 1, 1); - } } - if(Mathf.chance(0.025 * Time.delta)){ + if(flammability > 0 && (fireballTimer += Time.delta * Mathf.clamp(flammability / 10f, 0f, 0.5f)) >= fireballDelay){ + fireballTimer = 0f; + Bullets.fireball.createNet(Team.derelict, x, y, Mathf.random(360f), -1f, 1, 1); + } + + //apply damage to nearby units & building + if((damageTimer += Time.delta) >= damageDelay){ + damageTimer = 0f; Puddlec p = Puddles.get(tile); puddleFlammability = p != null ? p.getFlammability() / 3f : 0; if(damage){ - entity.damage(1.6f); + entity.damage(tileDamage); } - Damage.damageUnits(null, tile.worldx(), tile.worldy(), tilesize, 3f, + Damage.damageUnits(null, tile.worldx(), tile.worldy(), tilesize, unitDamage, unit -> !unit.isFlying() && !unit.isImmune(StatusEffects.burning), unit -> unit.apply(StatusEffects.burning, 60 * 5)); } } + @Override + public void draw(){ + if(regions[0] == null){ + for(int i = 0; i < frames; i++){ + regions[i] = Core.atlas.find("fire" + i); + } + } + + Draw.alpha(Mathf.clamp(warmup / warmupDuration)); + Draw.z(Layer.effect); + Draw.rect(regions[(int)animation], x, y); + Draw.reset(); + } + + @Replace + @Override + public float clipSize(){ + return 25; + } + @Override public void remove(){ + Fx.fireRemove.at(x, y, animation); Fires.remove(tile); } diff --git a/core/src/mindustry/entities/comp/LaunchCoreComp.java b/core/src/mindustry/entities/comp/LaunchCoreComp.java index 4f642c8493..e91e3749fc 100644 --- a/core/src/mindustry/entities/comp/LaunchCoreComp.java +++ b/core/src/mindustry/entities/comp/LaunchCoreComp.java @@ -43,7 +43,7 @@ abstract class LaunchCoreComp implements Drawc, Timedc{ Draw.z(Layer.weather - 1); - TextureRegion region = block.icon(Cicon.full); + TextureRegion region = block.fullIcon; float rw = region.width * Draw.scl * scale, rh = region.height * Draw.scl * scale; Draw.alpha(alpha); diff --git a/core/src/mindustry/entities/comp/PayloadComp.java b/core/src/mindustry/entities/comp/PayloadComp.java index 9afd26bb9a..0f02434a92 100644 --- a/core/src/mindustry/entities/comp/PayloadComp.java +++ b/core/src/mindustry/entities/comp/PayloadComp.java @@ -83,8 +83,8 @@ abstract class PayloadComp implements Posc, Rotc, Hitboxc, Unitc{ Tile on = tileOn(); //clear removed state of unit so it can be synced - if(Vars.net.client() && payload instanceof UnitPayload){ - Vars.netClient.clearRemovedEntity(((UnitPayload)payload).unit.id); + if(Vars.net.client() && payload instanceof UnitPayload u){ + Vars.netClient.clearRemovedEntity(u.unit.id); } //drop off payload on an acceptor if possible @@ -106,7 +106,7 @@ abstract class PayloadComp implements Posc, Rotc, Hitboxc, Unitc{ Unit u = payload.unit; //can't drop ground units - if(!u.canPass(tileX(), tileY())){ + if(!u.canPass(tileX(), tileY()) || Units.count(x, y, u.physicSize(), o -> o.isGrounded()) > 1){ return false; } @@ -159,7 +159,7 @@ abstract class PayloadComp implements Posc, Rotc, Hitboxc, Unitc{ } for(Payload p : payloads){ - table.image(p.icon(Cicon.small)).size(itemSize).padRight(pad); + table.image(p.icon()).size(itemSize).padRight(pad); } } } diff --git a/core/src/mindustry/entities/comp/PhysicsComp.java b/core/src/mindustry/entities/comp/PhysicsComp.java index d68e392dca..1924de8eb9 100644 --- a/core/src/mindustry/entities/comp/PhysicsComp.java +++ b/core/src/mindustry/entities/comp/PhysicsComp.java @@ -11,7 +11,7 @@ import mindustry.gen.*; * Has mass.*/ @Component abstract class PhysicsComp implements Velc, Hitboxc, Flyingc{ - @Import float hitSize; + @Import float hitSize, x, y; @Import Vec2 vel; transient PhysicRef physref; diff --git a/core/src/mindustry/entities/comp/PlayerComp.java b/core/src/mindustry/entities/comp/PlayerComp.java index 4f77c5b78c..0323906021 100644 --- a/core/src/mindustry/entities/comp/PlayerComp.java +++ b/core/src/mindustry/entities/comp/PlayerComp.java @@ -19,7 +19,6 @@ import mindustry.net.Administration.*; import mindustry.net.*; import mindustry.net.Packets.*; import mindustry.ui.*; -import mindustry.world.*; import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.CoreBlock.*; @@ -33,31 +32,31 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra @Import float x, y; @ReadOnly Unit unit = Nulls.unit; - transient private Unit lastReadUnit = Nulls.unit; transient @Nullable NetConnection con; - @ReadOnly Team team = Team.sharded; @SyncLocal boolean typing, shooting, boosting; - boolean admin; @SyncLocal float mouseX, mouseY; - String name = "noname"; + boolean admin; + String name = "frog"; Color color = new Color(); - //locale should not be synced. transient String locale = "en"; transient float deathTimer; transient String lastText = ""; transient float textFadeTime; + transient private Unit lastReadUnit = Nulls.unit; public boolean isBuilder(){ return unit.canBuild(); } - public @Nullable CoreBuild closestCore(){ + public @Nullable + CoreBuild closestCore(){ return state.teams.closestCore(x, y, team); } - public @Nullable CoreBuild core(){ + public @Nullable + CoreBuild core(){ return team.core(); } @@ -69,7 +68,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra public TextureRegion icon(){ //display default icon for dead players - if(dead()) return core() == null ? UnitTypes.alpha.icon(Cicon.full) : ((CoreBlock)core().block).unitType.icon(Cicon.full); + if(dead()) return core() == null ? UnitTypes.alpha.fullIcon : ((CoreBlock)core().block).unitType.fullIcon; return unit.icon(); } @@ -133,8 +132,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra //update some basic state to sync things if(unit.type.canBoost){ - Tile tile = unit.tileOn(); - unit.elevation = Mathf.approachDelta(unit.elevation, (tile != null && tile.solid()) || boosting ? 1f : 0f, unit.type.riseSpeed); + unit.elevation = Mathf.approachDelta(unit.elevation, unit.onSolid() || boosting || (unit.isFlying() && !unit.canLand()) ? 1f : 0f, unit.type.riseSpeed); } }else if((core = bestCore()) != null){ //have a small delay before death to prevent the camera from jumping around too quickly @@ -266,9 +264,9 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra layout.setText(font, text, Color.white, width, Align.bottom, true); - Draw.color(0f, 0f, 0f, 0.3f * (textFadeTime <= 0 || lastText == null ? 1f : visualFadeTime)); - Fill.rect(unit.x, unit.y + textHeight + layout.height - layout.height/2f, layout.width + 2, layout.height + 3); - font.draw(text, unit.x - width/2f, unit.y + textHeight + layout.height, width, Align.center, true); + Draw.color(0f, 0f, 0f, 0.3f * (textFadeTime <= 0 || lastText == null ? 1f : visualFadeTime)); + Fill.rect(unit.x, unit.y + textHeight + layout.height - layout.height / 2f, layout.width + 2, layout.height + 3); + font.draw(text, unit.x - width / 2f, unit.y + textHeight + layout.height, width, Align.center, true); } Draw.reset(); @@ -294,7 +292,7 @@ abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc, Dra sendMessage(text, from, NetClient.colorizeName(from.id(), from.name)); } - void sendMessage(String text, Player from, String fromName){ + void sendMessage(String text, Player from, String fromName){ if(isLocal()){ if(ui != null){ ui.chatfrag.addMessage(text, fromName); diff --git a/core/src/mindustry/entities/comp/PuddleComp.java b/core/src/mindustry/entities/comp/PuddleComp.java index cefebe8307..12f56fbaa4 100644 --- a/core/src/mindustry/entities/comp/PuddleComp.java +++ b/core/src/mindustry/entities/comp/PuddleComp.java @@ -21,10 +21,10 @@ import static mindustry.entities.Puddles.*; @Component(base = true) abstract class PuddleComp implements Posc, Puddlec, Drawc{ private static final int maxGeneration = 2; - private static final Color tmp = new Color(); private static final Rect rect = new Rect(), rect2 = new Rect(); private static int seeds; + @Import int id; @Import float x, y; transient float accepting, updateTime, lastRipple; @@ -92,13 +92,13 @@ abstract class PuddleComp implements Posc, Puddlec, Drawc{ public void draw(){ Draw.z(Layer.debris - 1); - seeds = id(); + seeds = id; boolean onLiquid = tile.floor().isLiquid; float f = Mathf.clamp(amount / (maxLiquid / 1.5f)); float smag = onLiquid ? 0.8f : 0f; float sscl = 25f; - Draw.color(tmp.set(liquid.color).shiftValue(-0.05f)); + Draw.color(Tmp.c1.set(liquid.color).shiftValue(-0.05f)); Fill.circle(x + Mathf.sin(Time.time + seeds * 532, sscl, smag), y + Mathf.sin(Time.time + seeds * 53, sscl, smag), f * 8f); Angles.randLenVectors(id(), 3, f * 6f, (ex, ey) -> { Fill.circle(x + ex + Mathf.sin(Time.time + seeds * 532, sscl, smag), diff --git a/core/src/mindustry/entities/comp/ShieldComp.java b/core/src/mindustry/entities/comp/ShieldComp.java index 5a489686ff..d1d3b50a87 100644 --- a/core/src/mindustry/entities/comp/ShieldComp.java +++ b/core/src/mindustry/entities/comp/ShieldComp.java @@ -3,6 +3,7 @@ package mindustry.entities.comp; import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; +import mindustry.game.*; import mindustry.gen.*; import static mindustry.Vars.*; @@ -11,10 +12,11 @@ import static mindustry.Vars.*; abstract class ShieldComp implements Healthc, Posc{ @Import float health, hitTime, x, y, healthMultiplier; @Import boolean dead; + @Import Team team; /** Absorbs health damage. */ float shield; - /** Substracts an amount from damage. */ + /** Subtracts an amount from damage. */ float armor; /** Shield opacity. */ transient float shieldAlpha = 0f; @@ -60,7 +62,7 @@ abstract class ShieldComp implements Healthc, Posc{ } if(hadShields && shield <= 0.0001f){ - Fx.unitShieldBreak.at(x, y, 0, this); + Fx.unitShieldBreak.at(x, y, 0, team.color, this); } } } diff --git a/core/src/mindustry/entities/comp/StatusComp.java b/core/src/mindustry/entities/comp/StatusComp.java index f029dfade1..70d1e38dcb 100644 --- a/core/src/mindustry/entities/comp/StatusComp.java +++ b/core/src/mindustry/entities/comp/StatusComp.java @@ -19,8 +19,9 @@ abstract class StatusComp implements Posc, Flyingc{ private Seq statuses = new Seq<>(); private transient Bits applied = new Bits(content.getBy(ContentType.status).size); - @ReadOnly transient float speedMultiplier = 1, damageMultiplier = 1, healthMultiplier = 1, reloadMultiplier = 1, buildSpeedMultiplier = 1, dragMultiplier = 1; - @ReadOnly transient boolean disarmed = false; + //these are considered read-only + transient float speedMultiplier = 1, damageMultiplier = 1, healthMultiplier = 1, reloadMultiplier = 1, buildSpeedMultiplier = 1, dragMultiplier = 1; + transient boolean disarmed = false; @Import UnitType type; @@ -46,15 +47,8 @@ abstract class StatusComp implements Posc, Flyingc{ if(entry.effect == effect){ entry.time = Math.max(entry.time, duration); return; - }else if(entry.effect.reactsWith(effect)){ //find opposite - StatusEntry.tmp.effect = entry.effect; - entry.effect.getTransition(self(), effect, entry.time, duration, StatusEntry.tmp); - entry.time = StatusEntry.tmp.time; - - if(StatusEntry.tmp.effect != entry.effect){ - entry.effect = StatusEntry.tmp.effect; - } - + }else if(entry.effect.applyTransition(self(), effect, entry, duration)){ //find reaction + //TODO effect may react with multiple other effects //stop looking when one is found return; } @@ -69,6 +63,11 @@ abstract class StatusComp implements Posc, Flyingc{ } } + float getDuration(StatusEffect effect){ + var entry = statuses.find(e -> e.effect == effect); + return entry == null ? 0 : entry.time; + } + void clearStatuses(){ statuses.clear(); } @@ -149,6 +148,10 @@ abstract class StatusComp implements Posc, Flyingc{ } } + public Bits statusBits(){ + return applied; + } + public void draw(){ for(StatusEntry e : statuses){ e.effect.draw(self()); diff --git a/core/src/mindustry/entities/comp/TeamComp.java b/core/src/mindustry/entities/comp/TeamComp.java index c58e97b7fd..b222229619 100644 --- a/core/src/mindustry/entities/comp/TeamComp.java +++ b/core/src/mindustry/entities/comp/TeamComp.java @@ -4,6 +4,7 @@ import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.game.*; import mindustry.gen.*; +import mindustry.world.blocks.storage.CoreBlock.*; import static mindustry.Vars.*; @@ -18,17 +19,17 @@ abstract class TeamComp implements Posc{ } @Nullable - public Building core(){ + public CoreBuild core(){ return team.core(); } @Nullable - public Building closestCore(){ + public CoreBuild closestCore(){ return state.teams.closestCore(x, y, team); } @Nullable - public Building closestEnemyCore(){ + public CoreBuild closestEnemyCore(){ return state.teams.closestEnemyCore(x, y, team); } } diff --git a/core/src/mindustry/entities/comp/UnitComp.java b/core/src/mindustry/entities/comp/UnitComp.java index 27c5512052..47a10d113c 100644 --- a/core/src/mindustry/entities/comp/UnitComp.java +++ b/core/src/mindustry/entities/comp/UnitComp.java @@ -46,8 +46,10 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I double flag; transient Seq abilities = new Seq<>(0); + transient float healTime; private transient float resupplyTime = Mathf.random(10f); private transient boolean wasPlayer; + private transient float lastHealth; public void moveAt(Vec2 vector){ moveAt(vector, type.accel); @@ -67,6 +69,16 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I lookAt(x, y); } + /** @return approx. square size of the physical hitbox for physics */ + public float physicSize(){ + return hitSize * 0.7f; + } + + /** @return whether there is solid, un-occupied ground under this unit. */ + public boolean canLand(){ + return !onSolid() && Units.count(x, y, physicSize(), f -> f != self() && f.isGrounded()) == 0; + } + public boolean inRange(Position other){ return within(other, type.range); } @@ -98,7 +110,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I return angleTo(buildPlan()); }else if(mineTile != null){ return angleTo(mineTile); - }else if(moving()){ + }else if(moving() && type.omniMovement){ return vel().angle(); } return rotation; @@ -114,7 +126,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I if(isBuilding()){ return state.rules.infiniteResources ? Float.MAX_VALUE : Math.max(type.clipSize, type.region.width) + buildingRange + tilesize*4f; } - return Math.max(type.region.width * 2f, type.clipSize); + return type.clipSize; } @Override @@ -146,7 +158,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I controller instanceof FormationAI ? ctrlFormation : 0; case commanded -> controller instanceof FormationAI && isValid() ? 1 : 0; - case payloadCount -> self() instanceof Payloadc pay ? pay.payloads().size : 0; + case payloadCount -> ((Object)this) instanceof Payloadc pay ? pay.payloads().size : 0; case size -> hitSize / tilesize; default -> Float.NaN; }; @@ -159,7 +171,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I case name -> controller instanceof Player p ? p.name : null; case firstItem -> stack().amount == 0 ? null : item(); case controller -> !isValid() ? null : controller instanceof LogicAI log ? log.controller : controller instanceof FormationAI form ? form.leader : this; - case payloadType -> self() instanceof Payloadc pay ? + case payloadType -> ((Object)this) instanceof Payloadc pay ? (pay.payloads().isEmpty() ? null : pay.payloads().peek() instanceof UnitPayload p1 ? p1.unit.type : pay.payloads().peek() instanceof BuildPayload p2 ? p2.block() : null) : null; @@ -313,6 +325,18 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I type.update(self()); + if(health > lastHealth && lastHealth > 0 && healTime <= -1f){ + healTime = 1f; + } + healTime -= Time.delta / 20f; + lastHealth = health; + + //check if environment is unsupported + if(!type.supportsEnv(state.rules.environment) && !dead){ + Call.unitCapDeath(self()); + team.data().updateCount(type, -1); + } + if(state.rules.unitAmmo && ammo < type.ammoCapacity - 0.0001f){ resupplyTime += Time.delta; @@ -336,7 +360,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I float relativeSize = state.rules.dropZoneRadius + hitSize/2f + 1f; for(Tile spawn : spawner.getSpawns()){ if(within(spawn.worldx(), spawn.worldy(), relativeSize)){ - vel().add(Tmp.v1.set(this).sub(spawn.worldx(), spawn.worldy()).setLength(0.1f + 1f - dst(spawn) / relativeSize).scl(0.45f * Time.delta)); + velAddNet(Tmp.v1.set(this).sub(spawn.worldx(), spawn.worldy()).setLength(0.1f + 1f - dst(spawn) / relativeSize).scl(0.45f * Time.delta)); } } } @@ -367,7 +391,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I elevation -= type.fallSpeed * Time.delta; if(isGrounded()){ - destroy(); + Call.unitDestroy(id); } } @@ -414,7 +438,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I /** @return a preview icon for this unit. */ public TextureRegion icon(){ - return type.icon(Cicon.full); + return type.fullIcon; } /** Actually destroys the unit, removing it and creating explosions. **/ diff --git a/core/src/mindustry/entities/comp/VelComp.java b/core/src/mindustry/entities/comp/VelComp.java index 4b0272b5f4..3e79b04e17 100644 --- a/core/src/mindustry/entities/comp/VelComp.java +++ b/core/src/mindustry/entities/comp/VelComp.java @@ -1,5 +1,6 @@ package mindustry.entities.comp; +import arc.math.*; import arc.math.geom.*; import arc.util.*; import mindustry.annotations.Annotations.*; @@ -20,7 +21,11 @@ abstract class VelComp implements Posc{ @MethodPriority(-1) @Override public void update(){ + float px = x, py = y; move(vel.x * Time.delta, vel.y * Time.delta); + if(Mathf.equal(px, x)) vel.x = 0; + if(Mathf.equal(py, y)) vel.y = 0; + vel.scl(Math.max(1f - drag * Time.delta, 0)); } @@ -45,6 +50,10 @@ abstract class VelComp implements Posc{ return !vel.isZero(0.01f); } + void move(Vec2 v){ + move(v.x, v.y); + } + void move(float cx, float cy){ SolidPred check = solidity(); @@ -55,4 +64,20 @@ abstract class VelComp implements Posc{ y += cy; } } + + void velAddNet(Vec2 v){ + vel.add(v); + if(isRemote()){ + x += v.x; + y += v.y; + } + } + + void velAddNet(float vx, float vy){ + vel.add(vx, vy); + if(isRemote()){ + x += vx; + y += vy; + } + } } diff --git a/core/src/mindustry/entities/comp/WeaponsComp.java b/core/src/mindustry/entities/comp/WeaponsComp.java index 10f2d59b2f..bc0b0c85c5 100644 --- a/core/src/mindustry/entities/comp/WeaponsComp.java +++ b/core/src/mindustry/entities/comp/WeaponsComp.java @@ -1,28 +1,18 @@ package mindustry.entities.comp; -import arc.math.*; import arc.math.geom.*; import arc.util.*; import mindustry.annotations.Annotations.*; -import mindustry.audio.*; -import mindustry.entities.*; -import mindustry.entities.bullet.*; import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.type.*; -import static mindustry.Vars.*; - @Component abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ - @Import float x, y, rotation, reloadMultiplier; + @Import float x, y; @Import boolean disarmed; - @Import Vec2 vel; @Import UnitType type; - /** temporary weapon sequence number */ - static int sequenceNum = 0; - /** weapon mount array, never null */ @SyncLocal WeaponMount[] mounts = {}; @ReadOnly transient boolean isRotate; @@ -43,7 +33,7 @@ abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ void setupWeapons(UnitType def){ mounts = new WeaponMount[def.weapons.size]; for(int i = 0; i < mounts.length; i++){ - mounts[i] = new WeaponMount(def.weapons.get(i)); + mounts[i] = def.weapons.get(i).mountType.get(def.weapons.get(i)); } } @@ -53,8 +43,10 @@ abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ void controlWeapons(boolean rotate, boolean shoot){ for(WeaponMount mount : mounts){ - mount.rotate = rotate; - mount.shoot = shoot; + if(mount.weapon.controllable){ + mount.rotate = rotate; + mount.shoot = shoot; + } } isRotate = rotate; isShooting = shoot; @@ -73,8 +65,10 @@ abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ y = Tmp.v1.y + this.y; for(WeaponMount mount : mounts){ - mount.aimX = x; - mount.aimY = y; + if(mount.weapon.controllable){ + mount.aimX = x; + mount.aimY = y; + } } aimX = x; @@ -88,7 +82,7 @@ abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ @Override public void remove(){ for(WeaponMount mount : mounts){ - if(mount.bullet != null){ + if(mount.bullet != null && mount.bullet.owner == self()){ mount.bullet.time = mount.bullet.lifetime - 10f; mount.bullet = null; } @@ -102,136 +96,8 @@ abstract class WeaponsComp implements Teamc, Posc, Rotc, Velc, Statusc{ /** Update shooting and rotation for this unit. */ @Override public void update(){ - boolean can = canShoot(); - for(WeaponMount mount : mounts){ - Weapon weapon = mount.weapon; - mount.reload = Math.max(mount.reload - Time.delta * reloadMultiplier, 0); - - float weaponRotation = this.rotation - 90 + (weapon.rotate ? mount.rotation : 0); - float mountX = this.x + Angles.trnsx(this.rotation - 90, weapon.x, weapon.y), - mountY = this.y + Angles.trnsy(this.rotation - 90, weapon.x, weapon.y); - float shootX = mountX + Angles.trnsx(weaponRotation, weapon.shootX, weapon.shootY), - shootY = mountY + Angles.trnsy(weaponRotation, weapon.shootX, weapon.shootY); - float shootAngle = weapon.rotate ? weaponRotation + 90 : Angles.angle(shootX, shootY, mount.aimX, mount.aimY) + (this.rotation - angleTo(mount.aimX, mount.aimY)); - - //update continuous state - if(weapon.continuous && mount.bullet != null){ - if(!mount.bullet.isAdded() || mount.bullet.time >= mount.bullet.lifetime || mount.bullet.type != weapon.bullet){ - mount.bullet = null; - }else{ - mount.bullet.rotation(weaponRotation + 90); - mount.bullet.set(shootX, shootY); - mount.reload = weapon.reload; - vel.add(Tmp.v1.trns(rotation + 180f, mount.bullet.type.recoil)); - if(weapon.shootSound != Sounds.none && !headless){ - if(mount.sound == null) mount.sound = new SoundLoop(weapon.shootSound, 1f); - mount.sound.update(x, y, true); - } - } - }else{ - //heat decreases when not firing - mount.heat = Math.max(mount.heat - Time.delta * reloadMultiplier / mount.weapon.cooldownTime, 0); - - if(mount.sound != null){ - mount.sound.update(x, y, false); - } - } - - //flip weapon shoot side for alternating weapons at half reload - if(weapon.otherSide != -1 && weapon.alternate && mount.side == weapon.flipSprite && - mount.reload + Time.delta * reloadMultiplier > weapon.reload/2f && mount.reload <= weapon.reload/2f){ - mounts[weapon.otherSide].side = !mounts[weapon.otherSide].side; - mount.side = !mount.side; - } - - //rotate if applicable - if(weapon.rotate && (mount.rotate || mount.shoot) && can){ - float axisX = this.x + Angles.trnsx(this.rotation - 90, weapon.x, weapon.y), - axisY = this.y + Angles.trnsy(this.rotation - 90, weapon.x, weapon.y); - - mount.targetRotation = Angles.angle(axisX, axisY, mount.aimX, mount.aimY) - this.rotation; - mount.rotation = Angles.moveToward(mount.rotation, mount.targetRotation, weapon.rotateSpeed * Time.delta); - }else if(!weapon.rotate){ - mount.rotation = 0; - mount.targetRotation = angleTo(mount.aimX, mount.aimY); - } - - //shoot if applicable - if(mount.shoot && //must be shooting - can && //must be able to shoot - (ammo > 0 || !state.rules.unitAmmo || team().rules().infiniteAmmo) && //check ammo - (!weapon.alternate || mount.side == weapon.flipSprite) && - //TODO checking for velocity this way isn't entirely correct - (vel.len() >= mount.weapon.minShootVelocity || (net.active() && !isLocal())) && //check velocity requirements - mount.reload <= 0.0001f && //reload has to be 0 - Angles.within(weapon.rotate ? mount.rotation : this.rotation, mount.targetRotation, mount.weapon.shootCone) //has to be within the cone - ){ - shoot(mount, shootX, shootY, mount.aimX, mount.aimY, mountX, mountY, shootAngle, Mathf.sign(weapon.x)); - - mount.reload = weapon.reload; - - ammo--; - if(ammo < 0) ammo = 0; - } + mount.weapon.update(self(), mount); } } - - private void shoot(WeaponMount mount, float x, float y, float aimX, float aimY, float mountX, float mountY, float rotation, int side){ - Weapon weapon = mount.weapon; - - float baseX = this.x, baseY = this.y; - boolean delay = weapon.firstShotDelay + weapon.shotDelay > 0f; - - (delay ? weapon.chargeSound : weapon.continuous ? Sounds.none : weapon.shootSound).at(x, y, Mathf.random(weapon.soundPitchMin, weapon.soundPitchMax)); - - BulletType ammo = weapon.bullet; - float lifeScl = ammo.scaleVelocity ? Mathf.clamp(Mathf.dst(x, y, aimX, aimY) / ammo.range()) : 1f; - - sequenceNum = 0; - if(delay){ - Angles.shotgun(weapon.shots, weapon.spacing, rotation, f -> { - Time.run(sequenceNum * weapon.shotDelay + weapon.firstShotDelay, () -> { - if(!isAdded()) return; - mount.bullet = bullet(weapon, x + this.x - baseX, y + this.y - baseY, f + Mathf.range(weapon.inaccuracy), lifeScl); - }); - sequenceNum++; - }); - }else{ - Angles.shotgun(weapon.shots, weapon.spacing, rotation, f -> mount.bullet = bullet(weapon, x, y, f + Mathf.range(weapon.inaccuracy), lifeScl)); - } - - boolean parentize = ammo.keepVelocity; - - if(delay){ - Time.run(weapon.firstShotDelay, () -> { - if(!isAdded()) return; - - vel.add(Tmp.v1.trns(rotation + 180f, ammo.recoil)); - Effect.shake(weapon.shake, weapon.shake, x, y); - mount.heat = 1f; - if(!weapon.continuous){ - weapon.shootSound.at(x, y, Mathf.random(weapon.soundPitchMin, weapon.soundPitchMax)); - } - }); - }else{ - vel.add(Tmp.v1.trns(rotation + 180f, ammo.recoil)); - Effect.shake(weapon.shake, weapon.shake, x, y); - mount.heat = 1f; - } - - weapon.ejectEffect.at(mountX, mountY, rotation * side); - ammo.shootEffect.at(x, y, rotation, parentize ? this : null); - ammo.smokeEffect.at(x, y, rotation, parentize ? this : null); - apply(weapon.shootStatus, weapon.shootStatusDuration); - } - - private Bullet bullet(Weapon weapon, float x, float y, float angle, float lifescl){ - float xr = Mathf.range(weapon.xRand); - - return weapon.bullet.create(this, team(), - x + Angles.trnsx(angle, 0, xr), - y + Angles.trnsy(angle, 0, xr), - angle, (1f - weapon.velocityRnd) + Mathf.random(weapon.velocityRnd), lifescl); - } } diff --git a/core/src/mindustry/entities/effect/ExplosionEffect.java b/core/src/mindustry/entities/effect/ExplosionEffect.java new file mode 100644 index 0000000000..352e676f9c --- /dev/null +++ b/core/src/mindustry/entities/effect/ExplosionEffect.java @@ -0,0 +1,47 @@ +package mindustry.entities.effect; + +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import mindustry.entities.*; +import mindustry.graphics.*; + +import static arc.graphics.g2d.Draw.*; +import static arc.graphics.g2d.Lines.*; +import static arc.math.Angles.*; + +public class ExplosionEffect extends Effect{ + public Color waveColor = Pal.missileYellow, smokeColor = Color.gray, sparkColor = Pal.missileYellowBack; + public float waveLife = 6f, waveStroke = 3f, waveRad = 15f, waveRadBase = 2f, sparkStroke = 1f, sparkRad = 23f, sparkLen = 3f, smokeSize = 4f, smokeSizeBase = 0.5f, smokeRad = 23f; + public int smokes = 5, sparks = 4; + + public ExplosionEffect(){ + clip = 100f; + lifetime = 22; + + renderer = e -> { + color(waveColor); + + e.scaled(waveLife, i -> { + stroke(waveStroke * i.fout()); + Lines.circle(e.x, e.y, waveRadBase + i.fin() * waveRad); + }); + + color(smokeColor); + + if(smokeSize > 0){ + randLenVectors(e.id, smokes, 2f + smokeRad * e.finpow(), (x, y) -> { + Fill.circle(e.x + x, e.y + y, e.fout() * smokeSize + smokeSizeBase); + }); + } + + color(sparkColor); + stroke(e.fout() * sparkStroke); + + randLenVectors(e.id + 1, sparks, 1f + sparkRad * e.finpow(), (x, y) -> { + lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), 1f + e.fout() * sparkLen); + Drawf.light(e.x + x, e.y + y, e.fout() * sparkLen * 4f, sparkColor, 0.7f); + }); + }; + } +} diff --git a/core/src/mindustry/entities/effect/ParticleEffect.java b/core/src/mindustry/entities/effect/ParticleEffect.java index 281410ada3..a6e5df24a8 100644 --- a/core/src/mindustry/entities/effect/ParticleEffect.java +++ b/core/src/mindustry/entities/effect/ParticleEffect.java @@ -6,17 +6,28 @@ import arc.graphics.g2d.*; import arc.math.*; import arc.util.*; import mindustry.entities.*; +import mindustry.graphics.*; /** The most essential effect class. Can create particles in various shapes. */ public class ParticleEffect extends Effect{ public Color colorFrom = Color.white.cpy(), colorTo = Color.white.cpy(); public int particles = 6; public float cone = 180f, length = 20f, baseLength = 0f; + /** Particle size/length/radius interpolation. */ public Interp interp = Interp.linear; + public float offsetX, offsetY; + public float lightScl = 2f, lightOpacity = 0.6f; + public @Nullable Color lightColor; //region only + + /** Spin in degrees per tick. */ + public float spin = 0f; + /** Controls the initial and final sprite sizes. */ public float sizeFrom = 2f, sizeTo = 0f; + /** Rotation offset. */ public float offset = 0; + /** Sprite to draw. */ public String region = "circle"; //line only @@ -37,19 +48,23 @@ public class ParticleEffect extends Effect{ float rawfin = e.fin(); float fin = e.fin(interp); float rad = interp.apply(sizeFrom, sizeTo, rawfin) * 2; + float ox = e.x + Angles.trnsx(e.rotation, offsetX, offsetY), oy = e.y + Angles.trnsy(e.rotation, offsetX, offsetY); Draw.color(colorFrom, colorTo, fin); + Color lightColor = this.lightColor == null ? Draw.getColor() : this.lightColor; if(line){ Lines.stroke(interp.apply(strokeFrom, strokeTo, rawfin)); float len = interp.apply(lenFrom, lenTo, rawfin); Angles.randLenVectors(e.id, particles, length * fin + baseLength, e.rotation, cone, (x, y) -> { - Lines.lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), len); + Lines.lineAngle(ox + x, oy + y, Mathf.angle(x, y), len); + Drawf.light(ox + x, oy + y, len * lightScl, lightColor, lightOpacity); }); }else{ Angles.randLenVectors(e.id, particles, length * fin + baseLength, e.rotation, cone, (x, y) -> { - Draw.rect(tex, e.x + x, e.y + y, rad, rad, e.rotation + offset); + Draw.rect(tex, ox + x, oy + y, rad, rad, e.rotation + offset + e.time * spin); + Drawf.light(ox + x, oy + y, rad * lightScl, lightColor, lightOpacity); }); } } diff --git a/core/src/mindustry/entities/effect/WaveEffect.java b/core/src/mindustry/entities/effect/WaveEffect.java index 88ff7391d2..7aaddd95b3 100644 --- a/core/src/mindustry/entities/effect/WaveEffect.java +++ b/core/src/mindustry/entities/effect/WaveEffect.java @@ -3,16 +3,21 @@ package mindustry.entities.effect; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; +import arc.util.*; import mindustry.entities.*; +import mindustry.graphics.*; /** Effect that renders a basic shockwave. */ public class WaveEffect extends Effect{ public Color colorFrom = Color.white.cpy(), colorTo = Color.white.cpy(); - public float sizeFrom = 0f, sizeTo = 100f; + public @Nullable Color lightColor; + public float sizeFrom = 0f, sizeTo = 100f, lightScl = 3f, lightOpacity = 0.8f; public int sides = -1; public float rotation = 0f; public float strokeFrom = 2f, strokeTo = 0f; public Interp interp = Interp.linear; + public Interp lightInterp = Interp.reverse; + public float offsetX, offsetY; @Override public void init(){ @@ -23,11 +28,14 @@ public class WaveEffect extends Effect{ public void render(EffectContainer e){ float fin = e.fin(); float ifin = e.fin(interp); + float ox = e.x + Angles.trnsx(e.rotation, offsetX, offsetY), oy = e.y + Angles.trnsy(e.rotation, offsetX, offsetY); Draw.color(colorFrom, colorTo, ifin); Lines.stroke(interp.apply(strokeFrom, strokeTo, fin)); float rad = interp.apply(sizeFrom, sizeTo, fin); - Lines.poly(e.x, e.y, sides <= 0 ? Lines.circleVertices(rad) : sides, rad, rotation + e.rotation); + Lines.poly(ox, oy, sides <= 0 ? Lines.circleVertices(rad) : sides, rad, rotation + e.rotation); + + Drawf.light(ox, oy, rad * lightScl, lightColor == null ? Draw.getColor() : lightColor, lightOpacity * e.fin(lightInterp)); } } diff --git a/core/src/mindustry/entities/units/AIController.java b/core/src/mindustry/entities/units/AIController.java index 9aaac0a4e0..e0c3231309 100644 --- a/core/src/mindustry/entities/units/AIController.java +++ b/core/src/mindustry/entities/units/AIController.java @@ -23,8 +23,6 @@ public class AIController implements UnitController{ /** main target that is being faced */ protected Teamc target; - /** targets for each weapon */ - protected Teamc[] targets = {}; { timer.reset(0, Mathf.random(40f)); @@ -94,8 +92,6 @@ public class AIController implements UnitController{ } protected void updateWeapons(){ - if(targets.length != unit.mounts.length) targets = new Teamc[unit.mounts.length]; - float rotation = unit.rotation - 90; boolean ret = retarget(); @@ -109,39 +105,39 @@ public class AIController implements UnitController{ unit.isShooting = false; - for(int i = 0; i < targets.length; i++){ - WeaponMount mount = unit.mounts[i]; + for(var mount : unit.mounts){ Weapon weapon = mount.weapon; + //let uncontrollable weapons do their own thing + if(!weapon.controllable) continue; + float mountX = unit.x + Angles.trnsx(rotation, weapon.x, weapon.y), mountY = unit.y + Angles.trnsy(rotation, weapon.x, weapon.y); if(unit.type.singleTarget){ - targets[i] = target; + mount.target = target; }else{ if(ret){ - targets[i] = findTarget(mountX, mountY, weapon.bullet.range(), weapon.bullet.collidesAir, weapon.bullet.collidesGround); + mount.target = findTarget(mountX, mountY, weapon.bullet.range(), weapon.bullet.collidesAir, weapon.bullet.collidesGround); } - if(checkTarget(targets[i], mountX, mountY, weapon.bullet.range())){ - targets[i] = null; + if(checkTarget(mount.target, mountX, mountY, weapon.bullet.range())){ + mount.target = null; } } boolean shoot = false; - if(targets[i] != null){ - shoot = targets[i].within(mountX, mountY, weapon.bullet.range()) && shouldShoot(); + if(mount.target != null){ + shoot = mount.target.within(mountX, mountY, weapon.bullet.range() + (mount.target instanceof Sized s ? s.hitSize()/2f : 0f)) && shouldShoot(); - Vec2 to = Predict.intercept(unit, targets[i], weapon.bullet.speed); + Vec2 to = Predict.intercept(unit, mount.target, weapon.bullet.speed); mount.aimX = to.x; mount.aimY = to.y; } - mount.shoot = shoot; - mount.rotate = shoot; + unit.isShooting |= (mount.shoot = mount.rotate = shoot); - unit.isShooting |= shoot; if(shoot){ unit.aimX = mount.aimX; unit.aimY = mount.aimY; diff --git a/core/src/mindustry/entities/units/StatusEntry.java b/core/src/mindustry/entities/units/StatusEntry.java index 1c1f07f9b7..b733457772 100644 --- a/core/src/mindustry/entities/units/StatusEntry.java +++ b/core/src/mindustry/entities/units/StatusEntry.java @@ -3,8 +3,6 @@ package mindustry.entities.units; import mindustry.type.*; public class StatusEntry{ - public static final StatusEntry tmp = new StatusEntry(); - public StatusEffect effect; public float time; diff --git a/core/src/mindustry/entities/units/WeaponMount.java b/core/src/mindustry/entities/units/WeaponMount.java index e5f61679c7..8254c8ca2f 100644 --- a/core/src/mindustry/entities/units/WeaponMount.java +++ b/core/src/mindustry/entities/units/WeaponMount.java @@ -28,6 +28,10 @@ public class WeaponMount{ public @Nullable Bullet bullet; /** sound loop for continuous weapons */ public @Nullable SoundLoop sound; + /** current target; used for autonomous weapons and AI */ + public @Nullable Teamc target; + /** retarget counter */ + public float retarget = 0f; public WeaponMount(Weapon weapon){ this.weapon = weapon; diff --git a/core/src/mindustry/game/EventType.java b/core/src/mindustry/game/EventType.java index 975a9baf1e..61faeaffd0 100644 --- a/core/src/mindustry/game/EventType.java +++ b/core/src/mindustry/game/EventType.java @@ -6,6 +6,7 @@ import mindustry.ctype.*; import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.net.*; +import mindustry.net.Packets.*; import mindustry.type.*; import mindustry.world.*; @@ -242,11 +243,31 @@ public class EventType{ } } - public static class TileChangeEvent{ - public final Tile tile; + /** + * Called *before* a tile has changed. + * WARNING! This event is special: its instance is reused! Do not cache or use with a timer. + * Do not modify any tiles inside listeners that use this tile. + * */ + public static class TilePreChangeEvent{ + public Tile tile; - public TileChangeEvent(Tile tile){ + public TilePreChangeEvent set(Tile tile){ this.tile = tile; + return this; + } + } + + /** + * Called *after* a tile has changed. + * WARNING! This event is special: its instance is reused! Do not cache or use with a timer. + * Do not modify any tiles inside listeners that use this tile. + * */ + public static class TileChangeEvent{ + public Tile tile; + + public TileChangeEvent set(Tile tile){ + this.tile = tile; + return this; } } @@ -392,6 +413,17 @@ public class EventType{ } } + /** Called when a player sends a connection packet. */ + public static class ConnectPacketEvent{ + public final NetConnection connection; + public final ConnectPacket packet; + + public ConnectPacketEvent(NetConnection connection, ConnectPacket packet){ + this.connection = connection; + this.packet = packet; + } + } + /** Called after connecting; when a player receives world data and is ready to play.*/ public static class PlayerJoin{ public final Player player; diff --git a/core/src/mindustry/game/Gamemode.java b/core/src/mindustry/game/Gamemode.java index e4c0d01c6c..a83fcd3e8d 100644 --- a/core/src/mindustry/game/Gamemode.java +++ b/core/src/mindustry/game/Gamemode.java @@ -2,6 +2,7 @@ package mindustry.game; import arc.*; import arc.func.*; +import arc.util.*; import mindustry.maps.*; import static mindustry.Vars.*; @@ -22,7 +23,7 @@ public enum Gamemode{ rules.waves = true; rules.waveTimer = true; - rules.waveSpacing /= 2f; + rules.waveSpacing = 60f * Time.toMinutes; rules.teams.get(rules.waveTeam).infiniteResources = true; }, map -> map.teams.contains(state.rules.waveTeam.id)), pvp(rules -> { diff --git a/core/src/mindustry/game/Rules.java b/core/src/mindustry/game/Rules.java index 9b95bbf7d3..093a09f722 100644 --- a/core/src/mindustry/game/Rules.java +++ b/core/src/mindustry/game/Rules.java @@ -10,6 +10,8 @@ import mindustry.io.*; import mindustry.type.*; import mindustry.type.Weather.*; import mindustry.world.*; +import mindustry.world.blocks.*; +import mindustry.world.meta.*; /** * Defines current rules on how the game should function. @@ -34,6 +36,8 @@ public class Rules{ public boolean editor = false; /** Whether a gameover can happen at all. Set this to false to implement custom gameover conditions. */ public boolean canGameOver = true; + /** Whether cores change teams when they are destroyed. */ + public boolean coreCapture = false; /** Whether reactors can explode and damage other blocks. */ public boolean reactorExplosions = true; /** Whether schematics are allowed. */ @@ -72,6 +76,10 @@ public class Rules{ public int winWave = 0; /** Base unit cap. Can still be increased by blocks. */ public int unitCap = 0; + /** Environmental flags that dictate visuals & how blocks function. */ + public int environment = Env.terrestrial | Env.spores | Env.groundOil | Env.groundWater; + /** Attributes of the environment. */ + public Attributes attributes = new Attributes(); /** Sector for saves that have them. */ public @Nullable Sector sector; /** Spawn layout. */ diff --git a/core/src/mindustry/game/Saves.java b/core/src/mindustry/game/Saves.java index c832f86471..b385e38884 100644 --- a/core/src/mindustry/game/Saves.java +++ b/core/src/mindustry/game/Saves.java @@ -214,7 +214,7 @@ public class Saves{ } previewExecutor.submit(() -> { try{ - previewFile().writePNG(renderer.minimap.getPixmap()); + previewFile().writePng(renderer.minimap.getPixmap()); requestedPreview = false; }catch(Throwable t){ Log.err(t); diff --git a/core/src/mindustry/game/Schematic.java b/core/src/mindustry/game/Schematic.java index e539ae69a2..89556e5881 100644 --- a/core/src/mindustry/game/Schematic.java +++ b/core/src/mindustry/game/Schematic.java @@ -15,6 +15,9 @@ import static mindustry.Vars.*; public class Schematic implements Publishable, Comparable{ public final Seq tiles; + /** These are used for the schematic tag UI. */ + public Seq labels = new Seq<>(); + /** Internal meta tags. */ public StringMap tags; public int width, height; public @Nullable Fi file; @@ -29,7 +32,7 @@ public class Schematic implements Publishable, Comparable{ } public float powerProduction(){ - return tiles.sumf(s -> s.block instanceof PowerGenerator ? ((PowerGenerator)s.block).powerProduction : 0f); + return tiles.sumf(s -> s.block instanceof PowerGenerator p ? p.powerProduction : 0f); } public float powerConsumption(){ diff --git a/core/src/mindustry/game/Schematics.java b/core/src/mindustry/game/Schematics.java index 4fd3a66079..f21381802c 100644 --- a/core/src/mindustry/game/Schematics.java +++ b/core/src/mindustry/game/Schematics.java @@ -26,7 +26,6 @@ import mindustry.input.*; import mindustry.input.Placement.*; import mindustry.io.*; import mindustry.world.*; -import mindustry.world.blocks.*; import mindustry.world.blocks.ConstructBlock.*; import mindustry.world.blocks.distribution.*; import mindustry.world.blocks.legacy.*; @@ -128,8 +127,6 @@ public class Schematics implements Loadable{ Log.err(e); ui.showException(e); } - - } private @Nullable Schematic loadFile(Fi file){ @@ -173,7 +170,7 @@ public class Schematics implements Loadable{ Draw.flush(); buffer.begin(); Pixmap pixmap = ScreenUtils.getFrameBufferPixmap(0, 0, buffer.getWidth(), buffer.getHeight()); - file.writePNG(pixmap); + file.writePng(pixmap); buffer.end(); } @@ -349,7 +346,7 @@ public class Schematics implements Loadable{ for(int cx = x; cx <= x2; cx++){ for(int cy = y; cy <= y2; cy++){ Building linked = world.build(cx, cy); - Block realBlock = linked == null ? null : linked instanceof ConstructBuild cons ? cons.cblock : linked.block; + Block realBlock = linked == null ? null : linked instanceof ConstructBuild cons ? cons.current : linked.block; if(linked != null && realBlock != null && (realBlock.isVisible() || realBlock instanceof CoreBlock)){ int top = realBlock.size/2; @@ -378,7 +375,7 @@ public class Schematics implements Loadable{ for(int cx = ox; cx <= ox2; cx++){ for(int cy = oy; cy <= oy2; cy++){ Building tile = world.build(cx, cy); - Block realBlock = tile == null ? null : tile instanceof ConstructBuild cons ? cons.cblock : tile.block; + Block realBlock = tile == null ? null : tile instanceof ConstructBuild cons ? cons.current : tile.block; if(tile != null && !counted.contains(tile.pos()) && realBlock != null && (realBlock.isVisible() || realBlock instanceof CoreBlock)){ @@ -424,7 +421,7 @@ public class Schematics implements Loadable{ Seq seq = new Seq<>(); if(coreTile == null) throw new IllegalArgumentException("Loadout schematic has no core tile!"); int ox = x - coreTile.x, oy = y - coreTile.y; - schem.tiles.each(st -> { + schem.tiles.copy().sort(s -> -s.block.schematicPriority).each(st -> { Tile tile = world.tile(st.x + ox, st.y + oy); if(tile == null) return; @@ -498,11 +495,19 @@ public class Schematics implements Loadable{ short width = stream.readShort(), height = stream.readShort(); StringMap map = new StringMap(); - byte tags = stream.readByte(); + int tags = stream.readUnsignedByte(); for(int i = 0; i < tags; i++){ map.put(stream.readUTF(), stream.readUTF()); } + String[] labels = null; + + //try to read the categories, but skip if it fails + try{ + labels = JsonIO.read(String[].class, map.get("labels", "[]")); + }catch(Exception ignored){ + } + IntMap blocks = new IntMap<>(); byte length = stream.readByte(); for(int i = 0; i < length; i++){ @@ -523,7 +528,9 @@ public class Schematics implements Loadable{ } } - return new Schematic(tiles, map, width, height); + Schematic out = new Schematic(tiles, map, width, height); + if(labels != null) out.labels.addAll(labels); + return out; } } @@ -540,6 +547,8 @@ public class Schematics implements Loadable{ stream.writeShort(schematic.width); stream.writeShort(schematic.height); + schematic.tags.put("labels", JsonIO.write(schematic.labels.toArray(String.class))); + stream.writeByte(schematic.tags.size); for(ObjectMap.Entry e : schematic.tags.entries()){ stream.writeUTF(e.key); diff --git a/core/src/mindustry/game/SectorInfo.java b/core/src/mindustry/game/SectorInfo.java index f49225d403..06dd82a59c 100644 --- a/core/src/mindustry/game/SectorInfo.java +++ b/core/src/mindustry/game/SectorInfo.java @@ -9,6 +9,7 @@ import mindustry.maps.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.storage.CoreBlock.*; +import mindustry.world.meta.*; import mindustry.world.modules.*; import java.util.*; @@ -69,6 +70,8 @@ public class SectorInfo{ public @Nullable String name; /** Displayed icon. */ public @Nullable String icon; + /** Displayed icon, as content. */ + public @Nullable UnlockableContent contentIcon; /** Version of generated waves. When it doesn't match, new waves are generated. */ public int waveVersion = -1; /** Whether this sector was indicated to the player or not. */ @@ -191,6 +194,13 @@ public class SectorInfo{ stat.mean = Math.min(stat.mean, rawProduction.get(item, ExportStat::new).mean); }); + var pads = indexer.getAllied(state.rules.defaultTeam, BlockFlag.launchPad); + + //disable export when launch pads are disabled, or there aren't any active ones + if(pads.size() == 0 || !Seq.with(pads).contains(t -> t.build.consValid())){ + export.clear(); + } + if(state.rules.sector != null){ state.rules.sector.saveInfo(); } diff --git a/core/src/mindustry/game/Teams.java b/core/src/mindustry/game/Teams.java index 610b32de86..92b85a2481 100644 --- a/core/src/mindustry/game/Teams.java +++ b/core/src/mindustry/game/Teams.java @@ -26,8 +26,8 @@ public class Teams{ public Seq active = new Seq<>(); /** Teams with block or unit presence. */ public Seq present = new Seq<>(TeamData.class); - /** Current boss unit. */ - public @Nullable Unit boss; + /** Current boss units. */ + public Seq bosses = new Seq<>(); public Teams(){ active.add(get(Team.crux)); @@ -117,7 +117,6 @@ public class Teams{ if(data.active() && !active.contains(data)){ active.add(data); updateEnemies(); - indexer.updateTeamIndex(data.team); } } @@ -145,7 +144,7 @@ public class Teams{ public void updateTeamStats(){ present.clear(); - boss = null; + bosses.clear(); for(Team team : Team.all){ TeamData data = team.data(); @@ -172,16 +171,17 @@ public class Teams{ } //update presence flag. - Groups.build.each( b -> b.team.data().presentFlag = true); + Groups.build.each(b -> b.team.data().presentFlag = true); for(Unit unit : Groups.unit){ + if(unit.type == null) continue; TeamData data = unit.team.data(); data.tree().insert(unit); data.units.add(unit); data.presentFlag = true; if(unit.team == state.rules.waveTeam && unit.isBoss()){ - boss = unit; + bosses.add(unit); } if(data.unitsByType == null || data.unitsByType.length <= unit.type.id){ @@ -241,12 +241,17 @@ public class Teams{ /** Target items to mine. */ public Seq mineItems = Seq.with(Items.copper, Items.lead, Items.titanium, Items.thorium); + /** Quadtree for all buildings of this team. Null if not active. */ + @Nullable + public QuadTree buildings; + /** Current unit cap. Do not modify externally. */ + public int unitCap; /** Total unit count. */ public int unitCount; /** Counts for each type of unit. Do not access directly. */ @Nullable public int[] typeCounts; - /** Quadtree for units of this type. Do not access directly. */ + /** Quadtree for units of this team. Do not access directly. */ @Nullable public QuadTree tree; /** Units of this team. Updated each frame. */ @@ -328,5 +333,16 @@ public class Teams{ this.block = block; this.config = config; } + + @Override + public String toString(){ + return "BlockPlan{" + + "x=" + x + + ", y=" + y + + ", rotation=" + rotation + + ", block=" + block + + ", config=" + config + + '}'; + } } } diff --git a/core/src/mindustry/game/Universe.java b/core/src/mindustry/game/Universe.java index 2389b22f7d..a9215f70d0 100644 --- a/core/src/mindustry/game/Universe.java +++ b/core/src/mindustry/game/Universe.java @@ -7,6 +7,7 @@ import arc.util.*; import mindustry.content.*; import mindustry.game.EventType.*; import mindustry.game.SectorInfo.*; +import mindustry.gen.*; import mindustry.maps.*; import mindustry.type.*; import mindustry.world.blocks.storage.*; @@ -236,6 +237,10 @@ public class Universe{ state.rules.winWave = waveMax; state.rules.waves = true; state.rules.attackMode = false; + //update rules in multiplayer + if(net.server()){ + Call.setRules(state.rules); + } }else{ sector.info.winWave = waveMax; sector.info.waves = true; diff --git a/core/src/mindustry/graphics/BlockRenderer.java b/core/src/mindustry/graphics/BlockRenderer.java index 8eb2a38f32..5183551b93 100644 --- a/core/src/mindustry/graphics/BlockRenderer.java +++ b/core/src/mindustry/graphics/BlockRenderer.java @@ -6,6 +6,7 @@ import arc.graphics.Texture.*; import arc.graphics.g2d.*; import arc.graphics.gl.*; import arc.math.*; +import arc.math.geom.*; import arc.struct.*; import arc.util.*; import mindustry.content.*; @@ -23,8 +24,7 @@ public class BlockRenderer{ public static final int crackRegions = 8, maxCrackSize = 9; private static final int initialRequests = 32 * 32; - private static final int expandr = 10; - private static final Color shadowColor = new Color(0, 0, 0, 0.71f); + private static final Color shadowColor = new Color(0, 0, 0, 0.71f), blendShadowColor = Color.white.cpy().lerp(Color.black, shadowColor.a); public final FloorRenderer floor = new FloorRenderer(); public TextureRegion[][] cracks; @@ -38,7 +38,11 @@ public class BlockRenderer{ private FrameBuffer dark = new FrameBuffer(); private Seq outArray2 = new Seq<>(); private Seq shadowEvents = new Seq<>(); - private IntSet procEntities = new IntSet(), procLinks = new IntSet(), procLights = new IntSet(); + private IntSet darkEvents = new IntSet(); + private IntSet procLinks = new IntSet(), procLights = new IntSet(); + + private BlockQuadtree blockTree; + private FloorQuadtree floorTree; public BlockRenderer(){ @@ -52,6 +56,8 @@ public class BlockRenderer{ }); Events.on(WorldLoadEvent.class, event -> { + blockTree = new BlockQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight())); + floorTree = new FloorQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight())); shadowEvents.clear(); lastCamY = lastCamX = -99; //invalidate camera position so blocks get updated @@ -61,7 +67,7 @@ public class BlockRenderer{ Core.graphics.clear(Color.white); Draw.proj().setOrtho(0, 0, shadows.getWidth(), shadows.getHeight()); - Draw.color(shadowColor); + Draw.color(blendShadowColor); for(Tile tile : world.tiles){ if(tile.block().hasShadow){ @@ -73,17 +79,19 @@ public class BlockRenderer{ Draw.color(); shadows.end(); - dark.getTexture().setFilter(TextureFilter.linear, TextureFilter.linear); + dark.getTexture().setFilter(TextureFilter.linear); dark.resize(world.width(), world.height()); dark.begin(); Core.graphics.clear(Color.white); Draw.proj().setOrtho(0, 0, dark.getWidth(), dark.getHeight()); for(Tile tile : world.tiles){ + recordIndex(tile); + float darkness = world.getDarkness(tile.x, tile.y); if(darkness > 0){ - Draw.color(0f, 0f, 0f, Math.min((darkness + 0.5f) / 4f, 1f)); + Draw.colorl(1f - Math.min((darkness + 0.5f) / 4f, 1f)); Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1); } } @@ -93,6 +101,11 @@ public class BlockRenderer{ dark.end(); }); + Events.on(TilePreChangeEvent.class, event -> { + if(indexBlock(event.tile)) blockTree.remove(event.tile); + if(indexFloor(event.tile)) floorTree.remove(event.tile); + }); + Events.on(TileChangeEvent.class, event -> { shadowEvents.add(event.tile); @@ -104,10 +117,60 @@ public class BlockRenderer{ if(Math.abs(avgx - event.tile.x) <= rangex && Math.abs(avgy - event.tile.y) <= rangey){ lastCamY = lastCamX = -99; //invalidate camera position so blocks get updated } + + recordIndex(event.tile); }); } + boolean indexBlock(Tile tile){ + var block = tile.block(); + return tile.isCenter() && block != Blocks.air && block.cacheLayer == CacheLayer.normal; + } + + boolean indexFloor(Tile tile){ + return tile.block() == Blocks.air && tile.floor().emitLight && world.getDarkness(tile.x, tile.y) < 3; + } + + void recordIndex(Tile tile){ + if(indexBlock(tile)) blockTree.insert(tile); + if(indexFloor(tile)) floorTree.insert(tile); + } + + public void recacheWall(Tile tile){ + for(int cx = tile.x - darkRadius; cx <= tile.x + darkRadius; cx++){ + for(int cy = tile.y - darkRadius; cy <= tile.y + darkRadius; cy++){ + Tile other = world.tile(cx, cy); + if(other != null){ + darkEvents.add(other.pos()); + } + } + } + } + public void drawDarkness(){ + if(!darkEvents.isEmpty()){ + Draw.flush(); + + dark.begin(); + Draw.proj().setOrtho(0, 0, dark.getWidth(), dark.getHeight()); + + darkEvents.each(pos -> { + var tile = world.tile(pos); + tile.data = world.getWallDarkness(tile); + float darkness = world.getDarkness(tile.x, tile.y); + //then draw the shadow + Draw.colorl(!tile.isDarkened() || darkness <= 0f ? 1f : 1f - Math.min((darkness + 0.5f) / 4f, 1f)); + Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1); + }); + + Draw.flush(); + Draw.color(); + dark.end(); + darkEvents.clear(); + + Draw.proj(camera); + } + Draw.shader(Shaders.darkness); Draw.fbo(dark, world.width(), world.height(), tilesize); Draw.shader(); @@ -129,7 +192,7 @@ public class BlockRenderer{ Draw.alpha(0.33f * brokenFade); Draw.mixcol(Color.white, 0.2f + Mathf.absin(Time.globalTime, 6f, 0.2f)); - Draw.rect(b.icon(Cicon.full), block.x * tilesize + b.offset, block.y * tilesize + b.offset, b.rotate ? block.rotation * 90 : 0f); + Draw.rect(b.fullIcon, block.x * tilesize + b.offset, block.y * tilesize + b.offset, b.rotate ? block.rotation * 90 : 0f); } Draw.reset(); } @@ -143,11 +206,8 @@ public class BlockRenderer{ Draw.proj().setOrtho(0, 0, shadows.getWidth(), shadows.getHeight()); for(Tile tile : shadowEvents){ - //clear it first - Draw.color(Color.white); - Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1); - //then draw the shadow - Draw.color(!tile.block().hasShadow ? Color.white : shadowColor); + //draw white/shadow color depending on blend + Draw.color(!tile.block().hasShadow ? Color.white : blendShadowColor); Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1); } @@ -176,12 +236,11 @@ public class BlockRenderer{ /** Process all blocks to draw. */ public void processBlocks(){ - int avgx = (int)(camera.position.x / tilesize); int avgy = (int)(camera.position.y / tilesize); - int rangex = (int)(camera.width / tilesize / 2) + 3; - int rangey = (int)(camera.height / tilesize / 2) + 3; + int rangex = (int)(camera.width / tilesize / 2); + int rangey = (int)(camera.height / tilesize / 2); if(avgx == lastCamX && avgy == lastCamY && lastRangeX == rangex && lastRangeY == rangey){ return; @@ -189,56 +248,32 @@ public class BlockRenderer{ tileview.clear(); lightview.clear(); - procEntities.clear(); procLinks.clear(); procLights.clear(); - int minx = Math.max(avgx - rangex - expandr, 0); - int miny = Math.max(avgy - rangey - expandr, 0); - int maxx = Math.min(world.width() - 1, avgx + rangex + expandr); - int maxy = Math.min(world.height() - 1, avgy + rangey + expandr); + var bounds = camera.bounds(Tmp.r3).grow(tilesize); - for(int x = minx; x <= maxx; x++){ - for(int y = miny; y <= maxy; y++){ - boolean expanded = (Math.abs(x - avgx) > rangex || Math.abs(y - avgy) > rangey); - Tile tile = world.rawTile(x, y); - Block block = tile.block(); - //link to center - if(tile.build != null){ - tile = tile.build.tile; - } + //draw floor lights + floorTree.intersect(bounds, tile -> lightview.add(tile)); - if(block != Blocks.air && block.cacheLayer == CacheLayer.normal && (tile.build == null || !procEntities.contains(tile.build.id))){ - if(block.expanded || !expanded){ - if(tile.build == null || procLinks.add(tile.build.id)){ - tileview.add(tile); - if(tile.build != null){ - procEntities.add(tile.build.id); - procLinks.add(tile.build.id); - } - } + blockTree.intersect(bounds, tile -> { + if(tile.build == null || procLinks.add(tile.build.id)){ + tileview.add(tile); + } + + //lights are drawn even in the expanded range + if(((tile.build != null && procLights.add(tile.build.pos())) || tile.block().emitLight)){ + lightview.add(tile); + } + + if(tile.build != null && tile.build.power != null && tile.build.power.links.size > 0){ + for(Building other : tile.build.getPowerConnections(outArray2)){ + if(other.block instanceof PowerNode && procLinks.add(other.id)){ //TODO need a generic way to render connections! + tileview.add(other.tile); } - - //lights are drawn even in the expanded range - if(((tile.build != null && procLights.add(tile.build.pos())) || tile.block().emitLight)){ - lightview.add(tile); - } - - if(tile.build != null && tile.build.power != null && tile.build.power.links.size > 0){ - for(Building other : tile.build.getPowerConnections(outArray2)){ - if(other.block instanceof PowerNode && procLinks.add(other.id)){ //TODO need a generic way to render connections! - tileview.add(other.tile); - } - } - } - } - - //special case for floors - if((block == Blocks.air && tile.floor().emitLight) && procLights.add(tile.pos())){ - lightview.add(tile); } } - } + }); lastCamX = avgx; lastCamY = avgy; @@ -246,7 +281,29 @@ public class BlockRenderer{ lastRangeY = rangey; } + //debug method for drawing block bounds + void drawTree(QuadTree tree){ + Draw.color(Color.blue); + Lines.rect(tree.bounds); + + Draw.color(Color.green); + for(var tile : tree.objects){ + var block = tile.block(); + Tmp.r1.setCentered(tile.worldx() + block.offset, tile.worldy() + block.offset, block.clipSize, block.clipSize); + Lines.rect(Tmp.r1); + } + + if(!tree.leaf){ + drawTree(tree.botLeft); + drawTree(tree.botRight); + drawTree(tree.topLeft); + drawTree(tree.topRight); + } + Draw.reset(); + } + public void drawBlocks(){ + drawDestroyed(); //draw most tile stuff @@ -291,13 +348,47 @@ public class BlockRenderer{ entity.drawLight(); }else if(tile.block().emitLight){ tile.block().drawEnvironmentLight(tile); - }else if(tile.floor().emitLight && !tile.block().solid && world.getDarkness(tile.x, tile.y) < 3){ //only draw floor light under non-solid blocks + }else if(tile.floor().emitLight && tile.block() == Blocks.air){ //only draw floor light under non-solid blocks tile.floor().drawEnvironmentLight(tile); } } } + } + static class BlockQuadtree extends QuadTree{ + public BlockQuadtree(Rect bounds){ + super(bounds); + } + + @Override + public void hitbox(Tile tile){ + var block = tile.block(); + tmp.setCentered(tile.worldx() + block.offset, tile.worldy() + block.offset, block.clipSize, block.clipSize); + } + + @Override + protected QuadTree newChild(Rect rect){ + return new BlockQuadtree(rect); + } + } + + static class FloorQuadtree extends QuadTree{ + + public FloorQuadtree(Rect bounds){ + super(bounds); + } + + @Override + public void hitbox(Tile tile){ + var floor = tile.floor(); + tmp.setCentered(tile.worldx(), tile.worldy(), floor.clipSize, floor.clipSize); + } + + @Override + protected QuadTree newChild(Rect rect){ + return new FloorQuadtree(rect); + } } } diff --git a/core/src/mindustry/graphics/CacheLayer.java b/core/src/mindustry/graphics/CacheLayer.java index 01ef5e5bff..31b8074196 100644 --- a/core/src/mindustry/graphics/CacheLayer.java +++ b/core/src/mindustry/graphics/CacheLayer.java @@ -6,92 +6,80 @@ import arc.graphics.gl.*; import static mindustry.Vars.*; -public enum CacheLayer{ - water{ - @Override - public void begin(){ - beginShader(); - } +public class CacheLayer{ + public static CacheLayer - @Override - public void end(){ - endShader(Shaders.water); - } - }, - mud{ - @Override - public void begin(){ - beginShader(); - } + water, mud, tar, slag, space, normal, walls; - @Override - public void end(){ - endShader(Shaders.mud); - } - }, - tar{ - @Override - public void begin(){ - beginShader(); - } + public static CacheLayer[] all = {}; - @Override - public void end(){ - endShader(Shaders.tar); - } - }, - slag{ - @Override - public void begin(){ - beginShader(); - } + public int id; - @Override - public void end(){ - endShader(Shaders.slag); - } - }, - space{ - @Override - public void begin(){ - beginShader(); - } + /** Register a new CacheLayer. */ + public static void add(CacheLayer... layers){ + int newSize = all.length + layers.length; + var prev = all; + //reallocate the array and copy everything over; performance matters very little here anyway + all = new CacheLayer[newSize]; + System.arraycopy(prev, 0, all, 0, prev.length); + System.arraycopy(layers, 0, all, prev.length, layers.length); - @Override - public void end(){ - endShader(Shaders.space); + for(int i = 0; i < all.length; i++){ + all[i].id = i; } - }, - normal, - walls; + } - public static final CacheLayer[] all = values(); + /** Loads default cache layers. */ + public static void init(){ + add( + water = new ShaderLayer(Shaders.water), + mud = new ShaderLayer(Shaders.mud), + tar = new ShaderLayer(Shaders.tar), + slag = new ShaderLayer(Shaders.slag), + space = new ShaderLayer(Shaders.space), + normal = new CacheLayer(), + walls = new CacheLayer() + ); + } + /** Called before the cache layer begins rendering. Begin FBOs here. */ public void begin(){ } + /** Called after the cache layer ends rendering. Blit FBOs here. */ public void end(){ } - void beginShader(){ - if(!Core.settings.getBool("animatedwater")) return; + public static class ShaderLayer extends CacheLayer{ + public Shader shader; - renderer.blocks.floor.endc(); - renderer.effectBuffer.begin(); - Core.graphics.clear(Color.clear); - renderer.blocks.floor.beginc(); - } + public ShaderLayer(Shader shader){ + //shader will be null on headless backend, but that's ok + this.shader = shader; + } - void endShader(Shader shader){ - if(!Core.settings.getBool("animatedwater")) return; + @Override + public void begin(){ + if(!Core.settings.getBool("animatedwater")) return; - renderer.blocks.floor.endc(); - renderer.effectBuffer.end(); + renderer.blocks.floor.endc(); + renderer.effectBuffer.begin(); + Core.graphics.clear(Color.clear); + renderer.blocks.floor.beginc(); + } - renderer.effectBuffer.blit(shader); + @Override + public void end(){ + if(!Core.settings.getBool("animatedwater")) return; - renderer.blocks.floor.beginc(); + renderer.blocks.floor.endc(); + renderer.effectBuffer.end(); + + renderer.effectBuffer.blit(shader); + + renderer.blocks.floor.beginc(); + } } } diff --git a/core/src/mindustry/graphics/Drawf.java b/core/src/mindustry/graphics/Drawf.java index 32576bf55a..7d8b2be9d5 100644 --- a/core/src/mindustry/graphics/Drawf.java +++ b/core/src/mindustry/graphics/Drawf.java @@ -10,13 +10,21 @@ import mindustry.*; import mindustry.ctype.*; import mindustry.game.*; import mindustry.gen.*; -import mindustry.ui.*; import mindustry.world.*; import static mindustry.Vars.*; public class Drawf{ + public static void dashLine(Color color, float x, float y, float x2, float y2){ + int segments = (int)(Math.max(Math.abs(x - x2), Math.abs(y - y2)) / tilesize * 2); + Lines.stroke(3f, Pal.gray); + Lines.dashLine(x, y, x2, y2, segments); + Lines.stroke(1f, color); + Lines.dashLine(x, y, x2, y2, segments); + Draw.reset(); + } + public static void target(float x, float y, float rad, Color color){ target(x, y, rad, 1, color); } @@ -96,7 +104,7 @@ public class Drawf{ public static void shadow(float x, float y, float rad, float alpha){ Draw.color(0, 0, 0, 0.4f * alpha); - Draw.rect("circle-shadow", x, y, rad, rad); + Draw.rect("circle-shadow", x, y, rad * Draw.xscl, rad * Draw.yscl); Draw.color(); } @@ -223,7 +231,7 @@ public class Drawf{ } public static void construct(Building t, UnlockableContent content, float rotation, float progress, float speed, float time){ - construct(t, content.icon(Cicon.full), rotation, progress, speed, time); + construct(t, content.fullIcon, rotation, progress, speed, time); } public static void construct(float x, float y, TextureRegion region, float rotation, float progress, float speed, float time){ diff --git a/core/src/mindustry/graphics/EnvRenderers.java b/core/src/mindustry/graphics/EnvRenderers.java new file mode 100644 index 0000000000..bc669b56ef --- /dev/null +++ b/core/src/mindustry/graphics/EnvRenderers.java @@ -0,0 +1,8 @@ +package mindustry.graphics; + +public class EnvRenderers{ + + public static void init(){ + + } +} diff --git a/core/src/mindustry/graphics/FloorRenderer.java b/core/src/mindustry/graphics/FloorRenderer.java index 900384e1e1..8d837b5dad 100644 --- a/core/src/mindustry/graphics/FloorRenderer.java +++ b/core/src/mindustry/graphics/FloorRenderer.java @@ -3,6 +3,7 @@ package mindustry.graphics; import arc.*; import arc.graphics.*; import arc.graphics.g2d.*; +import arc.graphics.gl.*; import arc.math.*; import arc.math.geom.*; import arc.struct.*; @@ -12,29 +13,93 @@ import mindustry.game.EventType.*; import mindustry.world.*; import mindustry.world.blocks.environment.*; -import java.util.*; - import static mindustry.Vars.*; -public class FloorRenderer implements Disposable{ - private static final int chunksize = mobile ? 16 : 32, chunkunits = chunksize * tilesize; +/** + * general implementation: + * + * caching: + * 1. create fixed-size float array fpr rendering into + * 2. for each chunk, cache each layer into buffer; record layer boundary indices (alternatively, create mesh per layer for fast recache) + * 3. create mesh for this chunk based on buffer size, copy buffer into mesh + * + * rendering: + * 1. iterate through visible chunks + * 2. activate the shader vertex attributes beforehand + * 3. bind each mesh individually, draw it + * + * */ +public class FloorRenderer{ + private static final VertexAttribute[] attributes = {VertexAttribute.position, VertexAttribute.color, VertexAttribute.texCoords}; + private static final int + chunksize = 30, //todo 32? + chunkunits = chunksize * tilesize, + vertexSize = 2 + 1 + 2, + spriteSize = vertexSize * 4, + maxSprites = chunksize * chunksize * 9; private static final float pad = tilesize/2f; + //if true, chunks are rendered on-demand; this causes small lag spikes and is generally not needed for most maps + private static final boolean dynamic = false; - private int[][][] cache; - private MultiCacheBatch cbatch; + private float[] vertices = new float[maxSprites * vertexSize * 4]; + private short[] indices = new short[maxSprites * 6]; + private int vidx; + private FloorRenderBatch batch = new FloorRenderBatch(); + private Shader shader; + private Texture texture; + private TextureRegion error; + + private Mesh[][][] cache; private IntSet drawnLayerSet = new IntSet(); private IntSet recacheSet = new IntSet(); private IntSeq drawnLayers = new IntSeq(); private ObjectSet used = new ObjectSet<>(); public FloorRenderer(){ + short j = 0; + for(int i = 0; i < indices.length; i += 6, j += 4){ + indices[i] = j; + indices[i + 1] = (short)(j + 1); + indices[i + 2] = (short)(j + 2); + indices[i + 3] = (short)(j + 2); + indices[i + 4] = (short)(j + 3); + indices[i + 5] = j; + } + + shader = new Shader( + """ + attribute vec4 a_position; + attribute vec4 a_color; + attribute vec2 a_texCoord0; + uniform mat4 u_projectionViewMatrix; + varying vec4 v_color; + varying vec2 v_texCoords; + + void main(){ + v_color = a_color; + v_color.a = v_color.a * (255.0/254.0); + v_texCoords = a_texCoord0; + gl_Position = u_projectionViewMatrix * a_position; + } + """, + """ + varying vec4 v_color; + varying vec2 v_texCoords; + uniform sampler2D u_texture; + + void main(){ + gl_FragColor = v_color * texture2D(u_texture, v_texCoords); + } + """); + Events.on(WorldLoadEvent.class, event -> clearTiles()); } - /**Queues up a cache change for a tile. Only runs in render loop. */ + /** Queues up a cache change for a tile. Only runs in render loop. */ public void recacheTile(Tile tile){ - //currently a no-op - //recacheSet.add(Point2.pack(tile.x / chunksize, tile.y / chunksize)); + //TODO will be faster it the position also specified the layer to be recached + //recaching all layers may not be necessary + recacheSet.add(Point2.pack(tile.x / chunksize, tile.y / chunksize)); } public void drawFloor(){ @@ -44,6 +109,8 @@ public class FloorRenderer implements Disposable{ Camera camera = Core.camera; + float pad = tilesize/2f; + int minx = (int)((camera.position.x - camera.width/2f - pad) / chunkunits), miny = (int)((camera.position.y - camera.height/2f - pad) / chunkunits), @@ -61,11 +128,15 @@ public class FloorRenderer implements Disposable{ if(!Structs.inBounds(x, y, cache)) continue; - int[] chunk = cache[x][y]; + if(cache[x][y].length == 0){ + cacheChunk(x, y); + } + + Mesh[] chunk = cache[x][y]; //loop through all layers, and add layer index if it exists for(int i = 0; i < layers; i++){ - if(chunk[i] != -1 && i != CacheLayer.walls.ordinal()){ + if(chunk[i] != null && i != CacheLayer.walls.id){ drawnLayerSet.add(i); } } @@ -92,11 +163,30 @@ public class FloorRenderer implements Disposable{ } public void beginc(){ - cbatch.beginDraw(); + shader.bind(); + shader.setUniformMatrix4("u_projectionViewMatrix", Core.camera.mat); + shader.setUniformi("u_texture", 0); + + //only ever use the base environment texture + //TODO show error texture for anything else + texture.bind(0); + + //enable all mesh attributes + for(VertexAttribute attribute : attributes){ + shader.enableVertexAttribute(attribute.alias); + } } public void endc(){ - cbatch.endDraw(); + + //disable all mesh attributes + for(VertexAttribute attribute : attributes){ + shader.disableVertexAttribute(attribute.alias); + } + + //unbind last buffer + Gl.bindBuffer(Gl.arrayBuffer, 0); + Gl.bindBuffer(Gl.elementArrayBuffer, 0); } public void checkChanges(){ @@ -118,8 +208,8 @@ public class FloorRenderer implements Disposable{ } Draw.flush(); - cbatch.setProjection(Core.camera.mat); - cbatch.beginDraw(); + + beginc(); Gl.enable(Gl.blend); } @@ -129,7 +219,7 @@ public class FloorRenderer implements Disposable{ return; } - cbatch.endDraw(); + endc(); } public void drawLayer(CacheLayer layer){ @@ -150,13 +240,37 @@ public class FloorRenderer implements Disposable{ for(int x = minx; x <= maxx; x++){ for(int y = miny; y <= maxy; y++){ - if(!Structs.inBounds(x, y, cache)){ + if(!Structs.inBounds(x, y, cache) || cache[x][y].length == 0){ continue; } - int[] chunk = cache[x][y]; - if(chunk[layer.ordinal()] == -1) continue; - cbatch.drawCache(chunk[layer.ordinal()]); + var mesh = cache[x][y][layer.id]; + + if(mesh != null){ + + //this *must* be a vertexbufferobject, so cast it and render it directly + if(mesh.vertices instanceof VertexBufferObject vbo && mesh.indices instanceof IndexBufferObject ibo){ + + //bindi the buffer and update its contents, but do not unnecessarily enable all the attributes again + vbo.bind(); + //set up vertex attribute pointers for this specific VBO + int offset = 0; + for(VertexAttribute attribute : attributes){ + int location = shader.getAttributeLocation(attribute.alias); + int aoffset = offset; + offset += attribute.size; + if(location < 0) continue; + + shader.setVertexAttribute(location, attribute.components, attribute.type, attribute.normalized, vertexSize * 4, aoffset); + } + + ibo.bind(); + + mesh.vertices.render(mesh.indices, Gl.triangles, 0, mesh.getNumIndices()); + }else{ + throw new ArcRuntimeException("Non-VBO meshes are not supported for caches."); + } + } } } @@ -165,7 +279,6 @@ public class FloorRenderer implements Disposable{ private void cacheChunk(int cx, int cy){ used.clear(); - int[] chunk = cache[cx][cy]; for(int tilex = cx * chunksize; tilex < (cx + 1) * chunksize && tilex < world.width(); tilex++){ for(int tiley = cy * chunksize; tiley < (cy + 1) * chunksize && tiley < world.height(); tiley++){ @@ -179,21 +292,29 @@ public class FloorRenderer implements Disposable{ } } + if(cache[cx][cy].length == 0){ + cache[cx][cy] = new Mesh[CacheLayer.all.length]; + } + + var meshes = cache[cx][cy]; + + for(CacheLayer layer : CacheLayer.all){ + if(meshes[layer.id] != null){ + meshes[layer.id].dispose(); + } + meshes[layer.id] = null; + } + for(CacheLayer layer : used){ - cacheChunkLayer(cx, cy, chunk, layer); + meshes[layer.id] = cacheChunkLayer(cx, cy, layer); } } - private void cacheChunkLayer(int cx, int cy, int[] chunk, CacheLayer layer){ - Batch current = Core.batch; - Core.batch = cbatch; + private Mesh cacheChunkLayer(int cx, int cy, CacheLayer layer){ + vidx = 0; - //begin a new cache - if(chunk[layer.ordinal()] == -1){ - cbatch.beginCache(); - }else{ - cbatch.beginCache(chunk[layer.ordinal()]); - } + Batch current = Core.batch; + Core.batch = batch; for(int tilex = cx * chunksize; tilex < (cx + 1) * chunksize; tilex++){ for(int tiley = cy * chunksize; tiley < (cy + 1) * chunksize; tiley++){ @@ -217,36 +338,173 @@ public class FloorRenderer implements Disposable{ } Core.batch = current; - chunk[layer.ordinal()] = cbatch.endCache(); + + int floats = vidx; + //every 4 vertices need 6 indices + int vertCount = floats / vertexSize, indCount = vertCount * 6/4; + + Mesh mesh = new Mesh(true, vertCount, indCount, attributes); + mesh.setAutoBind(false); + mesh.setVertices(vertices, 0, vidx); + mesh.setIndices(indices, 0, indCount); + + return mesh; } public void clearTiles(){ - if(cbatch != null) cbatch.dispose(); - - recacheSet.clear(); - int chunksx = Mathf.ceil((float)(world.width()) / chunksize), - chunksy = Mathf.ceil((float)(world.height()) / chunksize); - cache = new int[chunksx][chunksy][CacheLayer.all.length]; - cbatch = new MultiCacheBatch(chunksize * chunksize * 9); - - Time.mark(); - - for(int x = 0; x < chunksx; x++){ - for(int y = 0; y < chunksy; y++){ - Arrays.fill(cache[x][y], -1); - - cacheChunk(x, y); + //dispose all old meshes + if(cache != null){ + for(var x : cache){ + for(var y : x){ + for(var mesh : y){ + if(mesh != null){ + mesh.dispose(); + } + } + } } } - Log.debug("Time to cache: @", Time.elapsed()); + recacheSet.clear(); + int chunksx = Mathf.ceil((float)(world.width()) / chunksize), chunksy = Mathf.ceil((float)(world.height()) / chunksize); + cache = new Mesh[chunksx][chunksy][dynamic ? 0 : CacheLayer.all.length]; + + texture = Core.atlas.find("grass1").texture; + error = Core.atlas.find("env-error"); + //not supported due to internal access + Mesh.useVAO = false; + + //pre-cache chunks + if(!dynamic){ + Time.mark(); + + for(int x = 0; x < chunksx; x++){ + for(int y = 0; y < chunksy; y++){ + cacheChunk(x, y); + } + } + + Log.debug("Time to cache: @", Time.elapsed()); + } } - @Override - public void dispose(){ - if(cbatch != null){ - cbatch.dispose(); - cbatch = null; + class FloorRenderBatch extends Batch{ + + @Override + protected void draw(TextureRegion region, float x, float y, float originX, float originY, float width, float height, float rotation){ + + //substitute invalid regions with error + if(region.texture != texture && region != error){ + draw(error, x, y, originX, originY, width, height, rotation); + return; + } + + float[] verts = vertices; + int idx = vidx; + vidx += spriteSize; + + if(!Mathf.zero(rotation)){ + //bottom left and top right corner points relative to origin + float worldOriginX = x + originX; + float worldOriginY = y + originY; + float fx = -originX; + float fy = -originY; + float fx2 = width - originX; + float fy2 = height - originY; + + // rotate + 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; + float x4 = x1 + (x3 - x2); + float y4 = y3 - (y2 - y1); + + float u = region.u; + float v = region.v2; + float u2 = region.u2; + float v2 = region.v; + + float color = this.colorPacked; + + verts[idx] = x1; + verts[idx + 1] = y1; + verts[idx + 2] = color; + verts[idx + 3] = u; + verts[idx + 4] = v; + + verts[idx + 5] = x2; + verts[idx + 6] = y2; + verts[idx + 7] = color; + verts[idx + 8] = u; + verts[idx + 9] = v2; + + verts[idx + 10] = x3; + verts[idx + 11] = y3; + verts[idx + 12] = color; + verts[idx + 13] = u2; + verts[idx + 14] = v2; + + verts[idx + 15] = x4; + verts[idx + 16] = y4; + verts[idx + 17] = color; + verts[idx + 18] = u2; + verts[idx + 19] = v; + }else{ + float fx2 = x + width; + float fy2 = y + height; + float u = region.u; + float v = region.v2; + float u2 = region.u2; + float v2 = region.v; + + float color = this.colorPacked; + + verts[idx] = x; + verts[idx + 1] = y; + verts[idx + 2] = color; + verts[idx + 3] = u; + verts[idx + 4] = v; + + verts[idx + 5] = x; + verts[idx + 6] = fy2; + verts[idx + 7] = color; + verts[idx + 8] = u; + verts[idx + 9] = v2; + + verts[idx + 10] = fx2; + verts[idx + 11] = fy2; + verts[idx + 12] = color; + verts[idx + 13] = u2; + verts[idx + 14] = v2; + + verts[idx + 15] = fx2; + verts[idx + 16] = y; + verts[idx + 17] = color; + verts[idx + 18] = u2; + verts[idx + 19] = v; + } + + } + + @Override + public void flush(){ + + } + + @Override + public void setShader(Shader shader, boolean apply){ + throw new IllegalArgumentException("cache shader unsupported"); + } + + @Override + protected void draw(Texture texture, float[] spriteVertices, int offset, int count){ + throw new IllegalArgumentException("cache vertices unsupported"); } } } diff --git a/core/src/mindustry/graphics/LightRenderer.java b/core/src/mindustry/graphics/LightRenderer.java index ebf1cf9187..acf0583e02 100644 --- a/core/src/mindustry/graphics/LightRenderer.java +++ b/core/src/mindustry/graphics/LightRenderer.java @@ -19,6 +19,9 @@ public class LightRenderer{ private float[] vertices = new float[24]; private FrameBuffer buffer = new FrameBuffer(); private Seq lights = new Seq<>(); + private Seq circles = new Seq<>(CircleLight.class); + private int circleIndex = 0; + private TextureRegion circleRegion; public void add(Runnable run){ if(!enabled()) return; @@ -27,14 +30,17 @@ public class LightRenderer{ } public void add(float x, float y, float radius, Color color, float opacity){ - if(!enabled()) return; + if(!enabled() || radius <= 0f) return; - float res = color.toFloatBits(); - add(() -> { - Draw.color(res); - Draw.alpha(opacity); - Draw.rect("circle-shadow", x, y, radius * 2, radius * 2); - }); + float res = Color.toFloatBits(color.r, color.g, color.b, opacity); + + if(circles.size <= circleIndex) circles.add(new CircleLight()); + + //pool circles to prevent runaway GC usage from lambda capturing + var light = circles.items[circleIndex]; + light.set(x, y, res, radius); + + circleIndex ++; } public void add(float x, float y, TextureRegion region, Color color, float opacity){ @@ -170,7 +176,7 @@ public class LightRenderer{ } public boolean enabled(){ - return state.rules.lighting && state.rules.ambientLight.a > 0.00001f; + return state.rules.lighting && state.rules.ambientLight.a > 0.0001f; } public void draw(){ @@ -179,6 +185,8 @@ public class LightRenderer{ return; } + if(circleRegion == null) circleRegion = Core.atlas.find("circle-shadow"); + buffer.resize(Core.graphics.getWidth()/scaling, Core.graphics.getHeight()/scaling); Draw.color(); @@ -188,6 +196,11 @@ public class LightRenderer{ for(Runnable run : lights){ run.run(); } + for(int i = 0; i < circleIndex; i++){ + var cir = circles.items[i]; + Draw.color(cir.color); + Draw.rect(circleRegion, cir.x, cir.y, cir.radius * 2, cir.radius * 2); + } Draw.reset(); buffer.end(); Gl.blendEquationSeparate(Gl.funcAdd, Gl.funcAdd); @@ -197,5 +210,17 @@ public class LightRenderer{ buffer.blit(Shaders.light); lights.clear(); + circleIndex = 0; + } + + static class CircleLight{ + float x, y, color, radius; + + public void set(float x, float y, float color, float radius){ + this.x = x; + this.y = y; + this.color = color; + this.radius = radius; + } } } diff --git a/core/src/mindustry/graphics/LoadRenderer.java b/core/src/mindustry/graphics/LoadRenderer.java index 8a314b00df..c15e948285 100644 --- a/core/src/mindustry/graphics/LoadRenderer.java +++ b/core/src/mindustry/graphics/LoadRenderer.java @@ -37,6 +37,7 @@ public class LoadRenderer implements Disposable{ private int lastLength = -1; private FxProcessor fx; private WindowedMean renderTimes = new WindowedMean(20); + private BloomFilter bloom; private long lastFrameTime; { @@ -49,7 +50,7 @@ public class LoadRenderer implements Disposable{ //vignetting is probably too much //fx.addEffect(new VignettingFilter(false)); - fx.addEffect(new BloomFilter()); + fx.addEffect(bloom = new BloomFilter()); bars = new Bar[]{ new Bar("s_proc#", OS.cores / 16f, OS.cores < 4), @@ -69,6 +70,7 @@ public class LoadRenderer implements Disposable{ public void dispose(){ mesh.dispose(); fx.dispose(); + bloom.dispose(); } public void draw(){ diff --git a/core/src/mindustry/graphics/MenuRenderer.java b/core/src/mindustry/graphics/MenuRenderer.java index b44b6cc5e7..dbbf75f06a 100644 --- a/core/src/mindustry/graphics/MenuRenderer.java +++ b/core/src/mindustry/graphics/MenuRenderer.java @@ -12,7 +12,6 @@ import arc.util.*; import arc.util.noise.*; import mindustry.content.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.environment.*; @@ -30,7 +29,7 @@ public class MenuRenderer implements Disposable{ private float time = 0f; private float flyerRot = 45f; private int flyers = Mathf.chance(0.2) ? Mathf.random(35) : Mathf.random(15); - private UnitType flyerType = Structs.select(UnitTypes.flare, UnitTypes.flare, UnitTypes.horizon, UnitTypes.mono, UnitTypes.poly, UnitTypes.mega, UnitTypes.zenith); + private UnitType flyerType = content.units().select(u -> u.hitSize <= 20f && u.flying && u.region.found()).random(); public MenuRenderer(){ Time.mark(); @@ -42,7 +41,7 @@ public class MenuRenderer implements Disposable{ private void generate(){ world.beginMapLoad(); Tiles tiles = world.resize(width, height); - Seq ores = content.blocks().select(b -> b instanceof OreBlock); + Seq ores = content.blocks().select(b -> b instanceof OreBlock && !(b instanceof WallOreBlock)); shadows = new FrameBuffer(width, height); int offset = Mathf.random(100000); Simplex s1 = new Simplex(offset); @@ -239,7 +238,7 @@ public class MenuRenderer implements Disposable{ private void drawFlyers(){ Draw.color(0f, 0f, 0f, 0.4f); - TextureRegion icon = flyerType.icon(Cicon.full); + TextureRegion icon = flyerType.fullIcon; float size = Math.max(icon.width, icon.height) * Draw.scl * 1.6f; @@ -275,10 +274,12 @@ public class MenuRenderer implements Disposable{ float offset = -100f; for(int i = 0; i < flyers; i++){ - Tmp.v1.trns(flyerRot, time * (2f + flyerType.speed)); + Tmp.v1.trns(flyerRot, time * (flyerType.speed)); - cons.get((Mathf.randomSeedRange(i, range) + Tmp.v1.x + Mathf.absin(time + Mathf.randomSeedRange(i + 2, 500), 10f, 3.4f) + offset) % (tw + Mathf.randomSeed(i + 5, 0, 500)), - (Mathf.randomSeedRange(i + 1, range) + Tmp.v1.y + Mathf.absin(time + Mathf.randomSeedRange(i + 3, 500), 10f, 3.4f) + offset) % th); + cons.get( + (Mathf.randomSeedRange(i, range) + Tmp.v1.x + Mathf.absin(time + Mathf.randomSeedRange(i + 2, 500), 10f, 3.4f) + offset) % (tw + Mathf.randomSeed(i + 5, 0, 500)), + (Mathf.randomSeedRange(i + 1, range) + Tmp.v1.y + Mathf.absin(time + Mathf.randomSeedRange(i + 3, 500), 10f, 3.4f) + offset) % th + ); } } diff --git a/core/src/mindustry/graphics/MinimapRenderer.java b/core/src/mindustry/graphics/MinimapRenderer.java index 4cc044753e..a77face3f4 100644 --- a/core/src/mindustry/graphics/MinimapRenderer.java +++ b/core/src/mindustry/graphics/MinimapRenderer.java @@ -2,7 +2,6 @@ package mindustry.graphics; import arc.*; import arc.graphics.*; -import arc.graphics.Pixmap.*; import arc.graphics.g2d.*; import arc.math.*; import arc.math.geom.*; @@ -36,6 +35,7 @@ public class MinimapRenderer{ //make sure to call on the graphics thread Events.on(TileChangeEvent.class, event -> { + //TODO don't update when the minimap is off? if(!ui.editor.isShown()){ update(event.tile); } @@ -69,7 +69,7 @@ public class MinimapRenderer{ texture.dispose(); } setZoom(4f); - pixmap = new Pixmap(world.width(), world.height(), Format.rgba8888); + pixmap = new Pixmap(world.width(), world.height()); texture = new Texture(pixmap); region = new TextureRegion(texture); } @@ -96,7 +96,7 @@ public class MinimapRenderer{ Draw.mixcol(unit.team().color, 1f); float scale = Scl.scl(1f) / 2f * scaling * 32f; - var region = unit.type.icon(Cicon.full); + var region = unit.type.fullIcon; Draw.rect(region, x + rx, y + ry, scale, scale * (float)region.height / region.width, unit.rotation() - 90); Draw.reset(); } @@ -136,7 +136,7 @@ public class MinimapRenderer{ public void updateAll(){ for(Tile tile : world.tiles){ - pixmap.draw(tile.x, pixmap.getHeight() - 1 - tile.y, colorFor(tile)); + pixmap.set(tile.x, pixmap.height - 1 - tile.y, colorFor(tile)); } texture.draw(pixmap); } @@ -144,10 +144,10 @@ public class MinimapRenderer{ public void update(Tile tile){ if(world.isGenerating() || !state.isGame()) return; - int color = colorFor(world.tile(tile.x, tile.y)); - pixmap.draw(tile.x, pixmap.getHeight() - 1 - tile.y, color); + int color = colorFor(tile); + pixmap.set(tile.x, pixmap.height - 1 - tile.y, color); - Pixmaps.drawPixel(texture, tile.x, pixmap.getHeight() - 1 - tile.y, color); + Pixmaps.drawPixel(texture, tile.x, pixmap.height - 1 - tile.y, color); } public void updateUnitArray(){ diff --git a/core/src/mindustry/graphics/MultiPacker.java b/core/src/mindustry/graphics/MultiPacker.java index ac8277b078..eb823ea991 100644 --- a/core/src/mindustry/graphics/MultiPacker.java +++ b/core/src/mindustry/graphics/MultiPacker.java @@ -1,21 +1,40 @@ package mindustry.graphics; import arc.graphics.*; -import arc.graphics.Pixmap.*; import arc.graphics.Texture.*; import arc.graphics.g2d.*; import arc.util.*; +import mindustry.*; public class MultiPacker implements Disposable{ private PixmapPacker[] packers = new PixmapPacker[PageType.all.length]; public MultiPacker(){ for(int i = 0; i < packers.length; i++){ - int pageSize = 2048; - packers[i] = new PixmapPacker(pageSize, pageSize, Format.rgba8888, 2, true); + packers[i] = new PixmapPacker(Math.min(Vars.maxTextureSize, PageType.all[i].width), Math.min(Vars.maxTextureSize, PageType.all[i].height), 2, true); } } + @Nullable + public PixmapRegion get(String name){ + for(var packer : packers){ + var region = packer.getRegion(name); + if(region != null){ + return region; + } + } + return null; + } + + public boolean has(String name){ + for(var page : PageType.all){ + if(packers[page.ordinal()].getRect(name) != null){ + return true; + } + } + return false; + } + public boolean has(PageType type, String name){ return packers[type.ordinal()].getRect(name) != null; } @@ -24,6 +43,10 @@ public class MultiPacker implements Disposable{ packers[type.ordinal()].pack(name, region); } + public void add(PageType type, String name, PixmapRegion region, int[] splits, int[] pads){ + packers[type.ordinal()].pack(name, region, splits, pads); + } + public void add(PageType type, String name, Pixmap pix){ packers[type.ordinal()].pack(name, pix); } @@ -50,11 +73,27 @@ public class MultiPacker implements Disposable{ //zone page (sprites4.png) - zone previews //ui page (sprites5.png) - content icons, white icons and UI elements public enum PageType{ - main, + main(4096), environment, - editor, - ui; + editor(4096, 2048), + rubble, + ui(4096); public static final PageType[] all = values(); + + public int width = 2048, height = 2048; + + PageType(int defaultSize){ + this.width = this.height = defaultSize; + } + + PageType(int width, int height){ + this.width = width; + this.height = height; + } + + PageType(){ + + } } } diff --git a/core/src/mindustry/graphics/OverlayRenderer.java b/core/src/mindustry/graphics/OverlayRenderer.java index d811af6fbc..5279de9e4d 100644 --- a/core/src/mindustry/graphics/OverlayRenderer.java +++ b/core/src/mindustry/graphics/OverlayRenderer.java @@ -8,6 +8,7 @@ import arc.math.geom.*; import arc.util.*; import mindustry.*; import mindustry.ai.types.*; +import mindustry.entities.*; import mindustry.gen.*; import mindustry.input.*; import mindustry.ui.*; @@ -21,7 +22,7 @@ public class OverlayRenderer{ private static final Rect rect = new Rect(); private float buildFade, unitFade; - private Unit lastSelect; + private Sized lastSelect; public void drawBottom(){ InputHandler input = control.input; @@ -72,27 +73,31 @@ public class OverlayRenderer{ InputHandler input = control.input; - Unit select = input.selectedUnit(); + + Sized select = input.selectedUnit(); + if(select == null) select = input.selectedControlBuild(); if(!Core.input.keyDown(Binding.control)) select = null; + unitFade = Mathf.lerpDelta(unitFade, Mathf.num(select != null), 0.1f); if(select != null) lastSelect = select; if(select == null) select = lastSelect; - if(select != null && select.isAI()){ + if(select != null && (!(select instanceof Unitc u) || u.isAI())){ Draw.mixcol(Pal.accent, 1f); Draw.alpha(unitFade); + Building build = (select instanceof BlockUnitc b ? b.tile() : select instanceof Building b ? b : null); - if(select instanceof BlockUnitc){ + if(build != null){ //special selection for block "units" - Fill.square(select.x, select.y, ((BlockUnitc)select).tile().block.size * tilesize/2f); - }else{ - Draw.rect(select.type.icon(Cicon.full), select.x(), select.y(), select.rotation() - 90); + Fill.square(build.x, build.y, build.block.size * tilesize/2f); + }else if(select instanceof Unit u){ + Draw.rect(u.type.fullIcon, u.x, u.y, u.rotation - 90); } for(int i = 0; i < 4; i++){ float rot = i * 90f + 45f + (-Time.time) % 360f; float length = select.hitSize() * 1.5f + (unitFade * 2.5f); - Draw.rect("select-arrow", select.x + Angles.trnsx(rot, length), select.y + Angles.trnsy(rot, length), length / 1.9f, length / 1.9f, rot - 135f); + Draw.rect("select-arrow", select.getX() + Angles.trnsx(rot, length), select.getY() + Angles.trnsy(rot, length), length / 1.9f, length / 1.9f, rot - 135f); } Draw.reset(); @@ -159,7 +164,8 @@ public class OverlayRenderer{ input.drawOverSelect(); - if(ui.hudfrag.blockfrag.hover() instanceof Unit unit && unit.controller() instanceof LogicAI ai && ai.controller instanceof Building build && build.isValid()){ + if(ui.hudfrag.blockfrag.hover() instanceof Unit unit && unit.controller() instanceof LogicAI ai && ai.controller != null && ai.controller.isValid()){ + var build = ai.controller; Drawf.square(build.x, build.y, build.block.size * tilesize/2f + 2f); if(!unit.within(build, unit.hitSize * 2f)){ Drawf.arrow(unit.x, unit.y, build.x, build.y, unit.hitSize *2f, 4f); @@ -170,7 +176,7 @@ public class OverlayRenderer{ if(input.isDroppingItem()){ Vec2 v = Core.input.mouseWorld(input.getMouseX(), input.getMouseY()); float size = 8; - Draw.rect(player.unit().item().icon(Cicon.medium), v.x, v.y, size, size); + Draw.rect(player.unit().item().fullIcon, v.x, v.y, size, size); Draw.color(Pal.accent); Lines.circle(v.x, v.y, 6 + Mathf.absin(Time.time, 5f, 1f)); Draw.reset(); diff --git a/core/src/mindustry/graphics/Pal.java b/core/src/mindustry/graphics/Pal.java index 1b2b10ff94..012e5a0aff 100644 --- a/core/src/mindustry/graphics/Pal.java +++ b/core/src/mindustry/graphics/Pal.java @@ -14,10 +14,12 @@ public class Pal{ sapBullet = Color.valueOf("bf92f9"), sapBulletBack = Color.valueOf("6d56bf"), + reactorPurple = Color.valueOf("bf92f9"), + reactorPurple2 = Color.valueOf("8a73c6"), + spore = Color.valueOf("7457ce"), shield = Color.valueOf("ffd37f").a(0.7f), - shieldIn = Color.black.cpy().a(0f), bulletYellow = Color.valueOf("fff8e8"), bulletYellowBack = Color.valueOf("f9c27a"), diff --git a/core/src/mindustry/graphics/ScorchGenerator.java b/core/src/mindustry/graphics/ScorchGenerator.java deleted file mode 100644 index b7dfd15439..0000000000 --- a/core/src/mindustry/graphics/ScorchGenerator.java +++ /dev/null @@ -1,31 +0,0 @@ -package mindustry.graphics; - -import arc.graphics.*; -import arc.math.*; -import arc.util.noise.*; - -/** Generates a scorch pixmap based on parameters. Thread safe, unless multiple scorch generators are running in parallel. */ -public class ScorchGenerator{ - private static final Simplex sim = new Simplex(); - - public int size = 80, seed = 0, color = Color.white.rgba(); - public double scale = 18, pow = 2, octaves = 4, pers = 0.4, add = 2, nscl = 4.5f; - - public Pixmap generate(){ - Pixmap pix = new Pixmap(size, size); - sim.setSeed(seed); - - pix.each((x, y) -> { - double dst = Mathf.dst(x, y, size/2, size/2) / (size / 2f); - double scaled = Math.abs(dst - 0.5f) * 5f + add; - scaled -= noise(Angles.angle(x, y, size/2, size/2))*nscl; - if(scaled < 1.5f) pix.draw(x, y, color); - }); - - return pix; - } - - private double noise(float angle){ - return Math.pow(sim.octaveNoise2D(octaves, pers, 1 / scale, Angles.trnsx(angle, size/2f) + size/2f, Angles.trnsy(angle, size/2f) + size/2f), pow); - } -} diff --git a/core/src/mindustry/graphics/Shaders.java b/core/src/mindustry/graphics/Shaders.java index 8aa304065f..75d61a2178 100644 --- a/core/src/mindustry/graphics/Shaders.java +++ b/core/src/mindustry/graphics/Shaders.java @@ -21,7 +21,7 @@ public class Shaders{ public static UnitBuildShader build; public static DarknessShader darkness; public static LightShader light; - public static SurfaceShader water, mud, tar, slag, space; + public static SurfaceShader water, mud, tar, slag, space, caustics; public static PlanetShader planet; public static PlanetGridShader planetGrid; public static AtmosphereShader atmosphere; @@ -48,6 +48,12 @@ public class Shaders{ tar = new SurfaceShader("tar"); slag = new SurfaceShader("slag"); space = new SpaceShader("space"); + caustics = new SurfaceShader("caustics"){ + @Override + public String textureName(){ + return "caustics"; + } + }; planet = new PlanetShader(); planetGrid = new PlanetGridShader(); atmosphere = new AtmosphereShader(); @@ -249,6 +255,8 @@ public class Shaders{ } public static class SurfaceShader extends Shader{ + Texture noiseTex; + public SurfaceShader(String frag){ super(getShaderFi("screenspace.vert"), getShaderFi(frag + ".frag")); loadNoise(); @@ -259,8 +267,12 @@ public class Shaders{ loadNoise(); } + public String textureName(){ + return "noise"; + } + public void loadNoise(){ - Core.assets.load("sprites/noise.png", Texture.class).loaded = t -> { + Core.assets.load("sprites/" + textureName() + ".png", Texture.class).loaded = t -> { ((Texture)t).setFilter(TextureFilter.linear); ((Texture)t).setWrap(TextureWrap.repeat); }; @@ -273,7 +285,11 @@ public class Shaders{ setUniformf("u_time", Time.time); if(hasUniform("u_noise")){ - Core.assets.get("sprites/noise.png", Texture.class).bind(1); + if(noiseTex == null){ + noiseTex = Core.assets.get("sprites/" + textureName() + ".png", Texture.class); + } + + noiseTex.bind(1); renderer.effectBuffer.getTexture().bind(0); setUniformi("u_noise", 1); diff --git a/core/src/mindustry/graphics/Trail.java b/core/src/mindustry/graphics/Trail.java index fd5e36eb25..fc998e64ea 100644 --- a/core/src/mindustry/graphics/Trail.java +++ b/core/src/mindustry/graphics/Trail.java @@ -3,51 +3,101 @@ package mindustry.graphics; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; -import arc.math.geom.*; import arc.struct.*; -import arc.util.pooling.*; +import arc.util.*; public class Trail{ public int length; - private final Seq points; - private float lastX = -1, lastY = -1; + private final FloatSeq points; + private float lastX = -1, lastY = -1, lastAngle = -1, counter = 0f; public Trail(int length){ this.length = length; - points = new Seq<>(length); + points = new FloatSeq(length*3); + } + + public Trail copy(){ + Trail out = new Trail(length); + out.points.addAll(points); + out.lastX = lastX; + out.lastY = lastY; + out.lastAngle = lastAngle; + return out; } public void clear(){ points.clear(); } + public int size(){ + return points.size/3; + } + + public void drawCap(Color color, float width){ + if(points.size > 0){ + Draw.color(color); + float[] items = points.items; + int i = points.size - 3; + float x1 = items[i], y1 = items[i + 1], w1 = items[i + 2], w = w1 * width / (points.size/3) * i/3f * 2f; + Draw.rect("hcircle", x1, y1, w, w, -Mathf.radDeg * lastAngle + 180f); + Draw.reset(); + } + } + public void draw(Color color, float width){ Draw.color(color); + float[] items = points.items; + float lx = lastX, ly = lastY, lastAngle = this.lastAngle; - for(int i = 0; i < points.size - 1; i++){ - Vec3 c = points.get(i); - Vec3 n = points.get(i + 1); - float size = width / length; + for(int i = 0; i < points.size - 3; i+= 3){ + float x1 = items[i], y1 = items[i + 1], w1 = items[i + 2], + x2 = items[i + 3], y2 = items[i + 4], w2 = items[i + 5]; + float size = width / (points.size/3); + float z1 = lastAngle; + float z2 = -Angles.angleRad(x2, y2, lx, ly); - float cx = Mathf.sin(c.z) * i * size, cy = Mathf.cos(c.z) * i * size, nx = Mathf.sin(n.z) * (i + 1) * size, ny = Mathf.cos(n.z) * (i + 1) * size; - Fill.quad(c.x - cx, c.y - cy, c.x + cx, c.y + cy, n.x + nx, n.y + ny, n.x - nx, n.y - ny); + float cx = Mathf.sin(z1) * i/3f * size * w1, cy = Mathf.cos(z1) * i/3f * size * w1, + nx = Mathf.sin(z2) * (i/3f + 1) * size * w2, ny = Mathf.cos(z2) * (i/3f + 1) * size * w2; + Fill.quad(x1 - cx, y1 - cy, x1 + cx, y1 + cy, x2 + nx, y2 + ny, x2 - nx, y2 - ny); + + lastAngle = z2; + lx = x2; + ly = y2; } Draw.reset(); } - public void update(float x, float y){ - if(points.size > length){ - Pools.free(points.first()); - points.remove(0); + /** Removes the last point from the trail at intervals. */ + public void shorten(){ + if((counter += Time.delta) >= 0.99f){ + if(points.size >= 3){ + points.removeRange(0, 2); + } } + } - float angle = -Angles.angle(x, y, lastX, lastY); + /** Adds a new point to the trail at intervals. */ + public void update(float x, float y){ + update(x, y, 1f); + } - points.add(Pools.obtain(Vec3.class, Vec3::new).set(x, y, (angle) * Mathf.degRad)); + /** Adds a new point to the trail at intervals. */ + public void update(float x, float y, float width){ + if((counter += Time.delta) >= 0.99f){ + if(points.size > length*3){ + points.removeRange(0, 2); + } - lastX = x; - lastY = y; + lastAngle = -Angles.angleRad(x, y, lastX, lastY); + + points.add(x, y, width); + + lastX = x; + lastY = y; + + counter = 0f; + } } } diff --git a/core/src/mindustry/graphics/g3d/PlanetRenderer.java b/core/src/mindustry/graphics/g3d/PlanetRenderer.java index 7e84e7a8bb..3401ba8b3d 100644 --- a/core/src/mindustry/graphics/g3d/PlanetRenderer.java +++ b/core/src/mindustry/graphics/g3d/PlanetRenderer.java @@ -94,8 +94,12 @@ public class PlanetRenderer implements Disposable{ cam.position.setZero(); cam.update(); + Gl.depthMask(false); + skybox.render(cam.combined); + Gl.depthMask(true); + cam.position.set(lastPos); cam.update(); @@ -172,7 +176,7 @@ public class PlanetRenderer implements Disposable{ } public void renderOrbit(Planet planet){ - if(planet.parent == null || !planet.visible()) return; + if(planet.parent == null || !planet.visible() || orbitAlpha <= 0.02f) return; Vec3 center = planet.parent.position; float radius = planet.orbitRadius; @@ -182,6 +186,8 @@ public class PlanetRenderer implements Disposable{ } public void renderSectors(Planet planet){ + if(orbitAlpha <= 0.02f) return; + //apply transformed position batch.proj().mul(planet.getTransform(mat)); @@ -203,6 +209,7 @@ public class PlanetRenderer implements Disposable{ public void drawArc(Planet planet, Vec3 a, Vec3 b){ drawArc(planet, a, b, Pal.accent, Color.clear, 1f); } + public void drawArc(Planet planet, Vec3 a, Vec3 b, Color from, Color to, float length){ drawArc(planet, a, b, from, to, length, 80f, 25); } diff --git a/core/src/mindustry/input/DesktopInput.java b/core/src/mindustry/input/DesktopInput.java index 5c2961bc06..3746dae72e 100644 --- a/core/src/mindustry/input/DesktopInput.java +++ b/core/src/mindustry/input/DesktopInput.java @@ -162,7 +162,7 @@ public class DesktopInput extends InputHandler{ } lineRequests.each(this::drawOverRequest); }else if(isPlacing()){ - if(block.rotate){ + if(block.rotate && block.drawArrow){ drawArrow(block, cursorX, cursorY, rotation); } Draw.color(); @@ -225,17 +225,20 @@ public class DesktopInput extends InputHandler{ if(!scene.hasMouse()){ if(Core.input.keyDown(Binding.control) && Core.input.keyTap(Binding.select)){ Unit on = selectedUnit(); + var build = selectedControlBuild(); if(on != null){ Call.unitControl(player, on); shouldShoot = false; + }else if(build != null){ + Call.buildingControlSelect(player, build); } } } - if(!player.dead() && !state.isPaused() && !(Core.scene.getKeyboardFocus() instanceof TextField)){ + if(!player.dead() && !state.isPaused() && !scene.hasField()){ updateMovement(player.unit()); - if(Core.input.keyDown(Binding.respawn) && !player.unit().spawnedByCore() && !scene.hasField()){ + if(Core.input.keyDown(Binding.respawn)){ Call.unitClear(player); controlledType = null; } @@ -641,15 +644,17 @@ public class DesktopInput extends InputHandler{ unit.moveAt(movement); }else{ unit.moveAt(Tmp.v2.trns(unit.rotation, movement.len())); + + //problem: actual unit rotation is controlled by velocity, but velocity is 1) unpredictable and 2) can be set to 0 if(!movement.isZero()){ - unit.vel.rotateTo(movement.angle(), unit.type.rotateSpeed * Math.max(Time.delta, 1)); + unit.rotation = Angles.moveToward(unit.rotation, movement.angle(), unit.type.rotateSpeed * Math.max(Time.delta, 1)); } } unit.aim(unit.type.faceTarget ? Core.input.mouseWorld() : Tmp.v1.trns(unit.rotation, Core.input.mouseWorld().dst(unit)).add(unit.x, unit.y)); unit.controlWeapons(true, player.shooting && !boosted); - player.boosting = Core.input.keyDown(Binding.boost) && !movement.isZero(); + player.boosting = Core.input.keyDown(Binding.boost); player.mouseX = unit.aimX(); player.mouseY = unit.aimY(); diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index 874ade09e7..ebabd2523f 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -34,7 +34,6 @@ import mindustry.world.blocks.ConstructBlock.*; import mindustry.world.blocks.*; import mindustry.world.blocks.distribution.*; import mindustry.world.blocks.payloads.*; -import mindustry.world.blocks.storage.CoreBlock.*; import mindustry.world.meta.*; import java.util.*; @@ -44,6 +43,7 @@ import static mindustry.Vars.*; public abstract class InputHandler implements InputProcessor, GestureListener{ /** Used for dropping items. */ final static float playerSelectRange = mobile ? 17f : 11f; + final static IntSeq removed = new IntSeq(); /** Maximum line length. */ final static int maxLength = 100; final static Rect r1 = new Rect(), r2 = new Rect(); @@ -128,6 +128,27 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } } + @Remote(called = Loc.both, targets = Loc.both, forward = true, unreliable = true) + public static void deletePlans(Player player, int[] positions){ + if(netServer.admins.allowAction(player, ActionType.removePlanned, a -> a.plans = positions)){ + + var it = state.teams.get(player.team()).blocks.iterator(); + //O(n^2) search here; no way around it + outer: + while(it.hasNext()){ + BlockPlan req = it.next(); + + for(int pos : positions){ + if(req.x == Point2.x(pos) && req.y == Point2.y(pos)){ + it.remove(); + continue outer; + } + } + } + + } + } + public static void createItemTransfer(Item item, int amount, float x, float y, Position to, Runnable done){ Fx.itemTransfer.at(x, y, amount, item.color, to); if(done != null){ @@ -331,6 +352,20 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ Events.fire(new TapEvent(player, tile)); } + @Remote(targets = Loc.both, called = Loc.both, forward = true) + public static void buildingControlSelect(Player player, Building build){ + if(player == null || build == null || player.dead()) return; + + //make sure player is allowed to control the building + if(net.server() && !netServer.admins.allowAction(player, ActionType.buildSelect, action -> action.tile = build.tile)){ + throw new ValidateException(player, "Player cannot control a building."); + } + + if(player.team() == build.team && build.canControlSelect(player)){ + build.onControlSelect(player); + } + } + @Remote(targets = Loc.both, called = Loc.both, forward = true) public static void unitControl(Player player, @Nullable Unit unit){ if(player == null) return; @@ -341,16 +376,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } //clear player unit when they possess a core - if(unit instanceof BlockUnitc block && block.tile() instanceof CoreBuild build){ - Fx.spawn.at(player); - if(net.client()){ - control.input.controlledType = null; - } - - player.clearUnit(); - player.deathTimer = Player.deathDelay + 1f; - build.requestSpawn(player); - }else if(unit == null){ //just clear the unit (is this used?) + if(unit == null){ //just clear the unit (is this used?) player.clearUnit(); //make sure it's AI controlled, so players can't overwrite each other }else if(unit.isAI() && unit.team == player.team() && !unit.dead){ @@ -369,8 +395,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ @Remote(targets = Loc.both, called = Loc.both, forward = true) public static void unitClear(Player player){ - //no free core teleports? - if(player == null || !player.dead() && player.unit().spawnedByCore) return; + if(player == null) return; Fx.spawn.at(player); player.clearUnit(); @@ -379,19 +404,19 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ @Remote(targets = Loc.both, called = Loc.server, forward = true) public static void unitCommand(Player player){ - if(player == null || player.dead() || !(player.unit() instanceof Commanderc commander)) return; + if(player == null || player.dead() || (player.unit() == null)) return; //make sure player is allowed to make the command if(net.server() && !netServer.admins.allowAction(player, ActionType.command, action -> {})){ throw new ValidateException(player, "Player cannot command a unit."); } - if(commander.isCommanding()){ - commander.clearCommand(); - }else if(player.unit().type.commandLimit > 0 && player.unit().type.commandRadius > 0){ + if(player.unit().isCommanding()){ + player.unit().clearCommand(); + }else if(player.unit().type.commandLimit > 0){ //TODO try out some other formations - commander.commandNearby(new CircleFormation()); + player.unit().commandNearby(new CircleFormation()); Fx.commandSend.at(player, player.unit().type.commandRadius); } @@ -530,7 +555,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ public boolean requestMatches(BuildPlan request){ Tile tile = world.tile(request.x, request.y); - return tile != null && tile.build instanceof ConstructBuild cons && cons.cblock == request.block; + return tile != null && tile.build instanceof ConstructBuild cons && cons.current == request.block; } public void drawBreaking(int x, int y){ @@ -571,6 +596,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ int ox = schemOriginX(), oy = schemOriginY(); requests.each(req -> { + if(req.breaking) return; + req.pointConfig(p -> { int cx = p.x, cy = p.y; int lx = cx; @@ -605,6 +632,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ int origin = (x ? schemOriginX() : schemOriginY()) * tilesize; requests.each(req -> { + if(req.breaking) return; + float value = -((x ? req.x : req.y) * tilesize - origin + req.block.offset) + origin; if(x){ @@ -840,15 +869,23 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } } + removed.clear(); + //remove blocks to rebuild Iterator broken = state.teams.get(player.team()).blocks.iterator(); while(broken.hasNext()){ BlockPlan req = broken.next(); Block block = content.block(req.block); if(block.bounds(req.x, req.y, Tmp.r2).overlaps(Tmp.r1)){ + removed.add(Point2.pack(req.x, req.y)); broken.remove(); } } + + //TODO array may be too large? + if(removed.size > 0 && net.active()){ + Call.deletePlans(player, removed.toArray()); + } } protected void updateLine(int x1, int y1, int x2, int y2){ @@ -1041,6 +1078,14 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ return null; } + public @Nullable Building selectedControlBuild(){ + Building build = world.buildWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y); + if(build != null && !player.dead() && build.canControlSelect(player) && build.team == player.team()){ + return build; + } + return null; + } + public void remove(){ Core.input.removeProcessor(this); frag.remove(); @@ -1218,13 +1263,15 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ line.x = point.x; line.y = point.y; if(!overrideLineRotation || diagonal){ + int result = baseRotation; if(next != null){ - line.rotation = Tile.relativeTo(point.x, point.y, next.x, next.y); + result = Tile.relativeTo(point.x, point.y, next.x, next.y); }else if(block.conveyorPlacement && i > 0){ Point2 prev = points.get(i - 1); - line.rotation = Tile.relativeTo(prev.x, prev.y, point.x, point.y); - }else{ - line.rotation = baseRotation; + result = Tile.relativeTo(prev.x, prev.y, point.x, point.y); + } + if(result != -1){ + line.rotation = result; } }else{ line.rotation = rotation; diff --git a/core/src/mindustry/input/MobileInput.java b/core/src/mindustry/input/MobileInput.java index 6958e3fa0b..d037a0d351 100644 --- a/core/src/mindustry/input/MobileInput.java +++ b/core/src/mindustry/input/MobileInput.java @@ -59,20 +59,22 @@ public class MobileInput extends InputHandler implements GestureListener{ /** Current place mode. */ public PlaceMode mode = none; /** Whether no recipe was available when switching to break mode. */ - public Block lastBlock; + public @Nullable Block lastBlock; /** Last placed request. Used for drawing block overlay. */ - public BuildPlan lastPlaced; + public @Nullable BuildPlan lastPlaced; /** Down tracking for panning. */ public boolean down = false; /** Whether manual shooting (point with finger) is enabled. */ public boolean manualShooting = false; /** Current thing being shot at. */ - public Teamc target; + public @Nullable Teamc target; /** Payload target being moved to. Can be a position (for dropping), or a unit/block. */ - public Position payloadTarget; + public @Nullable Position payloadTarget; /** Unit last tapped, or null if last tap was not on a unit. */ - public Unit unitTapped; + public @Nullable Unit unitTapped; + /** Control building last tapped. */ + public @Nullable Building buildingTapped; //region utility methods @@ -362,7 +364,7 @@ public class MobileInput extends InputHandler implements GestureListener{ } //draw last placed request - if(!request.breaking && request == lastPlaced && request.block != null){ + if(!request.breaking && request == lastPlaced && request.block != null && request.block.drawArrow){ Draw.mixcol(); request.block.drawPlace(tile.x, tile.y, rotation, validPlace(tile.x, tile.y, request.block, rotation)); } @@ -530,7 +532,7 @@ public class MobileInput extends InputHandler implements GestureListener{ }else{ Building build = world.buildWorld(pos.x, pos.y); - if(build != null && build.team == player.team() && pay.canPickup(build)){ + if(build != null && build.team == player.team() && (pay.canPickup(build) || build.getPayload() != null && pay.canPickupPayload(build.getPayload()))){ payloadTarget = build; }else if(pay.hasPayload()){ //drop off at position @@ -612,6 +614,8 @@ public class MobileInput extends InputHandler implements GestureListener{ //control a unit/block detected on first tap of double-tap if(unitTapped != null){ Call.unitControl(player, unitTapped); + }else if(buildingTapped != null){ + Call.buildingControlSelect(player, buildingTapped); }else if(!tryBeginMine(cursor)){ tileTapped(linked.build); } @@ -620,6 +624,7 @@ public class MobileInput extends InputHandler implements GestureListener{ } unitTapped = selectedUnit(); + buildingTapped = selectedControlBuild(); //prevent mining if placing/breaking blocks if(!tryStopMine() && !canTapPlayer(worldx, worldy) && !tileTapped(linked.build) && mode == none && !Core.settings.getBool("doubletapmine")){ tryBeginMine(cursor); @@ -885,7 +890,7 @@ public class MobileInput extends InputHandler implements GestureListener{ if(payloadTarget instanceof Vec2 && pay.hasPayload()){ //vec -> dropping something tryDropPayload(); - }else if(payloadTarget instanceof Building build && pay.canPickup(build)){ + }else if(payloadTarget instanceof Building build && build.team == unit.team){ //building -> picking building up Call.requestBuildPayload(player, build); }else if(payloadTarget instanceof Unit other && pay.canPickup(other)){ diff --git a/core/src/mindustry/input/Placement.java b/core/src/mindustry/input/Placement.java index 99091fc7ef..f4afb96683 100644 --- a/core/src/mindustry/input/Placement.java +++ b/core/src/mindustry/input/Placement.java @@ -169,6 +169,62 @@ public class Placement{ plans.set(result); } + public static void calculateDuctBridges(Seq plans, DuctBridge bridge){ + //check for orthogonal placement + unlocked state + if(!(plans.first().x == plans.peek().x || plans.first().y == plans.peek().y) || !bridge.unlockedNow()){ + return; + } + + Boolf placeable = plan -> (plan.placeable(player.team())) || + (plan.tile() != null && plan.tile().block() == plan.block); //don't count the same block as inaccessible + + var result = plans1.clear(); + var team = player.team(); + var rot = plans.first().rotation; + + outer: + for(int i = 0; i < plans.size;){ + var cur = plans.get(i); + result.add(cur); + + //gap found + if(i < plans.size - 1 && placeable.get(cur) && !placeable.get(plans.get(i + 1))){ + + //find the closest valid position within range + for(int j = i + 1; j < plans.size; j++){ + var other = plans.get(j); + + //out of range now, set to current position and keep scanning forward for next occurrence + if(!bridge.positionsValid(cur.x, cur.y, other.x, other.y)){ + //add 'missed' conveyors + for(int k = i + 1; k < j; k++){ + result.add(plans.get(k)); + } + i = j; + continue outer; + }else if(other.placeable(team)){ + //found a link, assign bridges + cur.block = bridge; + other.block = bridge; + + i = j; + continue outer; + } + } + + //if it got here, that means nothing was found. this likely means there's a bunch of stuff at the end; add it and bail out + for(int j = i + 1; j < plans.size; j++){ + result.add(plans.get(j)); + } + break; + }else{ + i ++; + } + } + + plans.set(result); + } + private static float tileHeuristic(Tile tile, Tile other){ Block block = control.input.block; diff --git a/core/src/mindustry/io/JsonIO.java b/core/src/mindustry/io/JsonIO.java index d6ef8f8e17..ea15b3969c 100644 --- a/core/src/mindustry/io/JsonIO.java +++ b/core/src/mindustry/io/JsonIO.java @@ -192,11 +192,12 @@ public class JsonIO{ json.setSerializer(UnlockableContent.class, new Serializer<>(){ @Override public void write(Json json, UnlockableContent object, Class knownType){ - json.writeValue(object.name); + json.writeValue(object == null ? null : object.name); } @Override public UnlockableContent read(Json json, JsonValue jsonData, Class type){ + if(jsonData.isNull()) return null; String str = jsonData.asString(); Item item = Vars.content.getByName(ContentType.item, str); Liquid liquid = Vars.content.getByName(ContentType.liquid, str); diff --git a/core/src/mindustry/io/MapIO.java b/core/src/mindustry/io/MapIO.java index c484524709..d7c6c8a49f 100644 --- a/core/src/mindustry/io/MapIO.java +++ b/core/src/mindustry/io/MapIO.java @@ -2,7 +2,6 @@ package mindustry.io; import arc.files.*; import arc.graphics.*; -import arc.graphics.Pixmap.*; import arc.math.geom.*; import arc.struct.*; import arc.util.io.*; @@ -84,8 +83,8 @@ public class MapIO{ int c = colorFor(block(), Blocks.air, Blocks.air, team()); if(c != black){ - walls.draw(x, floors.getHeight() - 1 - y, c); - floors.draw(x, floors.getHeight() - 1 - y + 1, shade); + walls.setRaw(x, floors.height - 1 - y, c); + floors.set(x, floors.height - 1 - y + 1, shade); } } }; @@ -112,7 +111,7 @@ public class MapIO{ for(int dx = 0; dx < size; dx++){ for(int dy = 0; dy < size; dy++){ int drawx = tile.x + dx + offsetx, drawy = tile.y + dy + offsety; - walls.draw(drawx, floors.getHeight() - 1 - drawy, c); + walls.set(drawx, floors.height - 1 - drawy, c); } } @@ -132,9 +131,9 @@ public class MapIO{ @Override public Tile create(int x, int y, int floorID, int overlayID, int wallID){ if(overlayID != 0){ - floors.draw(x, floors.getHeight() - 1 - y, colorFor(Blocks.air, Blocks.air, content.block(overlayID), Team.derelict)); + floors.set(x, floors.height - 1 - y, colorFor(Blocks.air, Blocks.air, content.block(overlayID), Team.derelict)); }else{ - floors.draw(x, floors.getHeight() - 1 - y, colorFor(Blocks.air, content.block(floorID), Blocks.air, Team.derelict)); + floors.set(x, floors.height - 1 - y, colorFor(Blocks.air, content.block(floorID), Blocks.air, Team.derelict)); } if(content.block(overlayID) == Blocks.spawn){ map.spawns ++; @@ -143,7 +142,7 @@ public class MapIO{ } })); - floors.drawPixmap(walls, 0, 0); + floors.draw(walls, true); walls.dispose(); return floors; }finally{ @@ -152,11 +151,11 @@ public class MapIO{ } public static Pixmap generatePreview(Tiles tiles){ - Pixmap pixmap = new Pixmap(tiles.width, tiles.height, Format.rgba8888); - for(int x = 0; x < pixmap.getWidth(); x++){ - for(int y = 0; y < pixmap.getHeight(); y++){ + Pixmap pixmap = new Pixmap(tiles.width, tiles.height); + for(int x = 0; x < pixmap.width; x++){ + for(int y = 0; y < pixmap.height; y++){ Tile tile = tiles.getn(x, y); - pixmap.draw(x, pixmap.getHeight() - 1 - y, colorFor(tile.block(), tile.floor(), tile.overlay(), tile.team())); + pixmap.set(x, pixmap.height - 1 - y, colorFor(tile.block(), tile.floor(), tile.overlay(), tile.team())); } } return pixmap; @@ -175,14 +174,14 @@ public class MapIO{ //while synthetic blocks are possible, most of their data is lost, so in order to avoid questions like //"why is there air under my drill" and "why are all my conveyors facing right", they are disabled int color = tile.block().hasColor && !tile.block().synthetic() ? tile.block().mapColor.rgba() : tile.floor().mapColor.rgba(); - pix.draw(tile.x, tiles.height - 1 - tile.y, color); + pix.set(tile.x, tiles.height - 1 - tile.y, color); } return pix; } public static void readImage(Pixmap pixmap, Tiles tiles){ for(Tile tile : tiles){ - int color = pixmap.getPixel(tile.x, pixmap.getHeight() - 1 - tile.y); + int color = pixmap.get(tile.x, pixmap.height - 1 - tile.y); Block block = ColorMapper.get(color); if(block.isFloor()){ diff --git a/core/src/mindustry/io/SaveFileReader.java b/core/src/mindustry/io/SaveFileReader.java index 69e1ea67cc..178980207f 100644 --- a/core/src/mindustry/io/SaveFileReader.java +++ b/core/src/mindustry/io/SaveFileReader.java @@ -102,8 +102,8 @@ public abstract class SaveFileReader{ if(!isByte){ output.writeInt(length); }else{ - if(length > Short.MAX_VALUE){ - throw new IOException("Byte write length exceeded: " + length + " > " + Short.MAX_VALUE); + if(length > 65535){ + throw new IOException("Byte write length exceeded: " + length + " > 65535"); } output.writeShort(length); } diff --git a/core/src/mindustry/io/SaveIO.java b/core/src/mindustry/io/SaveIO.java index 8b56ef2a61..e6d04d5ddb 100644 --- a/core/src/mindustry/io/SaveIO.java +++ b/core/src/mindustry/io/SaveIO.java @@ -18,10 +18,10 @@ import java.util.zip.*; import static mindustry.Vars.*; public class SaveIO{ - /** Format header. This is the string 'MSAV' in ASCII. */ - public static final byte[] header = {77, 83, 65, 86}; + /** Save format header. */ + public static final byte[] header = {'M', 'S', 'A', 'V'}; public static final IntMap versions = new IntMap<>(); - public static final Seq versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4()); + public static final Seq versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5()); static{ for(SaveVersion version : versionArray){ diff --git a/core/src/mindustry/io/SavePreviewLoader.java b/core/src/mindustry/io/SavePreviewLoader.java index bf01424a11..46f51216b1 100644 --- a/core/src/mindustry/io/SavePreviewLoader.java +++ b/core/src/mindustry/io/SavePreviewLoader.java @@ -1,14 +1,14 @@ package mindustry.io; +import arc.*; import arc.assets.*; import arc.assets.loaders.*; -import arc.assets.loaders.resolvers.*; import arc.files.*; public class SavePreviewLoader extends TextureLoader{ public SavePreviewLoader(){ - super(new AbsoluteFileHandleResolver()); + super(Core.files::absolute); } @Override diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index 58d9d77d53..90fd35a59d 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -1,6 +1,7 @@ package mindustry.io; import arc.*; +import arc.func.*; import arc.math.geom.*; import arc.struct.*; import arc.util.*; @@ -12,10 +13,11 @@ import mindustry.ctype.*; import mindustry.game.*; import mindustry.game.Teams.*; import mindustry.gen.*; -import mindustry.maps.*; +import mindustry.maps.Map; import mindustry.world.*; import java.io.*; +import java.util.*; import static mindustry.Vars.*; @@ -24,6 +26,9 @@ public abstract class SaveVersion extends SaveFileReader{ //HACK stores the last read build of the save file, valid after read meta call protected int lastReadBuild; + //stores entity mappings for use after readEntityMapping + //if null, fall back to EntityMapping's values + protected @Nullable Prov[] entityMapping; public SaveVersion(int version){ this.version = version; @@ -284,7 +289,7 @@ public abstract class SaveVersion extends SaveFileReader{ } } - public void writeEntities(DataOutput stream) throws IOException{ + public void writeTeamBlocks(DataOutput stream) throws IOException{ //write team data with entities. Seq data = state.teams.getActive().copy(); if(!data.contains(Team.sharded.data())) data.add(Team.sharded.data()); @@ -300,7 +305,9 @@ public abstract class SaveVersion extends SaveFileReader{ TypeIO.writeObject(Writes.get(stream), block.config); } } + } + public void writeWorldEntities(DataOutput stream) throws IOException{ stream.writeInt(Groups.all.count(Entityc::serialize)); for(Entityc entity : Groups.all){ if(!entity.serialize()) continue; @@ -312,7 +319,21 @@ public abstract class SaveVersion extends SaveFileReader{ } } - public void readEntities(DataInput stream) throws IOException{ + public void writeEntityMapping(DataOutput stream) throws IOException{ + stream.writeShort(EntityMapping.customIdMap.size); + for(var entry : EntityMapping.customIdMap.entries()){ + stream.writeShort(entry.key); + stream.writeUTF(entry.value); + } + } + + public void writeEntities(DataOutput stream) throws IOException{ + writeEntityMapping(stream); + writeTeamBlocks(stream); + writeWorldEntities(stream); + } + + public void readTeamBlocks(DataInput stream) throws IOException{ int teamc = stream.readInt(); for(int i = 0; i < teamc; i++){ @@ -333,23 +354,48 @@ public abstract class SaveVersion extends SaveFileReader{ } } } + } + + public void readWorldEntities(DataInput stream) throws IOException{ + //entityMapping is null in older save versions, so use the default + Prov[] mapping = this.entityMapping == null ? EntityMapping.idMap : this.entityMapping; int amount = stream.readInt(); for(int j = 0; j < amount; j++){ readChunk(stream, true, in -> { byte typeid = in.readByte(); - if(EntityMapping.map(typeid) == null){ + if(mapping[typeid] == null){ in.skipBytes(lastRegionLength - 1); return; } - Entityc entity = (Entityc)EntityMapping.map(typeid).get(); + Entityc entity = (Entityc)mapping[typeid].get(); entity.read(Reads.get(in)); entity.add(); }); } } + public void readEntityMapping(DataInput stream) throws IOException{ + //copy entityMapping for further mutation; will be used in readWorldEntities + entityMapping = Arrays.copyOf(EntityMapping.idMap, EntityMapping.idMap.length); + + short amount = stream.readShort(); + for(int i = 0; i < amount; i++){ + //everything that corresponded to this ID in this save goes by this name + //so replace the prov in the current mapping with the one found with this name + short id = stream.readShort(); + String name = stream.readUTF(); + entityMapping[id] = EntityMapping.map(name); + } + } + + public void readEntities(DataInput stream) throws IOException{ + readEntityMapping(stream); + readTeamBlocks(stream); + readWorldEntities(stream); + } + public void readContentHeader(DataInput stream) throws IOException{ byte mapped = stream.readByte(); diff --git a/core/src/mindustry/io/TypeIO.java b/core/src/mindustry/io/TypeIO.java index 6e1ee0bd36..f070c3446b 100644 --- a/core/src/mindustry/io/TypeIO.java +++ b/core/src/mindustry/io/TypeIO.java @@ -5,7 +5,6 @@ import arc.math.geom.*; import arc.struct.*; import arc.util.*; import arc.util.io.*; -import arc.util.pooling.*; import mindustry.ai.types.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; @@ -29,7 +28,7 @@ import java.nio.*; import static mindustry.Vars.*; -/** Class for specifying read/write methods for code generation. */ +/** Class for specifying read/write methods for code generation. All IO MUST be thread safe!*/ @SuppressWarnings("unused") @TypeIOHandler public class TypeIO{ @@ -392,13 +391,13 @@ public class TypeIO{ return new Vec2(read.f(), read.f()); } - public static void writeStatuse(Writes write, StatusEntry entry){ + public static void writeStatus(Writes write, StatusEntry entry){ write.s(entry.effect.id); write.f(entry.time); } - public static StatusEntry readStatuse(Reads read){ - return Pools.obtain(StatusEntry.class, StatusEntry::new).set(content.getByID(ContentType.status, read.s()), read.f()); + public static StatusEntry readStatus(Reads read){ + return new StatusEntry().set(content.getByID(ContentType.status, read.s()), read.f()); } public static void writeItems(Writes write, ItemStack stack){ diff --git a/core/src/mindustry/io/versions/Save4.java b/core/src/mindustry/io/versions/Save4.java index 4c3198cc35..c75268592c 100644 --- a/core/src/mindustry/io/versions/Save4.java +++ b/core/src/mindustry/io/versions/Save4.java @@ -2,9 +2,25 @@ package mindustry.io.versions; import mindustry.io.*; +import java.io.*; + +/** This version only writes entities, no entity ID mappings. */ public class Save4 extends SaveVersion{ public Save4(){ super(4); } + + @Override + public void writeEntities(DataOutput stream) throws IOException{ + writeTeamBlocks(stream); + writeWorldEntities(stream); + } + + @Override + public void readEntities(DataInput stream) throws IOException{ + readTeamBlocks(stream); + readWorldEntities(stream); + } + } diff --git a/core/src/mindustry/io/versions/Save5.java b/core/src/mindustry/io/versions/Save5.java new file mode 100644 index 0000000000..98cdbad1dd --- /dev/null +++ b/core/src/mindustry/io/versions/Save5.java @@ -0,0 +1,10 @@ +package mindustry.io.versions; + +import mindustry.io.*; + +public class Save5 extends SaveVersion{ + + public Save5(){ + super(5); + } +} diff --git a/core/src/mindustry/logic/LAccess.java b/core/src/mindustry/logic/LAccess.java index 4f4517498a..baf4068707 100644 --- a/core/src/mindustry/logic/LAccess.java +++ b/core/src/mindustry/logic/LAccess.java @@ -21,6 +21,7 @@ public enum LAccess{ maxHealth, heat, efficiency, + progress, timescale, rotation, x, @@ -42,7 +43,6 @@ public enum LAccess{ controller, commanded, name, - config, payloadCount, payloadType, @@ -50,7 +50,7 @@ public enum LAccess{ enabled("to"), //"to" is standard for single parameter access shoot("x", "y", "shoot"), shootp(true, "unit", "shoot"), - configure(true, "to"), + config(true, "to"), color("r", "g", "b"); public final String[] params; diff --git a/core/src/mindustry/logic/LExecutor.java b/core/src/mindustry/logic/LExecutor.java index ddb498a69b..06661d83bc 100644 --- a/core/src/mindustry/logic/LExecutor.java +++ b/core/src/mindustry/logic/LExecutor.java @@ -541,7 +541,7 @@ public class LExecutor{ public void run(LExecutor exec){ Object obj = exec.obj(target); if(obj instanceof Building b && b.team == exec.team && exec.linkIds.contains(b.id)){ - if(type.isObj){ + if(type.isObj && exec.var(p1).isobj){ //TODO may break logic? b.control(type, exec.obj(p1), exec.num(p2), exec.num(p3), exec.num(p4)); }else{ b.control(type, exec.num(p1), exec.num(p2), exec.num(p3), exec.num(p4)); @@ -867,7 +867,7 @@ public class LExecutor{ } static int packSign(int value){ - return (Math.abs(value) & 0b011111111) | (value < 0 ? 0b1000000000 : 0); + return (Math.abs(value) & 0b0111111111) | (value < 0 ? 0b1000000000 : 0); } } diff --git a/core/src/mindustry/logic/LParser.java b/core/src/mindustry/logic/LParser.java index def8f98af2..d6e8b40e62 100644 --- a/core/src/mindustry/logic/LParser.java +++ b/core/src/mindustry/logic/LParser.java @@ -43,7 +43,7 @@ public class LParser{ String string(){ int from = pos; - while(pos++ < chars.length){ + while(++pos < chars.length){ var c = chars[pos]; if(c == '\n'){ error("Missing closing quote \" before end of line."); @@ -52,7 +52,7 @@ public class LParser{ } } - if(chars[pos] != '"') error("Missing closing quote \" before end of file."); + if(pos >= chars.length || chars[pos] != '"') error("Missing closing quote \" before end of file."); return new String(chars, from, ++pos - from); } @@ -128,6 +128,11 @@ public class LParser{ tokens[1] = "-1"; } + for(int i = 1; i < tok; i++){ + if(tokens[i].equals("@configure")) tokens[i] = "@config"; + if(tokens[i].equals("configure")) tokens[i] = "config"; + } + LStatement st; try{ @@ -165,7 +170,7 @@ public class LParser{ while(pos < chars.length && line < LExecutor.maxInstructions){ switch(chars[pos]){ - case '\n', ' ' -> pos ++; //skip newlines and spaces + case '\n', ';', ' ' -> pos ++; //skip newlines and spaces case '\r' -> pos += 2; //skip the newline after the \r default -> statement(); } diff --git a/core/src/mindustry/logic/LStatement.java b/core/src/mindustry/logic/LStatement.java index 638af52074..29c9c14677 100644 --- a/core/src/mindustry/logic/LStatement.java +++ b/core/src/mindustry/logic/LStatement.java @@ -45,8 +45,47 @@ public abstract class LStatement{ tooltip(label, text); } + protected String sanitize(String value){ + if(value.length() == 0){ + return ""; + }else if(value.length() == 1){ + if(value.charAt(0) == '"' || value.charAt(0) == ';' || value.charAt(0) == ' '){ + return "invalid"; + } + }else{ + StringBuilder res = new StringBuilder(value.length()); + if(value.charAt(0) == '"' && value.charAt(value.length() - 1) == '"'){ + res.append('\"'); + //strip out extra quotes + for(int i = 1; i < value.length() - 1; i++){ + if(value.charAt(i) == '"'){ + res.append('\''); + }else{ + res.append(value.charAt(i)); + } + } + res.append('\"'); + }else{ + //otherwise, strip out semicolons, spaces and quotes + for(int i = 0; i < value.length(); i++){ + char c = value.charAt(i); + res.append(switch(c){ + case ';' -> 's'; + case '"' -> '\''; + case ' ' -> '_'; + default -> c; + }); + } + } + + return res.toString(); + } + + return value; + } + protected Cell field(Table table, String value, Cons setter){ - return table.field(value, Styles.nodeField, setter) + return table.field(value, Styles.nodeField, s -> setter.get(sanitize(s))) .size(144f, 40f).pad(2f).color(table.color).maxTextLength(LAssembler.maxTokenLength).addInputDialog(); } @@ -144,7 +183,7 @@ public abstract class LStatement{ public void afterRead(){} public void write(StringBuilder builder){ - LogicIO.write(this,builder); + LogicIO.write(this, builder); } public void setupUI(){ diff --git a/core/src/mindustry/logic/LStatements.java b/core/src/mindustry/logic/LStatements.java index c93ac23448..9b78de37f8 100644 --- a/core/src/mindustry/logic/LStatements.java +++ b/core/src/mindustry/logic/LStatements.java @@ -15,6 +15,7 @@ import mindustry.type.*; import mindustry.ui.*; import mindustry.world.meta.*; +import static mindustry.Vars.*; import static mindustry.logic.LCanvas.*; import static mindustry.world.blocks.logic.LogicDisplay.*; @@ -484,7 +485,7 @@ public class LStatements{ int c = 0; for(Item item : Vars.content.items()){ if(!item.unlockedNow()) continue; - i.button(new TextureRegionDrawable(item.icon(Cicon.small)), Styles.cleari, () -> { + i.button(new TextureRegionDrawable(item.uiIcon), Styles.cleari, iconSmall, () -> { stype("@" + item.name); hide.run(); }).size(40f); @@ -498,7 +499,7 @@ public class LStatements{ int c = 0; for(Liquid item : Vars.content.liquids()){ if(!item.unlockedNow()) continue; - i.button(new TextureRegionDrawable(item.icon(Cicon.small)), Styles.cleari, () -> { + i.button(new TextureRegionDrawable(item.uiIcon), Styles.cleari, iconSmall, () -> { stype("@" + item.name); hide.run(); }).size(40f); @@ -785,11 +786,11 @@ public class LStatements{ int c = 0; for(UnitType item : Vars.content.units()){ if(!item.unlockedNow() || item.isHidden()) continue; - i.button(new TextureRegionDrawable(item.icon(Cicon.small)), Styles.cleari, () -> { + i.button(new TextureRegionDrawable(item.uiIcon), Styles.cleari, iconSmall, () -> { type = "@" + item.name; field.setText(type); hide.run(); - }).size(40f).get().resizeImage(Cicon.small.size); + }).size(40f); if(++c % 6 == 0) i.row(); } @@ -948,11 +949,11 @@ public class LStatements{ int c = 0; for(Item item : Vars.content.items()){ if(!item.unlockedNow()) continue; - i.button(new TextureRegionDrawable(item.icon(Cicon.small)), Styles.cleari, () -> { + i.button(new TextureRegionDrawable(item.uiIcon), Styles.cleari, iconSmall, () -> { ore = "@" + item.name; rebuild(table); hide.run(); - }).size(40f).get().resizeImage(Cicon.small.size); + }).size(40f); if(++c % 6 == 0) i.row(); } diff --git a/core/src/mindustry/maps/Map.java b/core/src/mindustry/maps/Map.java index f1cc24d5f4..0abe2aaffa 100644 --- a/core/src/mindustry/maps/Map.java +++ b/core/src/mindustry/maps/Map.java @@ -147,7 +147,7 @@ public class Map implements Comparable, Publishable{ public void addSteamID(String id){ tags.put("steamid", id); - ui.editor.editor.tags.put("steamid", id); + editor.tags.put("steamid", id); try{ ui.editor.save(); }catch(Exception e){ @@ -160,7 +160,7 @@ public class Map implements Comparable, Publishable{ public void removeSteamID(){ tags.remove("steamid"); - ui.editor.editor.tags.remove("steamid"); + editor.tags.remove("steamid"); try{ ui.editor.save(); }catch(Exception e){ @@ -204,7 +204,7 @@ public class Map implements Comparable, Publishable{ @Override public boolean prePublish(){ tags.put("author", player.name); - ui.editor.editor.tags.put("author", tags.get("author")); + editor.tags.put("author", tags.get("author")); ui.editor.save(); return true; diff --git a/core/src/mindustry/maps/MapPreviewLoader.java b/core/src/mindustry/maps/MapPreviewLoader.java index 6453c6f0b1..7419f7e9cb 100644 --- a/core/src/mindustry/maps/MapPreviewLoader.java +++ b/core/src/mindustry/maps/MapPreviewLoader.java @@ -1,8 +1,8 @@ package mindustry.maps; +import arc.*; import arc.assets.*; import arc.assets.loaders.*; -import arc.assets.loaders.resolvers.*; import arc.files.*; import arc.graphics.*; import arc.struct.*; @@ -13,7 +13,7 @@ import mindustry.ctype.*; public class MapPreviewLoader extends TextureLoader{ public MapPreviewLoader(){ - super(new AbsoluteFileHandleResolver()); + super(Core.files::absolute); } @Override diff --git a/core/src/mindustry/maps/Maps.java b/core/src/mindustry/maps/Maps.java index cd4020a07a..d7ea3b392d 100644 --- a/core/src/mindustry/maps/Maps.java +++ b/core/src/mindustry/maps/Maps.java @@ -217,7 +217,7 @@ public class Maps{ } Pixmap pix = MapIO.generatePreview(world.tiles); - executor.submit(() -> map.previewFile().writePNG(pix)); + executor.submit(() -> map.previewFile().writePng(pix)); writeCache(map); map.texture = new Texture(pix); @@ -409,7 +409,7 @@ public class Maps{ map.texture = new Texture(pix); executor.submit(() -> { try{ - map.previewFile().writePNG(pix); + map.previewFile().writePng(pix); writeCache(map); }catch(Exception e){ e.printStackTrace(); diff --git a/core/src/mindustry/maps/filters/BlendFilter.java b/core/src/mindustry/maps/filters/BlendFilter.java index fd3c1d9619..8aa0e7e067 100644 --- a/core/src/mindustry/maps/filters/BlendFilter.java +++ b/core/src/mindustry/maps/filters/BlendFilter.java @@ -3,6 +3,7 @@ package mindustry.maps.filters; import arc.math.*; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -26,6 +27,11 @@ public class BlendFilter extends GenerateFilter{ return true; } + @Override + public char icon(){ + return Iconc.blockSand; + } + @Override public void apply(){ if(in.floor == block || block == Blocks.air || in.floor == ignore) return; diff --git a/core/src/mindustry/maps/filters/ClearFilter.java b/core/src/mindustry/maps/filters/ClearFilter.java index 4da2564a79..59e1a5b53c 100644 --- a/core/src/mindustry/maps/filters/ClearFilter.java +++ b/core/src/mindustry/maps/filters/ClearFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -14,6 +15,11 @@ public class ClearFilter extends GenerateFilter{ return Structs.arr(new BlockOption("block", () -> block, b -> block = b, b -> oresOnly.get(b) || wallsOnly.get(b))); } + @Override + public char icon(){ + return Iconc.blockSnow; + } + @Override public void apply(){ diff --git a/core/src/mindustry/maps/filters/CoreSpawnFilter.java b/core/src/mindustry/maps/filters/CoreSpawnFilter.java index 6aa2db744b..84447ea1f3 100644 --- a/core/src/mindustry/maps/filters/CoreSpawnFilter.java +++ b/core/src/mindustry/maps/filters/CoreSpawnFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.struct.*; import arc.util.*; +import mindustry.gen.*; import mindustry.world.*; import mindustry.world.blocks.storage.*; @@ -19,6 +20,11 @@ public class CoreSpawnFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockCoreShard; + } + @Override public void apply(Tiles tiles, GenerateInput in){ IntSeq spawns = new IntSeq(); diff --git a/core/src/mindustry/maps/filters/DistortFilter.java b/core/src/mindustry/maps/filters/DistortFilter.java index daedca1765..4ae9c5a1d2 100644 --- a/core/src/mindustry/maps/filters/DistortFilter.java +++ b/core/src/mindustry/maps/filters/DistortFilter.java @@ -1,6 +1,7 @@ package mindustry.maps.filters; import arc.util.*; +import mindustry.gen.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; @@ -20,6 +21,11 @@ public class DistortFilter extends GenerateFilter{ return true; } + @Override + public char icon(){ + return Iconc.blockTendrils; + } + @Override public void apply(){ Tile tile = in.tile(in.x + noise(in.x, in.y, scl, mag) - mag / 2f, in.y + noise(in.x, in.y + o, scl, mag) - mag / 2f); diff --git a/core/src/mindustry/maps/filters/EnemySpawnFilter.java b/core/src/mindustry/maps/filters/EnemySpawnFilter.java index ac64761632..682d82debf 100644 --- a/core/src/mindustry/maps/filters/EnemySpawnFilter.java +++ b/core/src/mindustry/maps/filters/EnemySpawnFilter.java @@ -3,6 +3,7 @@ package mindustry.maps.filters; import arc.struct.*; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; @@ -17,6 +18,11 @@ public class EnemySpawnFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockSpawn; + } + @Override public void apply(Tiles tiles, GenerateInput in){ IntSeq spawns = new IntSeq(); diff --git a/core/src/mindustry/maps/filters/FilterOption.java b/core/src/mindustry/maps/filters/FilterOption.java index 1a867d8dee..f47d1c3a60 100644 --- a/core/src/mindustry/maps/filters/FilterOption.java +++ b/core/src/mindustry/maps/filters/FilterOption.java @@ -17,13 +17,13 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; public abstract class FilterOption{ - public static final Boolf floorsOnly = b -> (b instanceof Floor && !(b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.icon(Cicon.full)); - public static final Boolf wallsOnly = b -> (!b.synthetic() && !(b instanceof Floor)) && !headless && Core.atlas.isFound(b.icon(Cicon.full)) && b.inEditor; - public static final Boolf floorsOptional = b -> b == Blocks.air || ((b instanceof Floor && !(b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.icon(Cicon.full))); - public static final Boolf wallsOptional = b -> (b == Blocks.air || ((!b.synthetic() && !(b instanceof Floor)) && !headless && Core.atlas.isFound(b.icon(Cicon.full)))) && b.inEditor; - public static final Boolf wallsOresOptional = b -> b == Blocks.air || (((!b.synthetic() && !(b instanceof Floor)) || (b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.icon(Cicon.full))) && b.inEditor; - public static final Boolf oresOnly = b -> b instanceof OverlayFloor && !headless && Core.atlas.isFound(b.icon(Cicon.full)); - public static final Boolf oresFloorsOptional = b -> (b instanceof Floor) && !headless && Core.atlas.isFound(b.icon(Cicon.full)); + public static final Boolf floorsOnly = b -> (b instanceof Floor && !(b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.fullIcon); + public static final Boolf wallsOnly = b -> (!b.synthetic() && !(b instanceof Floor)) && !headless && Core.atlas.isFound(b.fullIcon) && b.inEditor; + public static final Boolf floorsOptional = b -> b == Blocks.air || ((b instanceof Floor && !(b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.fullIcon)); + public static final Boolf wallsOptional = b -> (b == Blocks.air || ((!b.synthetic() && !(b instanceof Floor)) && !headless && Core.atlas.isFound(b.fullIcon))) && b.inEditor; + public static final Boolf wallsOresOptional = b -> b == Blocks.air || (((!b.synthetic() && !(b instanceof Floor)) || (b instanceof OverlayFloor)) && !headless && Core.atlas.isFound(b.fullIcon)) && b.inEditor; + public static final Boolf oresOnly = b -> b instanceof OverlayFloor && !headless && Core.atlas.isFound(b.fullIcon); + public static final Boolf oresFloorsOptional = b -> (b instanceof Floor) && !headless && Core.atlas.isFound(b.fullIcon); public static final Boolf anyOptional = b -> (floorsOnly.get(b) || wallsOnly.get(b) || oresOnly.get(b) || b == Blocks.air) && b.inEditor; public abstract void build(Table table); @@ -89,15 +89,15 @@ public abstract class FilterOption{ @Override public void build(Table table){ - table.button(b -> b.image(supplier.get().icon(Cicon.small)).update(i -> ((TextureRegionDrawable)i.getDrawable()) - .setRegion(supplier.get() == Blocks.air ? Icon.none.getRegion() : supplier.get().icon(Cicon.small))).size(8 * 3), () -> { + table.button(b -> b.image(supplier.get().uiIcon).update(i -> ((TextureRegionDrawable)i.getDrawable()) + .setRegion(supplier.get() == Blocks.air ? Icon.none.getRegion() : supplier.get().uiIcon)).size(iconSmall), () -> { BaseDialog dialog = new BaseDialog(""); dialog.setFillParent(false); int i = 0; for(Block block : Vars.content.blocks()){ if(!filter.get(block)) continue; - dialog.cont.image(block == Blocks.air ? Icon.none.getRegion() : block.icon(Cicon.medium)).size(8 * 4).pad(3).get().clicked(() -> { + dialog.cont.image(block == Blocks.air ? Icon.none.getRegion() : block.uiIcon).size(iconMed).pad(3).get().clicked(() -> { consumer.get(block); dialog.hide(); changed.run(); diff --git a/core/src/mindustry/maps/filters/GenerateFilter.java b/core/src/mindustry/maps/filters/GenerateFilter.java index 07811c3baf..0887d4f135 100644 --- a/core/src/mindustry/maps/filters/GenerateFilter.java +++ b/core/src/mindustry/maps/filters/GenerateFilter.java @@ -82,6 +82,10 @@ public abstract class GenerateFilter{ return Core.bundle.get("filter." + c.getSimpleName().toLowerCase().replace("filter", ""), c.getSimpleName().replace("Filter", "")); } + public char icon(){ + return '\0'; + } + /** set the seed to a random number */ public void randomize(){ seed = Mathf.random(99999999); diff --git a/core/src/mindustry/maps/filters/MedianFilter.java b/core/src/mindustry/maps/filters/MedianFilter.java index 78a4df65d4..fa2c3e310b 100644 --- a/core/src/mindustry/maps/filters/MedianFilter.java +++ b/core/src/mindustry/maps/filters/MedianFilter.java @@ -3,6 +3,7 @@ package mindustry.maps.filters; import arc.math.*; import arc.struct.*; import arc.util.*; +import mindustry.gen.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; @@ -26,6 +27,11 @@ public class MedianFilter extends GenerateFilter{ return true; } + @Override + public char icon(){ + return Iconc.blockSporePine; + } + @Override public void apply(){ int rad = (int)radius; diff --git a/core/src/mindustry/maps/filters/MirrorFilter.java b/core/src/mindustry/maps/filters/MirrorFilter.java index d5642381ad..ed57af5138 100644 --- a/core/src/mindustry/maps/filters/MirrorFilter.java +++ b/core/src/mindustry/maps/filters/MirrorFilter.java @@ -6,6 +6,7 @@ import arc.math.geom.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.util.*; +import mindustry.gen.*; import mindustry.graphics.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; @@ -22,6 +23,11 @@ public class MirrorFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockMetalFloor5; + } + @Override protected void apply(){ v1.trnsExact(angle - 90, 1f); diff --git a/core/src/mindustry/maps/filters/NoiseFilter.java b/core/src/mindustry/maps/filters/NoiseFilter.java index abff26ceb8..acbf8162c6 100644 --- a/core/src/mindustry/maps/filters/NoiseFilter.java +++ b/core/src/mindustry/maps/filters/NoiseFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -23,6 +24,11 @@ public class NoiseFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockPebbles; + } + @Override public void apply(){ float noise = noise(in.x, in.y, scl, 1f, octaves, falloff); diff --git a/core/src/mindustry/maps/filters/OreFilter.java b/core/src/mindustry/maps/filters/OreFilter.java index 501d152d4f..5487ea0d59 100644 --- a/core/src/mindustry/maps/filters/OreFilter.java +++ b/core/src/mindustry/maps/filters/OreFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -22,6 +23,11 @@ public class OreFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockOreCopper; + } + @Override public void apply(){ float noise = noise(in.x, in.y, scl, 1f, octaves, falloff); diff --git a/core/src/mindustry/maps/filters/OreMedianFilter.java b/core/src/mindustry/maps/filters/OreMedianFilter.java index 27a10548af..cba1d56936 100644 --- a/core/src/mindustry/maps/filters/OreMedianFilter.java +++ b/core/src/mindustry/maps/filters/OreMedianFilter.java @@ -5,6 +5,7 @@ import arc.struct.*; import arc.util.*; import mindustry.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; @@ -27,6 +28,11 @@ public class OreMedianFilter extends GenerateFilter{ return true; } + @Override + public char icon(){ + return Iconc.blockOreLead; + } + @Override public void apply(){ if(in.overlay == Blocks.spawn) return; diff --git a/core/src/mindustry/maps/filters/RiverNoiseFilter.java b/core/src/mindustry/maps/filters/RiverNoiseFilter.java index 73ce77acb9..5019124c23 100644 --- a/core/src/mindustry/maps/filters/RiverNoiseFilter.java +++ b/core/src/mindustry/maps/filters/RiverNoiseFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -22,6 +23,11 @@ public class RiverNoiseFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockWater; + } + @Override public void apply(){ float noise = rnoise(in.x, in.y, scl, 1f); diff --git a/core/src/mindustry/maps/filters/ScatterFilter.java b/core/src/mindustry/maps/filters/ScatterFilter.java index 9cc809eeed..b766183087 100644 --- a/core/src/mindustry/maps/filters/ScatterFilter.java +++ b/core/src/mindustry/maps/filters/ScatterFilter.java @@ -2,6 +2,7 @@ package mindustry.maps.filters; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -20,6 +21,11 @@ public class ScatterFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockBoulder; + } + @Override public void apply(){ diff --git a/core/src/mindustry/maps/filters/SpawnPathFilter.java b/core/src/mindustry/maps/filters/SpawnPathFilter.java index 8384024186..ac49b13d31 100644 --- a/core/src/mindustry/maps/filters/SpawnPathFilter.java +++ b/core/src/mindustry/maps/filters/SpawnPathFilter.java @@ -6,6 +6,7 @@ import arc.util.*; import mindustry.*; import mindustry.ai.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.maps.filters.FilterOption.*; import mindustry.world.*; import mindustry.world.blocks.storage.*; @@ -23,6 +24,11 @@ public class SpawnPathFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockCommandCenter; + } + @Override public void apply(Tiles tiles, GenerateInput in){ Tile core = null; diff --git a/core/src/mindustry/maps/filters/TerrainFilter.java b/core/src/mindustry/maps/filters/TerrainFilter.java index 4747149448..f494d0c143 100644 --- a/core/src/mindustry/maps/filters/TerrainFilter.java +++ b/core/src/mindustry/maps/filters/TerrainFilter.java @@ -3,6 +3,7 @@ package mindustry.maps.filters; import arc.math.*; import arc.util.*; import mindustry.content.*; +import mindustry.gen.*; import mindustry.world.*; import static mindustry.maps.filters.FilterOption.*; @@ -25,6 +26,11 @@ public class TerrainFilter extends GenerateFilter{ ); } + @Override + public char icon(){ + return Iconc.blockStoneWall; + } + @Override public void apply(){ float noise = noise(in.x, in.y, scl, magnitude, octaves, falloff) + Mathf.dst((float)in.x / in.width, (float)in.y / in.height, 0.5f, 0.5f) * circleScl; diff --git a/core/src/mindustry/maps/generators/BaseGenerator.java b/core/src/mindustry/maps/generators/BaseGenerator.java index 02e02e8de2..636e386356 100644 --- a/core/src/mindustry/maps/generators/BaseGenerator.java +++ b/core/src/mindustry/maps/generators/BaseGenerator.java @@ -15,6 +15,7 @@ import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.defense.*; import mindustry.world.blocks.distribution.*; +import mindustry.world.blocks.payloads.*; import mindustry.world.blocks.power.*; import mindustry.world.blocks.production.*; import mindustry.world.meta.*; @@ -103,7 +104,7 @@ public class BaseGenerator{ Tile o = tiles.get(tile.x + p.x, tile.y + p.y); //do not block payloads - if(o != null && (o.block() instanceof PayloadConveyor || o.block() instanceof PayloadAcceptor)){ + if(o != null && (o.block() instanceof PayloadConveyor || o.block() instanceof PayloadBlock)){ return; } } diff --git a/core/src/mindustry/maps/generators/BasicGenerator.java b/core/src/mindustry/maps/generators/BasicGenerator.java index 1ef6d31297..bee63cc9eb 100644 --- a/core/src/mindustry/maps/generators/BasicGenerator.java +++ b/core/src/mindustry/maps/generators/BasicGenerator.java @@ -121,7 +121,10 @@ public abstract class BasicGenerator implements WorldGenerator{ } public void tech(){ - Block[] blocks = {Blocks.darkPanel3}; + tech(Blocks.darkPanel3, Blocks.darkPanel4, Blocks.darkMetal); + } + + public void tech(Block floor1, Block floor2, Block wall){ int secSize = 20; pass((x, y) -> { if(!floor.asFloor().hasSurface()) return; @@ -130,14 +133,14 @@ public abstract class BasicGenerator implements WorldGenerator{ int sclx = x / secSize, scly = y / secSize; if(noise(sclx, scly, 0.2f, 1f) > 0.63f && noise(sclx, scly + 999, 200f, 1f) > 0.6f && (mx == 0 || my == 0 || mx == secSize - 1 || my == secSize - 1)){ if(Mathf.chance(noise(x + 0x231523, y, 40f, 1f))){ - floor = blocks[rand.random(0, blocks.length - 1)]; + floor = floor1; if(Mathf.dst(mx, my, secSize/2, secSize/2) > secSize/2f + 2){ - floor = Blocks.darkPanel4; + floor = floor2; } } if(block.solid && Mathf.chance(0.7)){ - block = Blocks.darkMetal; + block = wall; } } }); @@ -215,7 +218,9 @@ public abstract class BasicGenerator implements WorldGenerator{ read.set(write); } - tiles.each((x, y) -> tiles.get(x, y).setBlock(!read.get(x, y) ? Blocks.air : tiles.get(x, y).floor().wall)); + for(var t : tiles){ + t.setBlock(!read.get(t.x, t.y) ? Blocks.air : t.floor().wall); + } } protected float noise(float x, float y, double scl, double mag){ diff --git a/core/src/mindustry/maps/generators/PlanetGenerator.java b/core/src/mindustry/maps/generators/PlanetGenerator.java index d1cfe30bf5..830be018fb 100644 --- a/core/src/mindustry/maps/generators/PlanetGenerator.java +++ b/core/src/mindustry/maps/generators/PlanetGenerator.java @@ -2,12 +2,19 @@ package mindustry.maps.generators; import arc.math.geom.*; import arc.struct.*; +import arc.struct.ObjectIntMap.*; import arc.util.noise.*; +import mindustry.content.*; +import mindustry.ctype.*; +import mindustry.game.*; import mindustry.graphics.g3d.*; import mindustry.graphics.g3d.PlanetGrid.*; import mindustry.type.*; +import mindustry.type.Weather.*; import mindustry.world.*; +import static mindustry.Vars.*; + public abstract class PlanetGenerator extends BasicGenerator implements HexMesher{ protected IntSeq ints = new IntSeq(); protected Sector sector; @@ -43,6 +50,65 @@ public abstract class PlanetGenerator extends BasicGenerator implements HexMeshe } } + public void addWeather(Sector sector, Rules rules){ + + //apply weather based on terrain + ObjectIntMap floorc = new ObjectIntMap<>(); + ObjectSet content = new ObjectSet<>(); + + for(Tile tile : world.tiles){ + if(world.getDarkness(tile.x, tile.y) >= 3){ + continue; + } + + Liquid liquid = tile.floor().liquidDrop; + if(tile.floor().itemDrop != null) content.add(tile.floor().itemDrop); + if(tile.overlay().itemDrop != null) content.add(tile.overlay().itemDrop); + if(liquid != null) content.add(liquid); + + if(!tile.block().isStatic()){ + floorc.increment(tile.floor()); + if(tile.overlay() != Blocks.air){ + floorc.increment(tile.overlay()); + } + } + } + + //sort counts in descending order + Seq> entries = floorc.entries().toArray(); + entries.sort(e -> -e.value); + //remove all blocks occuring < 30 times - unimportant + entries.removeAll(e -> e.value < 30); + + Block[] floors = new Block[entries.size]; + for(int i = 0; i < entries.size; i++){ + floors[i] = entries.get(i).key; + } + + //TODO bad code + boolean hasSnow = floors.length > 0 && (floors[0].name.contains("ice") || floors[0].name.contains("snow")); + boolean hasRain = floors.length > 0 && !hasSnow && content.contains(Liquids.water) && !floors[0].name.contains("sand"); + boolean hasDesert = floors.length > 0 && !hasSnow && !hasRain && floors[0] == Blocks.sand; + boolean hasSpores = floors.length > 0 && (floors[0].name.contains("spore") || floors[0].name.contains("moss") || floors[0].name.contains("tainted")); + + if(hasSnow){ + rules.weather.add(new WeatherEntry(Weathers.snow)); + } + + if(hasRain){ + rules.weather.add(new WeatherEntry(Weathers.rain)); + rules.weather.add(new WeatherEntry(Weathers.fog)); + } + + if(hasDesert){ + rules.weather.add(new WeatherEntry(Weathers.sandstorm)); + } + + if(hasSpores){ + rules.weather.add(new WeatherEntry(Weathers.sporestorm)); + } + } + protected void genTile(Vec3 position, TileGen tile){ } @@ -53,6 +119,11 @@ public abstract class PlanetGenerator extends BasicGenerator implements HexMeshe return (float)noise.octaveNoise3D(octaves, falloff, 1f / scl, v.x, v.y, v.z) * (float)mag; } + /** @return the scaling factor for sector rects. */ + public float getSizeScl(){ + return 3200; + } + public void generate(Tiles tiles, Sector sec){ this.tiles = tiles; this.sector = sec; diff --git a/core/src/mindustry/maps/planet/SerpuloPlanetGenerator.java b/core/src/mindustry/maps/planet/SerpuloPlanetGenerator.java index 2459a7b2eb..b5f8be471a 100644 --- a/core/src/mindustry/maps/planet/SerpuloPlanetGenerator.java +++ b/core/src/mindustry/maps/planet/SerpuloPlanetGenerator.java @@ -115,7 +115,7 @@ public class SerpuloPlanetGenerator extends PlanetGenerator{ tile.floor = getBlock(position); tile.block = tile.floor.asFloor().wall; - if(rid.getValue(position.x, position.y, position.z, 22) > 0.32){ + if(rid.getValue(position.x, position.y, position.z, 22) > 0.31){ tile.block = Blocks.air; } } @@ -162,12 +162,11 @@ public class SerpuloPlanetGenerator extends PlanetGenerator{ } void connect(Room to){ - if(connected.contains(to)) return; + if(!connected.add(to)) return; - connected.add(to); - float nscl = rand.random(20f, 60f); - int stroke = rand.random(4, 12); - brush(pathfind(x, y, to.x, to.y, tile -> (tile.solid() ? 5f : 0f) + noise(tile.x, tile.y, 1, 1, 1f / nscl) * 60, Astar.manhattan), stroke); + float nscl = rand.random(100f, 140f); + int stroke = rand.random(3, 9); + brush(pathfind(x, y, to.x, to.y, tile -> (tile.solid() ? 5f : 0f) + noise(tile.x, tile.y, 2, 0.4, 1f / nscl) * 500, Astar.manhattan), stroke); } } @@ -219,7 +218,7 @@ public class SerpuloPlanetGenerator extends PlanetGenerator{ for(int j = 0; j < enemySpawns; j++){ float enemyOffset = rand.range(60f); Tmp.v1.set(cx - width/2, cy - height/2).rotate(180f + enemyOffset).add(width/2, height/2); - Room espawn = new Room((int)Tmp.v1.x, (int)Tmp.v1.y, rand.random(8, 15)); + Room espawn = new Room((int)Tmp.v1.x, (int)Tmp.v1.y, rand.random(8, 16)); roomseq.add(espawn); enemies.add(espawn); } diff --git a/core/src/mindustry/maps/planet/TantrosPlanetGenerator.java b/core/src/mindustry/maps/planet/TantrosPlanetGenerator.java deleted file mode 100644 index d74f94e250..0000000000 --- a/core/src/mindustry/maps/planet/TantrosPlanetGenerator.java +++ /dev/null @@ -1,32 +0,0 @@ -package mindustry.maps.planet; - -import arc.graphics.*; -import arc.math.*; -import arc.math.geom.*; -import mindustry.content.*; -import mindustry.game.*; -import mindustry.maps.generators.*; - -public class TantrosPlanetGenerator extends PlanetGenerator{ - Color c1 = Color.valueOf("5057a6"), c2 = Color.valueOf("272766"), out = new Color(); - - @Override - public float getHeight(Vec3 position){ - return 0; - } - - @Override - public Color getColor(Vec3 position){ - float depth = (float)noise.octaveNoise3D(2, 0.56, 1.7f, position.x, position.y, position.z) / 2f; - return c1.write(out).lerp(c2, Mathf.clamp(Mathf.round(depth, 0.15f))).a(0.6f); - } - - @Override - protected void generate(){ - pass((x, y) -> { - floor = Blocks.deepwater; - }); - - Schematics.placeLaunchLoadout(width / 2, height / 2); - } -} diff --git a/core/src/mindustry/mod/ClassMap.java b/core/src/mindustry/mod/ClassMap.java index af000c2f96..0961e12999 100644 --- a/core/src/mindustry/mod/ClassMap.java +++ b/core/src/mindustry/mod/ClassMap.java @@ -1,20 +1,26 @@ package mindustry.mod; import arc.struct.*; +import mindustry.world.blocks.environment.*; + /** Generated class. Maps simple class names to concrete classes. For use in JSON mods. */ +@SuppressWarnings("deprecation") public class ClassMap{ public static final ObjectMap> classes = new ObjectMap<>(); static{ classes.put("BuilderAI", mindustry.ai.types.BuilderAI.class); + classes.put("DefenderAI", mindustry.ai.types.DefenderAI.class); classes.put("FlyingAI", mindustry.ai.types.FlyingAI.class); classes.put("FormationAI", mindustry.ai.types.FormationAI.class); classes.put("GroundAI", mindustry.ai.types.GroundAI.class); + classes.put("HugAI", mindustry.ai.types.HugAI.class); classes.put("LogicAI", mindustry.ai.types.LogicAI.class); classes.put("MinerAI", mindustry.ai.types.MinerAI.class); classes.put("RepairAI", mindustry.ai.types.RepairAI.class); classes.put("SuicideAI", mindustry.ai.types.SuicideAI.class); classes.put("Ability", mindustry.entities.abilities.Ability.class); + classes.put("EnergyFieldAbility", mindustry.entities.abilities.EnergyFieldAbility.class); classes.put("ForceFieldAbility", mindustry.entities.abilities.ForceFieldAbility.class); classes.put("MoveLightningAbility", mindustry.entities.abilities.MoveLightningAbility.class); classes.put("RepairFieldAbility", mindustry.entities.abilities.RepairFieldAbility.class); @@ -26,6 +32,7 @@ public class ClassMap{ classes.put("BombBulletType", mindustry.entities.bullet.BombBulletType.class); classes.put("BulletType", mindustry.entities.bullet.BulletType.class); classes.put("ContinuousLaserBulletType", mindustry.entities.bullet.ContinuousLaserBulletType.class); + classes.put("EmpBulletType", mindustry.entities.bullet.EmpBulletType.class); classes.put("FlakBulletType", mindustry.entities.bullet.FlakBulletType.class); classes.put("LaserBoltBulletType", mindustry.entities.bullet.LaserBoltBulletType.class); classes.put("LaserBulletType", mindustry.entities.bullet.LaserBulletType.class); @@ -37,6 +44,7 @@ public class ClassMap{ classes.put("RailBulletType", mindustry.entities.bullet.RailBulletType.class); classes.put("SapBulletType", mindustry.entities.bullet.SapBulletType.class); classes.put("ShrapnelBulletType", mindustry.entities.bullet.ShrapnelBulletType.class); + classes.put("ExplosionEffect", mindustry.entities.effect.ExplosionEffect.class); classes.put("MultiEffect", mindustry.entities.effect.MultiEffect.class); classes.put("ParticleEffect", mindustry.entities.effect.ParticleEffect.class); classes.put("WaveEffect", mindustry.entities.effect.WaveEffect.class); @@ -45,8 +53,36 @@ public class ClassMap{ classes.put("Produce", mindustry.game.Objectives.Produce.class); classes.put("Research", mindustry.game.Objectives.Research.class); classes.put("SectorComplete", mindustry.game.Objectives.SectorComplete.class); + classes.put("AmmoType", mindustry.type.AmmoType.class); + classes.put("AmmoTypes", mindustry.type.AmmoTypes.class); + classes.put("ItemAmmoType", mindustry.type.AmmoTypes.ItemAmmoType.class); + classes.put("PowerAmmoType", mindustry.type.AmmoTypes.PowerAmmoType.class); + classes.put("Category", mindustry.type.Category.class); + classes.put("ErrorContent", mindustry.type.ErrorContent.class); + classes.put("Item", mindustry.type.Item.class); + classes.put("ItemSeq", mindustry.type.ItemSeq.class); + classes.put("ItemStack", mindustry.type.ItemStack.class); + classes.put("Liquid", mindustry.type.Liquid.class); + classes.put("LiquidStack", mindustry.type.LiquidStack.class); + classes.put("Planet", mindustry.type.Planet.class); + classes.put("Publishable", mindustry.type.Publishable.class); + classes.put("Satellite", mindustry.type.Satellite.class); + classes.put("Sector", mindustry.type.Sector.class); + classes.put("SectorRect", mindustry.type.Sector.SectorRect.class); + classes.put("SectorPreset", mindustry.type.SectorPreset.class); + classes.put("StatusEffect", mindustry.type.StatusEffect.class); + classes.put("TransitionHandler", mindustry.type.StatusEffect.TransitionHandler.class); + classes.put("UnitType", mindustry.type.UnitType.class); + classes.put("Weapon", mindustry.type.Weapon.class); + classes.put("Weather", mindustry.type.Weather.class); + classes.put("WeatherEntry", mindustry.type.Weather.WeatherEntry.class); + classes.put("PointDefenseWeapon", mindustry.type.weapons.PointDefenseWeapon.class); + classes.put("RepairBeamWeapon", mindustry.type.weapons.RepairBeamWeapon.class); + classes.put("HealBeamMount", mindustry.type.weapons.RepairBeamWeapon.HealBeamMount.class); + classes.put("MagneticStorm", mindustry.type.weather.MagneticStorm.class); classes.put("ParticleWeather", mindustry.type.weather.ParticleWeather.class); classes.put("RainWeather", mindustry.type.weather.RainWeather.class); + classes.put("SolarFlare", mindustry.type.weather.SolarFlare.class); classes.put("Attributes", mindustry.world.blocks.Attributes.class); classes.put("Autotiler", mindustry.world.blocks.Autotiler.class); classes.put("AutotilerHolder", mindustry.world.blocks.Autotiler.AutotilerHolder.class); @@ -59,6 +95,8 @@ public class ClassMap{ classes.put("AcceleratorBuild", mindustry.world.blocks.campaign.Accelerator.AcceleratorBuild.class); classes.put("LaunchPad", mindustry.world.blocks.campaign.LaunchPad.class); classes.put("LaunchPadBuild", mindustry.world.blocks.campaign.LaunchPad.LaunchPadBuild.class); + classes.put("PayloadLaunchPad", mindustry.world.blocks.campaign.PayloadLaunchPad.class); + classes.put("PayloadLaunchPadBuild", mindustry.world.blocks.campaign.PayloadLaunchPad.PayloadLaunchPadBuild.class); classes.put("Door", mindustry.world.blocks.defense.Door.class); classes.put("DoorBuild", mindustry.world.blocks.defense.Door.DoorBuild.class); classes.put("ForceProjector", mindustry.world.blocks.defense.ForceProjector.class); @@ -76,6 +114,7 @@ public class ClassMap{ classes.put("BaseTurret", mindustry.world.blocks.defense.turrets.BaseTurret.class); classes.put("BaseTurretBuild", mindustry.world.blocks.defense.turrets.BaseTurret.BaseTurretBuild.class); classes.put("ItemTurret", mindustry.world.blocks.defense.turrets.ItemTurret.class); + classes.put("ItemEntry", mindustry.world.blocks.defense.turrets.ItemTurret.ItemEntry.class); classes.put("ItemTurretBuild", mindustry.world.blocks.defense.turrets.ItemTurret.ItemTurretBuild.class); classes.put("LaserTurret", mindustry.world.blocks.defense.turrets.LaserTurret.class); classes.put("LaserTurretBuild", mindustry.world.blocks.defense.turrets.LaserTurret.LaserTurretBuild.class); @@ -99,6 +138,12 @@ public class ClassMap{ classes.put("ChainedBuilding", mindustry.world.blocks.distribution.ChainedBuilding.class); classes.put("Conveyor", mindustry.world.blocks.distribution.Conveyor.class); classes.put("ConveyorBuild", mindustry.world.blocks.distribution.Conveyor.ConveyorBuild.class); + classes.put("Duct", mindustry.world.blocks.distribution.Duct.class); + classes.put("DuctBuild", mindustry.world.blocks.distribution.Duct.DuctBuild.class); + classes.put("DuctBridge", mindustry.world.blocks.distribution.DuctBridge.class); + classes.put("DuctBridgeBuild", mindustry.world.blocks.distribution.DuctBridge.DuctBridgeBuild.class); + classes.put("DuctRouter", mindustry.world.blocks.distribution.DuctRouter.class); + classes.put("DuctBuild", mindustry.world.blocks.distribution.DuctRouter.DuctBuild.class); classes.put("ExtendingItemBridge", mindustry.world.blocks.distribution.ExtendingItemBridge.class); classes.put("ExtendingItemBridgeBuild", mindustry.world.blocks.distribution.ExtendingItemBridge.ExtendingItemBridgeBuild.class); classes.put("ItemBridge", mindustry.world.blocks.distribution.ItemBridge.class); @@ -122,19 +167,22 @@ public class ClassMap{ classes.put("StackConveyor", mindustry.world.blocks.distribution.StackConveyor.class); classes.put("StackConveyorBuild", mindustry.world.blocks.distribution.StackConveyor.StackConveyorBuild.class); classes.put("AirBlock", mindustry.world.blocks.environment.AirBlock.class); - classes.put("Boulder", mindustry.world.blocks.environment.Boulder.class); classes.put("Cliff", mindustry.world.blocks.environment.Cliff.class); classes.put("DoubleOverlayFloor", mindustry.world.blocks.environment.DoubleOverlayFloor.class); classes.put("Floor", mindustry.world.blocks.environment.Floor.class); classes.put("OreBlock", mindustry.world.blocks.environment.OreBlock.class); classes.put("OverlayFloor", mindustry.world.blocks.environment.OverlayFloor.class); + classes.put("Prop", mindustry.world.blocks.environment.Prop.class); + classes.put("Bush", Bush.class); + classes.put("WavingProp", WavingProp.class); classes.put("ShallowLiquid", mindustry.world.blocks.environment.ShallowLiquid.class); classes.put("SpawnBlock", mindustry.world.blocks.environment.SpawnBlock.class); + classes.put("StaticClusterWall", StaticClusterWall.class); classes.put("StaticTree", mindustry.world.blocks.environment.StaticTree.class); classes.put("StaticWall", mindustry.world.blocks.environment.StaticWall.class); classes.put("TreeBlock", mindustry.world.blocks.environment.TreeBlock.class); - classes.put("BlockForge", mindustry.world.blocks.experimental.BlockForge.class); - classes.put("BlockForgeBuild", mindustry.world.blocks.experimental.BlockForge.BlockForgeBuild.class); + classes.put("WallOreBlock", mindustry.world.blocks.environment.WallOreBlock.class); + classes.put("WobbleProp", mindustry.world.blocks.environment.WobbleProp.class); classes.put("BlockLoader", mindustry.world.blocks.experimental.BlockLoader.class); classes.put("BlockLoaderBuild", mindustry.world.blocks.experimental.BlockLoader.BlockLoaderBuild.class); classes.put("BlockUnloader", mindustry.world.blocks.experimental.BlockUnloader.class); @@ -170,8 +218,26 @@ public class ClassMap{ classes.put("MessageBuild", mindustry.world.blocks.logic.MessageBlock.MessageBuild.class); classes.put("SwitchBlock", mindustry.world.blocks.logic.SwitchBlock.class); classes.put("SwitchBuild", mindustry.world.blocks.logic.SwitchBlock.SwitchBuild.class); + classes.put("BallisticSilo", mindustry.world.blocks.payloads.BallisticSilo.class); + classes.put("BallisticSiloBuild", mindustry.world.blocks.payloads.BallisticSilo.BallisticSiloBuild.class); + classes.put("BlockForge", mindustry.world.blocks.payloads.BlockForge.class); + classes.put("BlockForgeBuild", mindustry.world.blocks.payloads.BlockForge.BlockForgeBuild.class); + classes.put("BlockProducer", mindustry.world.blocks.payloads.BlockProducer.class); + classes.put("BlockProducerBuild", mindustry.world.blocks.payloads.BlockProducer.BlockProducerBuild.class); classes.put("BuildPayload", mindustry.world.blocks.payloads.BuildPayload.class); + classes.put("NuclearWarhead", mindustry.world.blocks.payloads.NuclearWarhead.class); + classes.put("NuclearWarheadBuild", mindustry.world.blocks.payloads.NuclearWarhead.NuclearWarheadBuild.class); classes.put("Payload", mindustry.world.blocks.payloads.Payload.class); + classes.put("PayloadBlock", mindustry.world.blocks.payloads.PayloadBlock.class); + classes.put("PayloadBlockBuild", mindustry.world.blocks.payloads.PayloadBlock.PayloadBlockBuild.class); + classes.put("PayloadMassDriver", mindustry.world.blocks.payloads.PayloadMassDriver.class); + classes.put("PayloadDriverBuild", mindustry.world.blocks.payloads.PayloadMassDriver.PayloadDriverBuild.class); + classes.put("PayloadDriverState", mindustry.world.blocks.payloads.PayloadMassDriver.PayloadDriverState.class); + classes.put("PayloadMassDriverData", mindustry.world.blocks.payloads.PayloadMassDriver.PayloadMassDriverData.class); + classes.put("PayloadSource", mindustry.world.blocks.payloads.PayloadSource.class); + classes.put("PayloadSourceBuild", mindustry.world.blocks.payloads.PayloadSource.PayloadSourceBuild.class); + classes.put("PayloadVoid", mindustry.world.blocks.payloads.PayloadVoid.class); + classes.put("BlockLoaderBuild", mindustry.world.blocks.payloads.PayloadVoid.BlockLoaderBuild.class); classes.put("UnitPayload", mindustry.world.blocks.payloads.UnitPayload.class); classes.put("Battery", mindustry.world.blocks.power.Battery.class); classes.put("BatteryBuild", mindustry.world.blocks.power.Battery.BatteryBuild.class); @@ -201,8 +267,12 @@ public class ClassMap{ classes.put("SolarGeneratorBuild", mindustry.world.blocks.power.SolarGenerator.SolarGeneratorBuild.class); classes.put("ThermalGenerator", mindustry.world.blocks.power.ThermalGenerator.class); classes.put("ThermalGeneratorBuild", mindustry.world.blocks.power.ThermalGenerator.ThermalGeneratorBuild.class); + classes.put("AttributeCrafter", mindustry.world.blocks.production.AttributeCrafter.class); + classes.put("AttributeCrafterBuild", mindustry.world.blocks.production.AttributeCrafter.AttributeCrafterBuild.class); classes.put("AttributeSmelter", mindustry.world.blocks.production.AttributeSmelter.class); classes.put("AttributeSmelterBuild", mindustry.world.blocks.production.AttributeSmelter.AttributeSmelterBuild.class); + classes.put("BeamDrill", mindustry.world.blocks.production.BeamDrill.class); + classes.put("BeamDrillBuild", mindustry.world.blocks.production.BeamDrill.BeamDrillBuild.class); classes.put("Cultivator", mindustry.world.blocks.production.Cultivator.class); classes.put("CultivatorBuild", mindustry.world.blocks.production.Cultivator.CultivatorBuild.class); classes.put("Drill", mindustry.world.blocks.production.Drill.class); @@ -223,6 +293,8 @@ public class ClassMap{ classes.put("PumpBuild", mindustry.world.blocks.production.Pump.PumpBuild.class); classes.put("Separator", mindustry.world.blocks.production.Separator.class); classes.put("SeparatorBuild", mindustry.world.blocks.production.Separator.SeparatorBuild.class); + classes.put("SingleBlockProducer", mindustry.world.blocks.production.SingleBlockProducer.class); + classes.put("SingleBlockProducerBuild", mindustry.world.blocks.production.SingleBlockProducer.SingleBlockProducerBuild.class); classes.put("SolidPump", mindustry.world.blocks.production.SolidPump.class); classes.put("SolidPumpBuild", mindustry.world.blocks.production.SolidPump.SolidPumpBuild.class); classes.put("ItemSource", mindustry.world.blocks.sandbox.ItemSource.class); @@ -256,10 +328,14 @@ public class ClassMap{ classes.put("UnitFactoryBuild", mindustry.world.blocks.units.UnitFactory.UnitFactoryBuild.class); classes.put("UnitPlan", mindustry.world.blocks.units.UnitFactory.UnitPlan.class); classes.put("DrawAnimation", mindustry.world.draw.DrawAnimation.class); + classes.put("DrawArcSmelter", mindustry.world.draw.DrawArcSmelter.class); classes.put("DrawBlock", mindustry.world.draw.DrawBlock.class); + classes.put("DrawCells", mindustry.world.draw.DrawCells.class); + classes.put("DrawCultivator", mindustry.world.draw.DrawCultivator.class); classes.put("DrawGlow", mindustry.world.draw.DrawGlow.class); classes.put("DrawMixer", mindustry.world.draw.DrawMixer.class); classes.put("DrawRotator", mindustry.world.draw.DrawRotator.class); + classes.put("DrawSmelter", mindustry.world.draw.DrawSmelter.class); classes.put("DrawWeave", mindustry.world.draw.DrawWeave.class); classes.put("Block", mindustry.world.Block.class); } diff --git a/core/src/mindustry/mod/ContentParser.java b/core/src/mindustry/mod/ContentParser.java index 11bd9d0416..720f5c2c02 100644 --- a/core/src/mindustry/mod/ContentParser.java +++ b/core/src/mindustry/mod/ContentParser.java @@ -26,6 +26,7 @@ import mindustry.entities.effect.*; import mindustry.game.*; import mindustry.game.Objectives.*; import mindustry.gen.*; +import mindustry.graphics.*; import mindustry.mod.Mods.*; import mindustry.type.*; import mindustry.type.weather.*; @@ -59,6 +60,7 @@ public class ContentParser{ return result; }); put(Interp.class, (type, data) -> field(Interp.class, data)); + put(CacheLayer.class, (type, data) -> field(CacheLayer.class, data)); put(Schematic.class, (type, data) -> { Object result = fieldOpt(Loadouts.class, data); if(result != null){ @@ -132,7 +134,9 @@ public class ContentParser{ return obj; }); put(Weapon.class, (type, data) -> { - Weapon weapon = new Weapon(); + var oc = resolve(data.getString("type", ""), Weapon.class); + data.remove("type"); + var weapon = make(oc); readFields(weapon, data); weapon.name = currentMod.name + "-" + weapon.name; return weapon; @@ -325,8 +329,17 @@ public class ContentParser{ return item; }, ContentType.item, parser(ContentType.item, Item::new), - ContentType.liquid, parser(ContentType.liquid, Liquid::new) - //ContentType.sector, parser(ContentType.sector, SectorPreset::new) + ContentType.liquid, parser(ContentType.liquid, Liquid::new), + ContentType.status, parser(ContentType.status, StatusEffect::new), + ContentType.sector, (TypeParser)(mod, name, value) -> { + if(value.isString()){ + return locate(ContentType.sector, name); + } + + if(!value.has("sector") || !value.get("sector").isNumber()) throw new RuntimeException("SectorPresets must have a sector number."); + + return new SectorPreset(name, locate(ContentType.planet, value.getString("planet", "serpulo")), value.getInt("sector")); + } ); private Prov unitType(JsonValue value){ @@ -714,14 +727,10 @@ public class ContentParser{ try{ return (Class)Class.forName(base); }catch(Exception ignored){ - //try to load from a mod's class loader - for(LoadedMod mod : mods.mods){ - if(mod.loader != null){ - try{ - return (Class)Class.forName(base, true, mod.loader); - }catch(Exception ignore){} - } - } + //try to use mod class loader + try{ + return (Class)Class.forName(base, true, mods.mainLoader()); + }catch(Exception ignore){} } } diff --git a/core/src/mindustry/mod/ModClassLoader.java b/core/src/mindustry/mod/ModClassLoader.java new file mode 100644 index 0000000000..74864cb81c --- /dev/null +++ b/core/src/mindustry/mod/ModClassLoader.java @@ -0,0 +1,35 @@ +package mindustry.mod; + +import arc.struct.*; + +public class ModClassLoader extends ClassLoader{ + private Seq children = new Seq<>(); + private volatile boolean inChild = false; + + public void addChild(ClassLoader child){ + children.add(child); + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException{ + //always try the superclass first + try{ + return super.loadClass(name, resolve); + }catch(ClassNotFoundException error){ + //a child may try to delegate class loading to its parent, which is *this class loader* - do not let that happen + if(inChild) throw error; + int size = children.size; + //if it doesn't exist in the main class loader, try all the children + for(int i = 0; i < size; i++){ + try{ + inChild = true; + var out = children.get(i).loadClass(name); + inChild = false; + return out; + }catch(ClassNotFoundException ignored){ + } + } + throw error; + } + } +} diff --git a/core/src/mindustry/mod/Mods.java b/core/src/mindustry/mod/Mods.java index a79634a43e..073c30f1b6 100644 --- a/core/src/mindustry/mod/Mods.java +++ b/core/src/mindustry/mod/Mods.java @@ -11,6 +11,7 @@ import arc.graphics.g2d.TextureAtlas.*; import arc.scene.ui.*; import arc.struct.*; import arc.util.*; +import arc.util.async.*; import arc.util.io.*; import arc.util.serialization.*; import arc.util.serialization.Jval.*; @@ -29,6 +30,7 @@ import java.util.*; import static mindustry.Vars.*; public class Mods implements Loadable{ + private AsyncExecutor async = new AsyncExecutor(); private Json json = new Json(); private @Nullable Scripts scripts; private ContentParser parser = new ContentParser(); @@ -37,15 +39,21 @@ public class Mods implements Loadable{ private int totalSprites; private MultiPacker packer; + private ModClassLoader mainLoader = new ModClassLoader(); Seq mods = new Seq<>(); private ObjectMap, ModMeta> metas = new ObjectMap<>(); - private boolean requiresReload, createdAtlas; + private boolean requiresReload; public Mods(){ Events.on(ClientLoadEvent.class, e -> Core.app.post(this::checkWarnings)); } + /** @return the main class loader for all mods */ + public ClassLoader mainLoader(){ + return mainLoader; + } + /** Returns a file named 'config.json' in a special folder for the specified plugin. * Call this in init(). */ public Fi getConfig(Mod mod){ @@ -97,9 +105,7 @@ public class Mods implements Loadable{ Core.settings.put("mod-" + loaded.name + "-enabled", true); sortMods(); //try to load the mod's icon so it displays on import - Core.app.post(() -> { - loadIcon(loaded); - }); + Core.app.post(() -> loadIcon(loaded)); return loaded; }catch(IOException e){ dest.delete(); @@ -117,16 +123,37 @@ public class Mods implements Loadable{ Time.mark(); packer = new MultiPacker(); + //all packing tasks to await + var tasks = new Seq>(); eachEnabled(mod -> { Seq sprites = mod.root.child("sprites").findAll(f -> f.extension().equals("png")); Seq overrides = mod.root.child("sprites-override").findAll(f -> f.extension().equals("png")); - packSprites(sprites, mod, true); - packSprites(overrides, mod, false); + + packSprites(sprites, mod, true, tasks); + packSprites(overrides, mod, false, tasks); + Log.debug("Packed @ images for mod '@'.", sprites.size + overrides.size, mod.meta.name); totalSprites += sprites.size + overrides.size; }); + for(var result : tasks){ + try{ + var packRun = result.get(); + if(packRun != null){ //can be null for very strange reasons, ignore if that's the case + try{ + //actually pack the image + packRun.run(); + }catch(Exception e){ //the image can fail to fit in the spritesheet + Log.err("Failed to fit image into the spritesheet, skipping."); + Log.err(e); + } + } + }catch(Exception e){ //this means loading the image failed, log it and move on + Log.err(e); + } + } + Log.debug("Time to pack textures: @", Time.elapsed()); } @@ -148,23 +175,29 @@ public class Mods implements Loadable{ } } - private void packSprites(Seq sprites, LoadedMod mod, boolean prefix){ + private void packSprites(Seq sprites, LoadedMod mod, boolean prefix, Seq> tasks){ + boolean linear = Core.settings.getBool("linear"); + for(Fi file : sprites){ - try(InputStream stream = file.read()){ - byte[] bytes = Streams.copyBytes(stream, Math.max((int)file.length(), 512)); - Pixmap pixmap = new Pixmap(bytes, 0, bytes.length); - packer.add(getPage(file), (prefix ? mod.name + "-" : "") + file.nameWithoutExtension(), new PixmapRegion(pixmap)); - pixmap.dispose(); - }catch(IOException e){ - Core.app.post(() -> { - Log.err("Error packing images for mod: @", mod.meta.name); - Log.err(e); - if(!headless) ui.showException(e); - }); - break; - } + //read and bleed pixmaps in parallel + tasks.add(async.submit(() -> { + try{ + Pixmap pix = new Pixmap(file.readBytes()); + //only bleeds when linear filtering is on at startup + if(linear){ + Pixmaps.bleed(pix); + } + //this returns a *runnable* which actually packs the resulting pixmap; this has to be done synchronously outside the method + return () -> { + packer.add(getPage(file), (prefix ? mod.name + "-" : "") + file.nameWithoutExtension(), new PixmapRegion(pix)); + pix.dispose(); + }; + }catch(Exception e){ + //rethrow exception with details about the cause of failure + throw new Exception("Failed to load image " + file + " for mod " + mod.name, e); + } + })); } - totalSprites += sprites.size; } @Override @@ -176,32 +209,77 @@ public class Mods implements Loadable{ //get textures packed if(totalSprites > 0){ - if(!createdAtlas) Core.atlas = new TextureAtlas(Core.files.internal("sprites/sprites.atlas")); - createdAtlas = true; for(AtlasRegion region : Core.atlas.getRegions()){ + //TODO PageType completely breaks down with multiple pages. PageType type = getPage(region); if(!packer.has(type, region.name)){ - packer.add(type, region.name, Core.atlas.getPixmap(region)); + packer.add(type, region.name, Core.atlas.getPixmap(region), region.splits, region.pads); } } - TextureFilter filter = Core.settings.getBool("linear") ? TextureFilter.linear : TextureFilter.nearest; + Core.atlas.dispose(); - //flush so generators can use these sprites - packer.flush(filter, Core.atlas); + //dead shadow-atlas for getting regions, but not pixmaps + var shadow = Core.atlas; + //dummy texture atlas that returns the 'shadow' regions; used for mod loading + Core.atlas = new TextureAtlas(){ + + @Override + public AtlasRegion find(String name){ + var base = packer.get(name); + + if(base != null){ + var reg = new AtlasRegion(shadow.find(name).texture, base.x, base.y, base.width, base.height); + reg.name = name; + reg.pixmapRegion = base; + return reg; + } + + return shadow.find(name); + } + + @Override + public boolean isFound(TextureRegion region){ + return region != shadow.find("error"); + } + + @Override + public TextureRegion find(String name, TextureRegion def){ + return !has(name) ? def : find(name); + } + + @Override + public boolean has(String s){ + return shadow.has(s) || packer.get(s) != null; + } + + //return the *actual* pixmap regions, not the disposed ones. + @Override + public PixmapRegion getPixmap(AtlasRegion region){ + PixmapRegion out = packer.get(region.name); + //this should not happen in normal situations + if(out == null) return packer.get("error"); + return out; + } + }; + + TextureFilter filter = Core.settings.getBool("linear") ? TextureFilter.linear : TextureFilter.nearest; //generate new icons for(Seq arr : content.getContentMap()){ arr.each(c -> { if(c instanceof UnlockableContent u && c.minfo.mod != null){ u.load(); + u.loadIcon(); u.createIcons(packer); } }); } + //dispose old atlas data Core.atlas = packer.flush(filter, new TextureAtlas()); + Core.atlas.setErrorRegion("error"); Log.debug("Total pages: @", Core.atlas.getTextures().size); } @@ -217,6 +295,7 @@ public class Mods implements Loadable{ region.texture == Core.atlas.find("stone1").texture ? PageType.environment : region.texture == Core.atlas.find("clear-editor").texture ? PageType.editor : region.texture == Core.atlas.find("whiteui").texture ? PageType.ui : + region.texture == Core.atlas.find("rubble-1-0").texture ? PageType.rubble : PageType.main; } @@ -225,6 +304,7 @@ public class Mods implements Loadable{ return parent.equals("environment") ? PageType.environment : parent.equals("editor") ? PageType.editor : + parent.equals("rubble") ? PageType.editor : parent.equals("ui") || file.parent().parent().name().equals("ui") ? PageType.ui : PageType.main; } @@ -696,12 +776,12 @@ public class Mods implements Loadable{ Version.isAtLeast(meta.minGameVersion) && (meta.getMinMajor() >= 105 || headless) ){ - if(ios){ throw new IllegalArgumentException("Java class mods are not supported on iOS."); } - loader = platform.loadJar(sourceFile, mainClass); + loader = platform.loadJar(sourceFile, mainLoader); + mainLoader.addChild(loader); Class main = Class.forName(mainClass, true, loader); metas.put(main, meta); mainMod = (Mod)main.getDeclaredConstructor().newInstance(); diff --git a/core/src/mindustry/mod/Scripts.java b/core/src/mindustry/mod/Scripts.java index 1634a0bde3..0517d99311 100644 --- a/core/src/mindustry/mod/Scripts.java +++ b/core/src/mindustry/mod/Scripts.java @@ -25,12 +25,13 @@ public class Scripts implements Disposable{ private static final Seq blacklist = Seq.with(".net.", "java.net", "files", "reflect", "javax", "rhino", "file", "channels", "jdk", "runtime", "util.os", "rmi", "security", "org.", "sun.", "beans", "sql", "http", "exec", "compiler", "process", "system", ".awt", "socket", "classloader", "oracle", "invoke", "java.util.function", "java.util.stream", "org.", "mod.classmap"); - private static final Seq whitelist = Seq.with("mindustry.net", "netserver", "netclient", "com.sun.proxy.$proxy", "jdk.proxy1", "mindustry.gen.", - "mindustry.logic.", "mindustry.async.", "saveio", "systemcursor", "filetreeinitevent"); + private static final Seq whitelist = Seq.with("mindustry.net", "netserver", "netclient", "com.sun.proxy.$proxy", "jdk.proxy", "mindustry.gen.", + "mindustry.logic.", "mindustry.async.", "saveio", "systemcursor", "filetreeinitevent", "asyncexecutor"); private final Context context; private final Scriptable scope; private boolean errored; + LoadedMod currentMod = null; public static boolean allowClass(String type){ @@ -41,10 +42,6 @@ public class Scripts implements Disposable{ Time.mark(); context = Vars.platform.getScriptContext(); - context.setClassShutter(Scripts::allowClass); - context.getWrapFactory().setJavaPrimitiveWrap(false); - context.setLanguageVersion(Context.VERSION_ES6); - scope = new ImporterTopLevel(context); new RequireBuilder() diff --git a/core/src/mindustry/net/Administration.java b/core/src/mindustry/net/Administration.java index 5f78270421..306a6d8291 100644 --- a/core/src/mindustry/net/Administration.java +++ b/core/src/mindustry/net/Administration.java @@ -369,7 +369,7 @@ public class Administration{ ObjectSet result = new ObjectSet<>(); for(PlayerInfo info : playerInfo.values()){ - if(info.lastName.equalsIgnoreCase(name) || (info.names.contains(name, false)) + if(info.lastName.equalsIgnoreCase(name) || info.names.contains(name, false) || Strings.stripColors(Strings.stripColors(info.lastName)).equals(name) || info.ips.contains(name, false) || info.id.equals(name)){ result.add(info); @@ -617,6 +617,9 @@ public class Administration{ /** valid for unit-type events only, and even in that case may be null. */ public @Nullable Unit unit; + /** valid only for removePlanned events only; contains packed positions. */ + public @Nullable int[] plans; + public PlayerAction set(Player player, ActionType type, Tile tile){ this.player = player; this.type = type; @@ -641,11 +644,12 @@ public class Administration{ tile = null; block = null; unit = null; + plans = null; } } public enum ActionType{ - breakBlock, placeBlock, rotate, configure, withdrawItem, depositItem, control, command + breakBlock, placeBlock, rotate, configure, withdrawItem, depositItem, control, buildSelect, command, removePlanned } } diff --git a/core/src/mindustry/net/ArcNetProvider.java b/core/src/mindustry/net/ArcNetProvider.java index de88aabdac..a18c8d57de 100644 --- a/core/src/mindustry/net/ArcNetProvider.java +++ b/core/src/mindustry/net/ArcNetProvider.java @@ -2,14 +2,17 @@ package mindustry.net; import arc.*; import arc.func.*; +import arc.math.*; import arc.net.*; import arc.net.FrameworkMessage.*; import arc.struct.*; import arc.util.*; +import arc.util.Log.*; import arc.util.async.*; -import arc.util.pooling.*; +import arc.util.io.*; import mindustry.net.Net.*; import mindustry.net.Packets.*; +import net.jpountz.lz4.*; import java.io.*; import java.net.*; @@ -28,8 +31,15 @@ public class ArcNetProvider implements NetProvider{ final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>(); Thread serverThread; + private static final LZ4FastDecompressor decompressor = LZ4Factory.fastestInstance().fastDecompressor(); + private static final LZ4Compressor compressor = LZ4Factory.fastestInstance().fastCompressor(); + public ArcNetProvider(){ - ArcNet.errorHandler = e -> Log.debug(Strings.getStackTrace(e)); + ArcNet.errorHandler = e -> { + if(Log.level == LogLevel.debug){ + Log.debug(Strings.getStackTrace(e)); + } + }; client = new Client(8192, 8192, new PacketSerializer()); client.setDiscoveryPacket(packetSupplier); @@ -56,11 +66,11 @@ public class ArcNetProvider implements NetProvider{ @Override public void received(Connection connection, Object object){ - if(object instanceof FrameworkMessage) return; + if(!(object instanceof Packet p)) return; Core.app.post(() -> { try{ - net.handleClientReceived(object); + net.handleClientReceived(p); }catch(Throwable e){ net.handleException(e); } @@ -111,13 +121,13 @@ public class ArcNetProvider implements NetProvider{ @Override public void received(Connection connection, Object object){ ArcConnection k = getByArcID(connection.getID()); - if(object instanceof FrameworkMessage || k == null) return; + if(!(object instanceof Packet pack) || k == null) return; Core.app.post(() -> { try{ - net.handleServerReceived(k, object); + net.handleServerReceived(k, pack); }catch(Throwable e){ - e.printStackTrace(); + Log.err(e); } }); } @@ -163,9 +173,9 @@ public class ArcNetProvider implements NetProvider{ } @Override - public void sendClient(Object object, SendMode mode){ + public void sendClient(Object object, boolean reliable){ try{ - if(mode == SendMode.tcp){ + if(reliable){ client.sendTCP(object); }else{ client.sendUDP(object); @@ -174,8 +184,6 @@ public class ArcNetProvider implements NetProvider{ }catch(BufferOverflowException | BufferUnderflowException e){ net.showError(e); } - - Pools.free(object); } @Override @@ -293,7 +301,7 @@ public class ArcNetProvider implements NetProvider{ //send an object so the receiving side knows how to handle the following chunks StreamBegin begin = new StreamBegin(); begin.total = stream.stream.available(); - begin.type = Registrator.getID(stream.getClass()); + begin.type = Net.getPacketId(stream); connection.sendTCP(begin); id = begin.id; } @@ -309,9 +317,9 @@ public class ArcNetProvider implements NetProvider{ } @Override - public void send(Object object, SendMode mode){ + public void send(Object object, boolean reliable){ try{ - if(mode == SendMode.tcp){ + if(reliable){ connection.sendTCP(object); }else{ connection.sendUDP(object); @@ -332,32 +340,129 @@ public class ArcNetProvider implements NetProvider{ } } - @SuppressWarnings("unchecked") public static class PacketSerializer implements NetSerializer{ + //for debugging total read/write speeds + private static final boolean debug = false; + + ThreadLocal decompressBuffer = new ThreadLocal<>(){ + @Override + protected ByteBuffer initialValue(){ + return ByteBuffer.allocate(32768); + } + }; + ThreadLocal reads = new ThreadLocal<>(){ + @Override + protected Reads initialValue(){ + return new Reads(new ByteBufferInput(decompressBuffer.get())); + } + }; + ThreadLocal writes = new ThreadLocal<>(){ + @Override + protected Writes initialValue(){ + return new Writes(new ByteBufferOutput(decompressBuffer.get())); + } + }; + + //for debugging network write counts + static WindowedMean upload = new WindowedMean(5), download = new WindowedMean(5); + static long lastUpload, lastDownload, uploadAccum, downloadAccum; + static int lastPos; @Override public Object read(ByteBuffer byteBuffer){ + if(debug){ + if(Time.timeSinceMillis(lastDownload) >= 1000){ + lastDownload = Time.millis(); + download.add(downloadAccum); + downloadAccum = 0; + Log.info("Download: @ b/s", download.mean()); + } + downloadAccum += byteBuffer.remaining(); + } + byte id = byteBuffer.get(); if(id == -2){ return readFramework(byteBuffer); }else{ - Packet packet = Pools.obtain((Class)Registrator.getByID(id).type, (Prov)Registrator.getByID(id).constructor); - packet.read(byteBuffer); + //read length int, followed by compressed lz4 data + //TODO not thread safe!!! + Packet packet = Net.newPacket(id); + var buffer = decompressBuffer.get(); + int length = byteBuffer.getShort() & 0xffff; + byte compression = byteBuffer.get(); + + //no compression, copy over buffer + if(compression == 0){ + buffer.position(0).limit(length); + buffer.put(byteBuffer.array(), byteBuffer.position(), length); + buffer.position(0); + packet.read(reads.get()); + //move read packets forward + byteBuffer.position(byteBuffer.position() + buffer.position()); + }else{ + //decompress otherwise + int read = decompressor.decompress(byteBuffer, byteBuffer.position(), buffer, 0, length); + + buffer.position(0); + buffer.limit(length); + packet.read(reads.get()); + //move buffer forward based on bytes read by decompressor + byteBuffer.position(byteBuffer.position() + read); + } + return packet; } } @Override public void write(ByteBuffer byteBuffer, Object o){ - if(o instanceof FrameworkMessage){ + if(debug){ + lastPos = byteBuffer.position(); + } + + //write raw buffer + if(o instanceof ByteBuffer raw){ + byteBuffer.put(raw); + }else if(o instanceof FrameworkMessage msg){ byteBuffer.put((byte)-2); //code for framework message - writeFramework(byteBuffer, (FrameworkMessage)o); + writeFramework(byteBuffer, msg); }else{ - if(!(o instanceof Packet)) throw new RuntimeException("All sent objects must implement be Packets! Class: " + o.getClass()); - byte id = Registrator.getID(o.getClass()); - if(id == -1) throw new RuntimeException("Unregistered class: " + o.getClass()); + if(!(o instanceof Packet pack)) throw new RuntimeException("All sent objects must implement be Packets! Class: " + o.getClass()); + byte id = Net.getPacketId(pack); byteBuffer.put(id); - ((Packet)o).write(byteBuffer); + + var temp = decompressBuffer.get(); + temp.position(0); + temp.limit(temp.capacity()); + pack.write(writes.get()); + + short length = (short)temp.position(); + + //write length, uncompressed + byteBuffer.putShort(length); + + //don't bother with small packets + if(length < 36 || pack instanceof StreamChunk){ + //write direct contents... + byteBuffer.put((byte)0); //0 = no compression + byteBuffer.put(temp.array(), 0, length); + }else{ + byteBuffer.put((byte)1); //1 = compression + //write compressed data; this does not modify position! + int written = compressor.compress(temp, 0, temp.position(), byteBuffer, byteBuffer.position(), byteBuffer.remaining()); + //skip to indicate the written, compressed data + byteBuffer.position(byteBuffer.position() + written); + } + } + + if(debug){ + if(Time.timeSinceMillis(lastUpload) >= 1000){ + lastUpload = Time.millis(); + upload.add(uploadAccum); + uploadAccum = 0; + Log.info("Upload: @ b/s", upload.mean()); + } + uploadAccum += byteBuffer.position() - lastPos; } } @@ -388,9 +493,9 @@ public class ArcNetProvider implements NetProvider{ p.isReply = buffer.get() == 1; return p; }else if(id == 1){ - return new DiscoverHost(); + return FrameworkMessage.discoverHost; }else if(id == 2){ - return new KeepAlive(); + return FrameworkMessage.keepAlive; }else if(id == 3){ RegisterUDP p = new RegisterUDP(); p.connectionID = buffer.getInt(); diff --git a/core/src/mindustry/net/Net.java b/core/src/mindustry/net/Net.java index 194c6dabbf..689834c07b 100644 --- a/core/src/mindustry/net/Net.java +++ b/core/src/mindustry/net/Net.java @@ -5,33 +5,62 @@ import arc.func.*; import arc.net.*; import arc.struct.*; import arc.util.*; -import arc.util.pooling.*; import mindustry.gen.*; import mindustry.net.Packets.*; import mindustry.net.Streamable.*; -import net.jpountz.lz4.*; import java.io.*; import java.nio.*; import java.nio.channels.*; +import static arc.util.Log.*; import static mindustry.Vars.*; @SuppressWarnings("unchecked") public class Net{ + private static Seq> packetProvs = new Seq<>(); + private static Seq> packetClasses = new Seq<>(); + private static ObjectIntMap> packetToId = new ObjectIntMap<>(); + private boolean server; private boolean active; private boolean clientLoaded; private @Nullable StreamBuilder currentStream; - private final Seq packetQueue = new Seq<>(); + private final Seq packetQueue = new Seq<>(); private final ObjectMap, Cons> clientListeners = new ObjectMap<>(); private final ObjectMap, Cons2> serverListeners = new ObjectMap<>(); private final IntMap streams = new IntMap<>(); private final NetProvider provider; - private final LZ4FastDecompressor decompressor = LZ4Factory.fastestInstance().fastDecompressor(); - private final LZ4Compressor compressor = LZ4Factory.fastestInstance().fastCompressor(); + + static{ + registerPacket(StreamBegin::new); + registerPacket(StreamChunk::new); + registerPacket(WorldStream::new); + registerPacket(ConnectPacket::new); + + //register generated packet classes + Call.registerPackets(); + } + + /** Registers a new packet type for serialization. */ + public static void registerPacket(Prov cons){ + packetProvs.add(cons); + var t = cons.get(); + packetClasses.add(t.getClass()); + packetToId.put(t.getClass(), packetProvs.size - 1); + } + + public static byte getPacketId(Packet packet){ + int id = packetToId.get(packet.getClass(), -1); + if(id == -1) throw new ArcRuntimeException("Unknown packet type: " + packet.getClass()); + return (byte)id; + } + + public static T newPacket(byte id){ + return ((Prov)packetProvs.get(id & 0xff)).get(); + } public Net(NetProvider provider){ this.provider = provider; @@ -39,9 +68,9 @@ public class Net{ public void handleException(Throwable e){ if(e instanceof ArcNetException){ - Core.app.post(() -> showError(new IOException("mismatch"))); + Core.app.post(() -> showError(new IOException("mismatch", e))); }else if(e instanceof ClosedChannelException){ - Core.app.post(() -> showError(new IOException("alreadyconnected"))); + Core.app.post(() -> showError(new IOException("alreadyconnected", e))); }else{ Core.app.post(() -> showError(e)); } @@ -171,14 +200,6 @@ public class Net{ active = false; } - public byte[] compressSnapshot(byte[] input){ - return compressor.compress(input); - } - - public byte[] decompressSnapshot(byte[] input, int size){ - return decompressor.decompress(input, size); - } - /** * Starts discovering servers on a different thread. * Callback is run on the main Arc thread. @@ -195,21 +216,21 @@ public class Net{ } /** Send an object to all connected clients, or to the server if this is a client.*/ - public void send(Object object, SendMode mode){ + public void send(Object object, boolean reliable){ if(server){ for(NetConnection con : provider.getConnections()){ - con.send(object, mode); + con.send(object, reliable); } }else{ - provider.sendClient(object, mode); + provider.sendClient(object, reliable); } } /** Send an object to everyone EXCEPT a certain client. Server-side only.*/ - public void sendExcept(NetConnection except, Object object, SendMode mode){ + public void sendExcept(NetConnection except, Object object, boolean reliable){ for(NetConnection con : getConnections()){ if(con != except){ - con.send(object, mode); + con.send(object, reliable); } } } @@ -235,7 +256,7 @@ public class Net{ /** * Call to handle a packet being received for the client. */ - public void handleClientReceived(Object object){ + public void handleClientReceived(Packet object){ if(object instanceof StreamBegin b){ streams.put(b.id, currentStream = new StreamBuilder(b)); @@ -251,33 +272,43 @@ public class Net{ handleClientReceived(builder.build()); currentStream = null; } - }else if(clientListeners.get(object.getClass()) != null){ + }else{ + int p = object.getPriority(); - if(clientLoaded || ((object instanceof Packet) && ((Packet)object).isImportant())){ + if(clientLoaded || p == Packet.priorityHigh){ if(clientListeners.get(object.getClass()) != null){ clientListeners.get(object.getClass()).get(object); + }else{ + object.handleClient(); } - Pools.free(object); - }else if(!((object instanceof Packet) && ((Packet)object).isUnimportant())){ + }else if(p != Packet.priorityLow){ packetQueue.add(object); - }else{ - Pools.free(object); } - }else{ - Log.err("Unhandled packet type: '@'!", object); } } /** * Call to handle a packet being received for the server. */ - public void handleServerReceived(NetConnection connection, Object object){ - - if(serverListeners.get(object.getClass()) != null){ - serverListeners.get(object.getClass()).get(connection, object); - Pools.free(object); - }else{ - Log.err("Unhandled packet type: '@'!", object.getClass()); + public void handleServerReceived(NetConnection connection, Packet object){ + try{ + //handle object normally + if(serverListeners.get(object.getClass()) != null){ + serverListeners.get(object.getClass()).get(connection, object); + }else{ + object.handleServer(connection); + } + }catch(ValidateException e){ + //ignore invalid actions + debug("Validation failed for '@': @", e.player, e.getMessage()); + }catch(RuntimeException e){ + //ignore indirect ValidateException-s + if(e.getCause() instanceof ValidateException v){ + debug("Validation failed for '@': @", v.player, v.getMessage()); + }else{ + //rethrow if not ValidateException + throw e; + } } } @@ -315,17 +346,13 @@ public class Net{ active = false; } - public enum SendMode{ - tcp, udp - } - /** Networking implementation. */ public interface NetProvider{ /** Connect to a server. */ void connectClient(String ip, int port, Runnable success) throws IOException; /** Send an object to the server. */ - void sendClient(Object object, SendMode mode); + void sendClient(Object object, boolean reliable); /** Disconnect from the server. */ void disconnectClient(); @@ -355,4 +382,5 @@ public class Net{ closeServer(); } } + } diff --git a/core/src/mindustry/net/NetConnection.java b/core/src/mindustry/net/NetConnection.java index 0b1af2d769..a7bcc9b7fd 100644 --- a/core/src/mindustry/net/NetConnection.java +++ b/core/src/mindustry/net/NetConnection.java @@ -5,7 +5,6 @@ import arc.util.*; import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.net.Administration.*; -import mindustry.net.Net.*; import mindustry.net.Packets.*; import java.io.*; @@ -40,7 +39,7 @@ public abstract class NetConnection{ public void kick(KickReason reason){ if(kicked) return; - Log.info("Kicking connection @; Reason: @", address, reason.name()); + Log.info("Kicking connection @ / @; Reason: @", address, uuid, reason.name()); if((reason == KickReason.kick || reason == KickReason.banned || reason == KickReason.vote)){ PlayerInfo info = netServer.admins.getInfo(uuid); @@ -65,7 +64,7 @@ public abstract class NetConnection{ public void kick(String reason, long kickDuration){ if(kicked) return; - Log.info("Kicking connection @; Reason: @", address, reason.replace("\n", " ")); + Log.info("Kicking connection @ / @; Reason: @", address, uuid, reason.replace("\n", " ")); netServer.admins.handleKicked(uuid, address, kickDuration); @@ -86,8 +85,8 @@ public abstract class NetConnection{ int cid; StreamBegin begin = new StreamBegin(); begin.total = stream.stream.available(); - begin.type = Registrator.getID(stream.getClass()); - send(begin, SendMode.tcp); + begin.type = Net.getPacketId(stream); + send(begin, true); cid = begin.id; while(stream.stream.available() > 0){ @@ -97,14 +96,14 @@ public abstract class NetConnection{ StreamChunk chunk = new StreamChunk(); chunk.id = cid; chunk.data = bytes; - send(chunk, SendMode.tcp); + send(chunk, true); } }catch(IOException e){ throw new RuntimeException(e); } } - public abstract void send(Object object, SendMode mode); + public abstract void send(Object object, boolean reliable); public abstract void close(); } diff --git a/core/src/mindustry/net/NetworkIO.java b/core/src/mindustry/net/NetworkIO.java index f4494e1c4f..9253f71181 100644 --- a/core/src/mindustry/net/NetworkIO.java +++ b/core/src/mindustry/net/NetworkIO.java @@ -46,6 +46,7 @@ public class NetworkIO{ SaveIO.getSaveWriter().writeContentHeader(stream); SaveIO.getSaveWriter().writeMap(stream); + SaveIO.getSaveWriter().writeTeamBlocks(stream); }catch(IOException e){ throw new RuntimeException(e); } @@ -70,6 +71,7 @@ public class NetworkIO{ SaveIO.getSaveWriter().readContentHeader(stream); SaveIO.getSaveWriter().readMap(stream, world.context); + SaveIO.getSaveWriter().readTeamBlocks(stream); }catch(IOException e){ throw new RuntimeException(e); }finally{ diff --git a/core/src/mindustry/net/Packet.java b/core/src/mindustry/net/Packet.java index 2aaa2a05c0..a6200547cc 100644 --- a/core/src/mindustry/net/Packet.java +++ b/core/src/mindustry/net/Packet.java @@ -1,19 +1,24 @@ package mindustry.net; -import arc.util.pooling.Pool.*; +import arc.util.io.*; -import java.nio.*; +public abstract class Packet{ + //these are constants because I don't want to bother making an enum to mirror the annotation enum -public interface Packet extends Poolable{ - default void read(ByteBuffer buffer){} - default void write(ByteBuffer buffer){} - default void reset(){} + /** Does not get handled unless client is connected. */ + public static final int priorityLow = 0; + /** Gets put in a queue and processed if not connected. */ + public static final int priorityNormal = 1; + /** Gets handled immediately, regardless of connection status. */ + public static final int priorityHigh = 2; - default boolean isImportant(){ - return false; + public void read(Reads read){} + public void write(Writes write){} + + public int getPriority(){ + return priorityNormal; } - default boolean isUnimportant(){ - return false; - } + public void handleClient(){} + public void handleServer(NetConnection con){} } diff --git a/core/src/mindustry/net/Packets.java b/core/src/mindustry/net/Packets.java index 779be43b03..a1cd13670c 100644 --- a/core/src/mindustry/net/Packets.java +++ b/core/src/mindustry/net/Packets.java @@ -2,13 +2,12 @@ package mindustry.net; import arc.*; import arc.struct.*; +import arc.util.*; import arc.util.io.*; import arc.util.serialization.*; import mindustry.core.*; import mindustry.io.*; -import java.io.*; -import java.nio.*; import java.util.zip.*; /** @@ -45,21 +44,23 @@ public class Packets{ kick, ban, trace, wave } - public static class Connect implements Packet{ + /** Generic client connection event. */ + public static class Connect extends Packet{ public String addressTCP; @Override - public boolean isImportant(){ - return true; + public int getPriority(){ + return priorityHigh; } } - public static class Disconnect implements Packet{ + /** Generic client disconnection event. */ + public static class Disconnect extends Packet{ public String reason; @Override - public boolean isImportant(){ - return true; + public int getPriority(){ + return priorityHigh; } } @@ -67,55 +68,8 @@ public class Packets{ } - public static class InvokePacket implements Packet{ - private static ReusableByteInStream bin; - private static Reads read = new Reads(new DataInputStream(bin = new ReusableByteInStream())); - - public byte type, priority; - - public byte[] bytes; - public int length; - - @Override - public void read(ByteBuffer buffer){ - type = buffer.get(); - priority = buffer.get(); - short writeLength = buffer.getShort(); - bytes = new byte[writeLength]; - buffer.get(bytes); - } - - @Override - public void write(ByteBuffer buffer){ - buffer.put(type); - buffer.put(priority); - buffer.putShort((short)length); - buffer.put(bytes, 0, length); - } - - @Override - public void reset(){ - priority = 0; - } - - @Override - public boolean isImportant(){ - return priority == 1; - } - - @Override - public boolean isUnimportant(){ - return priority == 2; - } - - public Reads reader(){ - bin.setBytes(bytes); - return read; - } - } - /** Marks the beginning of a stream. */ - public static class StreamBegin implements Packet{ + public static class StreamBegin extends Packet{ private static int lastid; public int id = lastid++; @@ -123,40 +77,39 @@ public class Packets{ public byte type; @Override - public void write(ByteBuffer buffer){ - buffer.putInt(id); - buffer.putInt(total); - buffer.put(type); + public void write(Writes buffer){ + buffer.i(id); + buffer.i(total); + buffer.b(type); } @Override - public void read(ByteBuffer buffer){ - id = buffer.getInt(); - total = buffer.getInt(); - type = buffer.get(); + public void read(Reads buffer){ + id = buffer.i(); + total = buffer.i(); + type = buffer.b(); } } - public static class StreamChunk implements Packet{ + public static class StreamChunk extends Packet{ public int id; public byte[] data; @Override - public void write(ByteBuffer buffer){ - buffer.putInt(id); - buffer.putShort((short)data.length); - buffer.put(data); + public void write(Writes buffer){ + buffer.i(id); + buffer.s((short)data.length); + buffer.b(data); } @Override - public void read(ByteBuffer buffer){ - id = buffer.getInt(); - data = new byte[buffer.getShort()]; - buffer.get(data); + public void read(Reads buffer){ + id = buffer.i(); + data = buffer.b(buffer.s()); } } - public static class ConnectPacket implements Packet{ + public static class ConnectPacket extends Packet{ public int version; public String versionType; public Seq mods; @@ -165,40 +118,41 @@ public class Packets{ public int color; @Override - public void write(ByteBuffer buffer){ - buffer.putInt(Version.build); + public void write(Writes buffer){ + buffer.i(Version.build); TypeIO.writeString(buffer, versionType); TypeIO.writeString(buffer, name); TypeIO.writeString(buffer, locale); TypeIO.writeString(buffer, usid); byte[] b = Base64Coder.decode(uuid); - buffer.put(b); + buffer.b(b); CRC32 crc = new CRC32(); crc.update(Base64Coder.decode(uuid), 0, b.length); - buffer.putLong(crc.getValue()); + buffer.l(crc.getValue()); - buffer.put(mobile ? (byte)1 : 0); - buffer.putInt(color); - buffer.put((byte)mods.size); + Log.info("CRC value sent: @", Long.toHexString(crc.getValue())); + + buffer.b(mobile ? (byte)1 : 0); + buffer.i(color); + buffer.b((byte)mods.size); for(int i = 0; i < mods.size; i++){ TypeIO.writeString(buffer, mods.get(i)); } } @Override - public void read(ByteBuffer buffer){ - version = buffer.getInt(); + public void read(Reads buffer){ + version = buffer.i(); versionType = TypeIO.readString(buffer); name = TypeIO.readString(buffer); locale = TypeIO.readString(buffer); usid = TypeIO.readString(buffer); - byte[] idbytes = new byte[16]; - buffer.get(idbytes); + byte[] idbytes = buffer.b(16); uuid = new String(Base64Coder.encode(idbytes)); - mobile = buffer.get() == 1; - color = buffer.getInt(); - int totalMods = buffer.get(); + mobile = buffer.b() == 1; + color = buffer.i(); + int totalMods = buffer.b(); mods = new Seq<>(totalMods); for(int i = 0; i < totalMods; i++){ mods.add(TypeIO.readString(buffer)); diff --git a/core/src/mindustry/net/Registrator.java b/core/src/mindustry/net/Registrator.java deleted file mode 100644 index 2280d016e0..0000000000 --- a/core/src/mindustry/net/Registrator.java +++ /dev/null @@ -1,45 +0,0 @@ -package mindustry.net; - -import arc.func.*; -import arc.struct.*; -import mindustry.net.Packets.*; - -public class Registrator{ - private static final ClassEntry[] classes = { - new ClassEntry(StreamBegin.class, StreamBegin::new), - new ClassEntry(StreamChunk.class, StreamChunk::new), - new ClassEntry(WorldStream.class, WorldStream::new), - new ClassEntry(ConnectPacket.class, ConnectPacket::new), - new ClassEntry(InvokePacket.class, InvokePacket::new) - }; - private static final ObjectIntMap> ids = new ObjectIntMap<>(); - - static{ - if(classes.length > 127) throw new RuntimeException("Can't have more than 127 registered classes!"); - for(int i = 0; i < classes.length; i++){ - ids.put(classes[i].type, i); - } - } - - public static ClassEntry getByID(byte id){ - return classes[id]; - } - - public static byte getID(Class type){ - return (byte)ids.get(type, -1); - } - - public static ClassEntry[] getClasses(){ - return classes; - } - - public static class ClassEntry{ - public final Class type; - public final Prov constructor; - - public ClassEntry(Class type, Prov constructor){ - this.type = type; - this.constructor = constructor; - } - } -} diff --git a/core/src/mindustry/net/Streamable.java b/core/src/mindustry/net/Streamable.java index 52ce5d36d8..c150fc26f3 100644 --- a/core/src/mindustry/net/Streamable.java +++ b/core/src/mindustry/net/Streamable.java @@ -4,12 +4,12 @@ import mindustry.net.Packets.*; import java.io.*; -public class Streamable implements Packet{ +public class Streamable extends Packet{ public transient ByteArrayInputStream stream; @Override - public boolean isImportant(){ - return true; + public int getPriority(){ + return priorityHigh; } public static class StreamBuilder{ @@ -37,7 +37,7 @@ public class Streamable implements Packet{ } public Streamable build(){ - Streamable s = (Streamable)Registrator.getByID(type).constructor.get(); + Streamable s = Net.newPacket(type); s.stream = new ByteArrayInputStream(stream.toByteArray()); return s; } diff --git a/desktop/src/mindustry/desktop/steam/SAchievement.java b/core/src/mindustry/service/Achievement.java similarity index 83% rename from desktop/src/mindustry/desktop/steam/SAchievement.java rename to core/src/mindustry/service/Achievement.java index 073ff23942..857baca118 100644 --- a/desktop/src/mindustry/desktop/steam/SAchievement.java +++ b/core/src/mindustry/service/Achievement.java @@ -1,6 +1,8 @@ -package mindustry.desktop.steam; +package mindustry.service; -public enum SAchievement{ +import static mindustry.Vars.*; + +public enum Achievement{ kill1kEnemies(SStat.unitsDestroyed, 1000), kill100kEnemies(SStat.unitsDestroyed, 100_000), launch100kItems(SStat.itemsLaunched, 100_000), @@ -62,22 +64,24 @@ public enum SAchievement{ private final SStat stat; private final int statGoal; - public static final SAchievement[] all = values(); + private boolean completed = false; + + public static final Achievement[] all = values(); /** Creates an achievement that is triggered when this stat reaches a number.*/ - SAchievement(SStat stat, int goal){ + Achievement(SStat stat, int goal){ this.stat = stat; this.statGoal = goal; } - SAchievement(){ + Achievement(){ this(null, 0); } public void complete(){ if(!isAchieved()){ - SVars.stats.stats.setAchievement(name()); - SVars.stats.stats.storeStats(); + service.completeAchievement(name()); + service.storeStats(); } } @@ -88,6 +92,9 @@ public enum SAchievement{ } public boolean isAchieved(){ - return SVars.stats.stats.isAchieved(name(), false); + if(completed){ + return true; + } + return (completed = service.isAchieved(name())); } } diff --git a/core/src/mindustry/service/GameService.java b/core/src/mindustry/service/GameService.java new file mode 100644 index 0000000000..97b4cd002b --- /dev/null +++ b/core/src/mindustry/service/GameService.java @@ -0,0 +1,358 @@ +package mindustry.service; + +import arc.*; +import arc.struct.*; +import arc.util.*; +import mindustry.*; +import mindustry.content.*; +import mindustry.ctype.*; +import mindustry.entities.units.*; +import mindustry.game.EventType.*; +import mindustry.game.SectorInfo.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.blocks.distribution.*; + +import static mindustry.Vars.*; +import static mindustry.service.Achievement.*; + +/** + * Interface for handling game service across multiple platforms. + * + * This includes: + * - Desktop (Steam) + * - iOS (Game Center) + * - Android (Google Play Games) + * + * The default implementation does nothing. + * */ +public class GameService{ + private ObjectSet blocksBuilt = new ObjectSet<>(), unitsBuilt = new ObjectSet<>(); + private ObjectSet t5s = new ObjectSet<>(); + private IntSet checked = new IntSet(); + + /** Begin listening for new achievement events, once the game service is activated. This can be called at any time, but only once. */ + public void init(){ + if(clientLoaded){ + registerEvents(); + }else{ + Events.on(ClientLoadEvent.class, e -> registerEvents()); + } + } + + public boolean enabled(){ + return false; + } + + public void completeAchievement(String name){ + + } + + public boolean isAchieved(String name){ + return false; + } + + public int getStat(String name, int def){ + return def; + } + + public void setStat(String name, int amount){ + + } + + public void storeStats(){ + + } + + private void registerEvents(){ + unitsBuilt = Core.settings.getJson("units-built" , ObjectSet.class, String.class, ObjectSet::new); + blocksBuilt = Core.settings.getJson("blocks-built" , ObjectSet.class, String.class, ObjectSet::new); + t5s = ObjectSet.with(UnitTypes.omura, UnitTypes.reign, UnitTypes.toxopid, UnitTypes.eclipse, UnitTypes.oct, UnitTypes.corvus); + + //periodically check for various conditions + float updateInterval = 2f; + Timer.schedule(this::checkUpdate, updateInterval, updateInterval); + + if(Items.thorium.unlocked()) obtainThorium.complete(); + if(Items.titanium.unlocked()) obtainTitanium.complete(); + if(!content.sectors().contains(UnlockableContent::locked)){ + unlockAllZones.complete(); + } + + Events.on(UnitDestroyEvent.class, e -> { + if(campaign()){ + if(e.unit.team != Vars.player.team()){ + SStat.unitsDestroyed.add(); + + if(e.unit.isBoss()){ + SStat.bossesDefeated.add(); + } + } + } + }); + + Events.on(TurnEvent.class, e -> { + float total = 0; + for(Planet planet : content.planets()){ + for(Sector sec : planet.sectors){ + if(sec.hasBase()){ + for(ExportStat v : sec.info.production.values()){ + if(v.mean > 0) total += v.mean * 60; + } + } + } + } + + SStat.maxProduction.max(Math.round(total)); + }); + + Events.run(Trigger.newGame, () -> Core.app.post(() -> { + if(campaign() && player.core() != null && player.core().items.total() >= 10 * 1000){ + drop10kitems.complete(); + } + })); + + Events.on(CommandIssueEvent.class, e -> { + if(campaign() && e.command == UnitCommand.attack){ + issueAttackCommand.complete(); + } + }); + + Events.on(BlockBuildEndEvent.class, e -> { + if(campaign() && e.unit != null && e.unit.isLocal() && !e.breaking){ + SStat.blocksBuilt.add(); + + if(e.tile.block() == Blocks.router && e.tile.build.proximity().contains(t -> t.block == Blocks.router)){ + chainRouters.complete(); + } + + if(e.tile.block() == Blocks.groundFactory){ + buildGroundFactory.complete(); + } + + if(blocksBuilt.add(e.tile.block().name)){ + if(blocksBuilt.contains("meltdown") && blocksBuilt.contains("spectre") && blocksBuilt.contains("foreshadow")){ + buildMeltdownSpectre.complete(); + } + + save(); + } + + if(!circleConveyor.isAchieved() && e.tile.block() instanceof Conveyor){ + checked.clear(); + check: { + Tile current = e.tile; + for(int i = 0; i < 4; i++){ + checked.add(current.pos()); + if(current.build == null) break check; + Tile next = current.nearby(current.build.rotation); + if(next != null && next.block() instanceof Conveyor){ + current = next; + }else{ + break check; + } + } + + if(current == e.tile && checked.size == 4){ + circleConveyor.complete(); + } + } + } + } + }); + + Events.on(UnitCreateEvent.class, e -> { + if(campaign()){ + if(unitsBuilt.add(e.unit.type.name)){ + SStat.unitTypesBuilt.set(content.units().count(u -> unitsBuilt.contains(u.name) && !u.isHidden())); + } + + if(t5s.contains(e.unit.type)){ + buildT5.complete(); + } + } + }); + + Events.on(UnitControlEvent.class, e -> { + if(e.unit instanceof BlockUnitc && ((BlockUnitc)e.unit).tile().block == Blocks.router){ + becomeRouter.complete(); + } + }); + + Events.on(SchematicCreateEvent.class, e -> { + SStat.schematicsCreated.add(); + }); + + Events.on(BlockDestroyEvent.class, e -> { + if(campaign() && e.tile.team() != player.team()){ + SStat.blocksDestroyed.add(); + } + }); + + Events.on(MapMakeEvent.class, e -> SStat.mapsMade.add()); + + Events.on(MapPublishEvent.class, e -> SStat.mapsPublished.add()); + + Events.on(UnlockEvent.class, e -> { + if(e.content == Items.thorium) obtainThorium.complete(); + if(e.content == Items.titanium) obtainTitanium.complete(); + if(e.content instanceof SectorPreset && !content.sectors().contains(s -> s.locked())){ + unlockAllZones.complete(); + } + }); + + Events.run(Trigger.openWiki, openWiki::complete); + + Events.run(Trigger.exclusionDeath, dieExclusion::complete); + + Events.on(UnitDrownEvent.class, e -> { + if(campaign() && e.unit.isPlayer()){ + drown.complete(); + } + }); + + trigger(Trigger.acceleratorUse, useAccelerator); + + trigger(Trigger.impactPower, powerupImpactReactor); + + trigger(Trigger.flameAmmo, useFlameAmmo); + + trigger(Trigger.turretCool, coolTurret); + + trigger(Trigger.suicideBomb, suicideBomb); + + Events.run(Trigger.enablePixelation, enablePixelation::complete); + + Events.run(Trigger.thoriumReactorOverheat, () -> { + if(campaign()){ + SStat.reactorsOverheated.add(); + } + }); + + trigger(Trigger.shock, shockWetEnemy); + + trigger(Trigger.phaseDeflectHit, killEnemyPhaseWall); + + Events.on(LaunchItemEvent.class, e -> { + if(campaign()){ + launchItemPad.complete(); + } + }); + + Events.on(PickupEvent.class, e -> { + if(e.carrier.isPlayer() && campaign() && e.unit != null && t5s.contains(e.unit.type)){ + pickupT5.complete(); + } + }); + + Events.on(UnitCreateEvent.class, e -> { + if(campaign() && e.unit.team() == player.team()){ + SStat.unitsBuilt.add(); + } + }); + + Events.on(SectorLaunchEvent.class, e -> { + SStat.timesLaunched.add(); + }); + + Events.on(LaunchItemEvent.class, e -> { + SStat.itemsLaunched.add(e.stack.amount); + }); + + Events.on(WaveEvent.class, e -> { + if(campaign()){ + SStat.maxWavesSurvived.max(Vars.state.wave); + + if(state.stats.buildingsBuilt == 0 && state.wave >= 10){ + survive10WavesNoBlocks.complete(); + } + } + }); + + Events.on(PlayerJoin.class, e -> { + if(Vars.net.server()){ + SStat.maxPlayersServer.max(Groups.player.size()); + } + }); + + Runnable checkUnlocks = () -> { + if(Blocks.router.unlocked()) researchRouter.complete(); + + if(!TechTree.all.contains(t -> t.content.locked())){ + researchAll.complete(); + } + }; + + //check unlocked stuff on load as well + Events.on(ResearchEvent.class, e -> checkUnlocks.run()); + Events.on(UnlockEvent.class, e -> checkUnlocks.run()); + Events.on(ClientLoadEvent.class, e -> checkUnlocks.run()); + + Events.on(WinEvent.class, e -> { + if(state.rules.pvp){ + SStat.pvpsWon.add(); + } + }); + + Events.on(SectorCaptureEvent.class, e -> { + if(e.sector.isBeingPlayed() || net.client()){ + if(Vars.state.wave <= 5 && state.rules.attackMode){ + defeatAttack5Waves.complete(); + } + + if(state.stats.buildingsDestroyed == 0){ + captureNoBlocksBroken.complete(); + } + } + + if(Vars.state.rules.attackMode){ + SStat.attacksWon.add(); + } + + if(!e.sector.isBeingPlayed() && !net.client()){ + captureBackground.complete(); + } + + if(!e.sector.planet.sectors.contains(s -> !s.hasBase())){ + captureAllSectors.complete(); + } + + SStat.sectorsControlled.set(e.sector.planet.sectors.count(Sector::hasBase)); + }); + } + + private void checkUpdate(){ + if(campaign()){ + SStat.maxUnitActive.max(Groups.unit.count(t -> t.team == player.team())); + + if(Groups.unit.count(u -> u.type == UnitTypes.poly && u.team == player.team()) >= 10){ + active10Polys.complete(); + } + + for(Building entity : player.team().cores()){ + if(!content.items().contains(i -> entity.items.get(i) < entity.block.itemCapacity)){ + fillCoreAllCampaign.complete(); + break; + } + } + } + } + + private void save(){ + Core.settings.putJson("units-built" , String.class, unitsBuilt); + Core.settings.putJson("blocks-built" , String.class, blocksBuilt); + } + + private void trigger(Trigger trigger, Achievement ach){ + Events.run(trigger, () -> { + if(campaign()){ + ach.complete(); + } + }); + } + + private boolean campaign(){ + return Vars.state.isCampaign(); + } +} diff --git a/desktop/src/mindustry/desktop/steam/SStat.java b/core/src/mindustry/service/SStat.java similarity index 76% rename from desktop/src/mindustry/desktop/steam/SStat.java rename to core/src/mindustry/service/SStat.java index 0086f68785..d099b83bda 100644 --- a/desktop/src/mindustry/desktop/steam/SStat.java +++ b/core/src/mindustry/service/SStat.java @@ -1,4 +1,6 @@ -package mindustry.desktop.steam; +package mindustry.service; + +import static mindustry.Vars.*; public enum SStat{ unitsDestroyed, @@ -23,7 +25,7 @@ public enum SStat{ ; public int get(){ - return SVars.stats.stats.getStatI(name(), 0); + return service.getStat(name(), 0); } public void max(int amount){ @@ -33,10 +35,10 @@ public enum SStat{ } public void set(int amount){ - SVars.stats.stats.setStatI(name(), amount); - SVars.stats.onUpdate(); + service.setStat(name(), amount); + service.storeStats(); - for(SAchievement a : SAchievement.all){ + for(Achievement a : Achievement.all){ a.checkCompletion(); } } diff --git a/core/src/mindustry/type/Item.java b/core/src/mindustry/type/Item.java index 24dd790d37..b4fbc2cdd5 100644 --- a/core/src/mindustry/type/Item.java +++ b/core/src/mindustry/type/Item.java @@ -13,9 +13,9 @@ public class Item extends UnlockableContent{ /** how explosive this item is. */ public float explosiveness = 0f; - /** flammability above 0.3 makes this eleigible for item burners. */ + /** flammability above 0.3 makes this eligible for item burners. */ public float flammability = 0f; - /** how radioactive this item is. 0=none, 1=chernobyl ground zero */ + /** how radioactive this item is. */ public float radioactivity; /** how electrically potent this item is. */ public float charge = 0f; @@ -58,6 +58,6 @@ public class Item extends UnlockableContent{ /** Allocates a new array containing all items that generate ores. */ public static Seq getAllOres(){ - return content.blocks().select(b -> b instanceof OreBlock).map(b -> ((Floor)b).itemDrop); + return content.blocks().select(b -> b instanceof OreBlock).map(b -> b.itemDrop); } } diff --git a/core/src/mindustry/type/ItemStack.java b/core/src/mindustry/type/ItemStack.java index 26cffe97b4..ec15207760 100644 --- a/core/src/mindustry/type/ItemStack.java +++ b/core/src/mindustry/type/ItemStack.java @@ -37,7 +37,7 @@ public class ItemStack implements Comparable{ } public static ItemStack[] mult(ItemStack[] stacks, float amount){ - ItemStack[] copy = new ItemStack[stacks.length]; + var copy = new ItemStack[stacks.length]; for(int i = 0; i < copy.length; i++){ copy[i] = new ItemStack(stacks[i].item, Mathf.round(stacks[i].amount * amount)); } @@ -45,7 +45,7 @@ public class ItemStack implements Comparable{ } public static ItemStack[] with(Object... items){ - ItemStack[] stacks = new ItemStack[items.length / 2]; + var stacks = new ItemStack[items.length / 2]; for(int i = 0; i < items.length; i += 2){ stacks[i / 2] = new ItemStack((Item)items[i], ((Number)items[i + 1]).intValue()); } diff --git a/core/src/mindustry/type/Planet.java b/core/src/mindustry/type/Planet.java index 40d571eec7..60ef7f0d9b 100644 --- a/core/src/mindustry/type/Planet.java +++ b/core/src/mindustry/type/Planet.java @@ -18,7 +18,7 @@ import static mindustry.Vars.*; public class Planet extends UnlockableContent{ /** Default spacing between planet orbits in world units. */ - private static final float orbitSpacing = 9f; + private static final float orbitSpacing = 11f; /** intersect() temp var. */ private static final Vec3 intersectResult = new Vec3(); /** Mesh used for rendering. Created on load() - will be null on the server! */ @@ -152,7 +152,7 @@ public class Planet extends UnlockableContent{ public float getRotation(){ //tidally locked planets always face toward parents if(tidalLock){ - return getOrbitAngle(); + return -getOrbitAngle() + 90; } //random offset for more variability float offset = Mathf.randomSeed(id+1, 360); @@ -202,8 +202,18 @@ public class Planet extends UnlockableContent{ return mat.setToTranslation(position).rotate(Vec3.Y, getRotation()); } + /** Regenerates the planet mesh. For debugging only. */ + public void reloadMesh(){ + if(mesh != null){ + mesh.dispose(); + } + mesh = meshLoader.get(); + } + @Override public void load(){ + super.load(); + mesh = meshLoader.get(); } diff --git a/core/src/mindustry/type/Satellite.java b/core/src/mindustry/type/Satellite.java index a30a44c136..986d53a01a 100644 --- a/core/src/mindustry/type/Satellite.java +++ b/core/src/mindustry/type/Satellite.java @@ -1,8 +1,9 @@ package mindustry.type; +//TODO /** Any object that is orbiting a planet. */ public class Satellite{ - public Planet planet; + public transient Planet planet; public Satellite(Planet orbiting){ this.planet = orbiting; diff --git a/core/src/mindustry/type/Sector.java b/core/src/mindustry/type/Sector.java index dda4cc6a6c..7aea305426 100644 --- a/core/src/mindustry/type/Sector.java +++ b/core/src/mindustry/type/Sector.java @@ -123,7 +123,7 @@ public class Sector{ @Nullable public TextureRegion icon(){ - return info.icon == null ? null : Fonts.getLargeIcon(info.icon); + return info.contentIcon != null ? info.contentIcon.uiIcon : info.icon == null ? null : Fonts.getLargeIcon(info.icon); } public boolean isCaptured(){ @@ -148,7 +148,8 @@ public class Sector{ /** @return the sector size, in tiles */ public int getSize(){ - int res = (int)(rect.radius * 3200); + if(planet.generator == null) return 1; + int res = (int)(rect.radius * planet.generator.getSizeScl()); return res % 2 == 0 ? res : res + 1; } diff --git a/core/src/mindustry/type/SectorPreset.java b/core/src/mindustry/type/SectorPreset.java index 997e014c63..3aa2743dc2 100644 --- a/core/src/mindustry/type/SectorPreset.java +++ b/core/src/mindustry/type/SectorPreset.java @@ -1,12 +1,10 @@ package mindustry.type; import arc.func.*; -import arc.graphics.g2d.*; import mindustry.ctype.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.maps.generators.*; -import mindustry.ui.*; public class SectorPreset extends UnlockableContent{ public FileMapGenerator generator; @@ -18,6 +16,7 @@ public class SectorPreset extends UnlockableContent{ public boolean useAI = true; /** Difficulty, 0-10. */ public float difficulty; + public float startWaveTimeMultiplier = 2f; public boolean addStartingItems = false; public SectorPreset(String name, Planet planet, int sector){ @@ -32,8 +31,10 @@ public class SectorPreset extends UnlockableContent{ } @Override - public TextureRegion icon(Cicon c){ - return Icon.terrain.getRegion(); + public void loadIcon(){ + if(Icon.terrain != null){ + uiIcon = fullIcon = Icon.terrain.getRegion(); + } } @Override diff --git a/core/src/mindustry/type/StatusEffect.java b/core/src/mindustry/type/StatusEffect.java index c7d825c188..c173492ab6 100644 --- a/core/src/mindustry/type/StatusEffect.java +++ b/core/src/mindustry/type/StatusEffect.java @@ -10,7 +10,6 @@ import mindustry.ctype.*; import mindustry.entities.*; import mindustry.entities.units.*; import mindustry.gen.*; -import mindustry.ui.*; import mindustry.world.meta.*; public class StatusEffect extends UnlockableContent{ @@ -38,6 +37,8 @@ public class StatusEffect extends UnlockableContent{ public boolean permanent; /** If true, this effect will only react with other effects and cannot be applied. */ public boolean reactive; + /** Whether to show this effect in the database. */ + public boolean show = true; /** Tint color of effect. */ public Color color = Color.white.cpy(); /** Effect that happens randomly on top of the affected unit. */ @@ -47,8 +48,7 @@ public class StatusEffect extends UnlockableContent{ /** Called on init. */ protected Runnable initblock = () -> {}; - public ObjectSet affinities = new ObjectSet<>(); - public ObjectSet opposites = new ObjectSet<>(); + public ObjectSet affinities = new ObjectSet<>(), opposites = new ObjectSet<>(); public StatusEffect(String name){ super(name); @@ -65,7 +65,7 @@ public class StatusEffect extends UnlockableContent{ @Override public boolean isHidden(){ - return localizedName.equals(name); + return localizedName.equals(name) || !show; } @Override @@ -105,11 +105,6 @@ public class StatusEffect extends UnlockableContent{ } - @Override - public Cicon prefDatabaseIcon(){ - return Cicon.large; - } - @Override public boolean showUnlock(){ return false; @@ -131,7 +126,6 @@ public class StatusEffect extends UnlockableContent{ protected void trans(StatusEffect effect, TransitionHandler handler){ transitions.put(effect, handler); - effect.transitions.put(this, handler); } protected void affinity(StatusEffect effect, TransitionHandler handler){ @@ -141,20 +135,23 @@ public class StatusEffect extends UnlockableContent{ } protected void opposite(StatusEffect... effect){ - opposites.addAll(effect); - for(StatusEffect sup : effect){ - sup.opposites.add(this); - trans(sup, (unit, time, newTime, result) -> { - time -= newTime * 0.5f; - if(time > 0){ - result.set(this, time); - return; - } - result.set(sup, newTime); - }); + for(var other : effect){ + handleOpposite(other); + other.handleOpposite(this); } } + protected void handleOpposite(StatusEffect other){ + opposites.add(other); + trans(other, (unit, result, time) -> { + result.time -= time * 0.5f; + if(result.time <= 0f){ + result.time = time; + result.effect = other; + } + }); + } + public void draw(Unit unit){ } @@ -166,16 +163,16 @@ public class StatusEffect extends UnlockableContent{ /** * Called when transitioning between two status effects. * @param to The state to transition to - * @param time The current status effect time - * @param newTime The time that the new status effect will last + * @param time The applies status effect time + * @return whether a reaction occurred */ - public StatusEntry getTransition(Unit unit, StatusEffect to, float time, float newTime, StatusEntry result){ - if(transitions.containsKey(to)){ - transitions.get(to).handle(unit, time, newTime, result); - return result; + public boolean applyTransition(Unit unit, StatusEffect to, StatusEntry entry, float time){ + var trans = transitions.get(to); + if(trans != null){ + trans.handle(unit, entry, time); + return true; } - - return result.set(to, newTime); + return false; } @Override @@ -189,6 +186,6 @@ public class StatusEffect extends UnlockableContent{ } public interface TransitionHandler{ - void handle(Unit unit, float time, float newTime, StatusEntry result); + void handle(Unit unit, StatusEntry current, float time); } } diff --git a/core/src/mindustry/type/UnitType.java b/core/src/mindustry/type/UnitType.java index 713c9cdf52..14ced69a2d 100644 --- a/core/src/mindustry/type/UnitType.java +++ b/core/src/mindustry/type/UnitType.java @@ -5,13 +5,13 @@ import arc.audio.*; import arc.func.*; import arc.graphics.*; import arc.graphics.g2d.*; +import arc.graphics.g2d.TextureAtlas.*; import arc.math.*; import arc.math.geom.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; -import mindustry.*; import mindustry.ai.types.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; @@ -23,6 +23,7 @@ import mindustry.entities.units.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.graphics.MultiPacker.*; import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.environment.*; @@ -30,18 +31,26 @@ import mindustry.world.blocks.payloads.*; import mindustry.world.blocks.units.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; +//TODO document public class UnitType extends UnlockableContent{ - public static final float shadowTX = -12, shadowTY = -13, outlineSpace = 0.01f; + public static final float shadowTX = -12, shadowTY = -13; private static final Vec2 legOffset = new Vec2(); /** If true, the unit is always at elevation 1. */ public boolean flying; public Prov constructor; public Prov defaultController = () -> !flying ? new GroundAI() : new FlyingAI(); + + /** Environmental flags that are *all* required for this unit to function. 0 = any environment */ + public int envRequired = 0; + /** The environment flags that this unit can function in. If the env matches any of these, it will be enabled. */ + public int envEnabled = Env.terrestrial; + /** The environment flags that this unit *cannot* function in. If the env matches any of these, it will be *disabled*. */ + public int envDisabled = 0; + public float speed = 1.1f, boostMultiplier = 1f, rotateSpeed = 5f, baseRotateSpeed = 5f; public float drag = 0.3f, accel = 0.5f, landShake = 0f, rippleScale = 1f, riseSpeed = 0.08f, fallSpeed = 0.018f; public float health = 200f, range = -1, miningRange = 70f, armor = 0f, maxRange = -1f; @@ -65,6 +74,10 @@ public class UnitType extends UnlockableContent{ public Seq abilities = new Seq<>(); public BlockFlag targetFlag = BlockFlag.generator; + public Color outlineColor = Pal.darkerMetal; + public int outlineRadius = 3; + public boolean outlines = true; + public int legCount = 4, legGroupSize = 2; public float legLength = 10f, legSpeed = 0.1f, legTrns = 1f, legBaseOffset = 0f, legMoveSpace = 1f, legExtension = 0, legPairOffset = 0, legLengthScl = 1f, kinematicScl = 1f, maxStretch = 1.75f; public float legSplashDamage = 0f, legSplashRange = 5; @@ -95,13 +108,14 @@ public class UnitType extends UnlockableContent{ public float strafePenalty = 0.5f; public float hitSize = 6f; public float itemOffsetY = 3f; - public float lightRadius = 60f, lightOpacity = 0.6f; + public float lightRadius = -1f, lightOpacity = 0.6f; public Color lightColor = Pal.powerLight; public boolean drawCell = true, drawItems = true, drawShields = true; public int trailLength = 3; public float trailX = 4f, trailY = -3f, trailScl = 1f; /** Whether the unit can heal blocks. Initialized in init() */ public boolean canHeal = false; + /** If true, all weapons will attack the same target. */ public boolean singleTarget = false; public ObjectSet immunities = new ObjectSet<>(); @@ -117,7 +131,7 @@ public class UnitType extends UnlockableContent{ public UnitType(String name){ super(name); - constructor = EntityMapping.map(name); + constructor = EntityMapping.map(this.name); } public UnitController createController(){ @@ -166,7 +180,7 @@ public class UnitType extends UnlockableContent{ public void display(Unit unit, Table table){ table.table(t -> { t.left(); - t.add(new Image(icon(Cicon.medium))).size(8 * 4).scaling(Scaling.fit); + t.add(new Image(uiIcon)).size(iconMed).scaling(Scaling.fit); t.labelWrap(localizedName).left().width(190f).padLeft(5); }).growX().left(); table.row(); @@ -210,6 +224,11 @@ public class UnitType extends UnlockableContent{ table.row(); } + /** @return whether this block supports a specific environment. */ + public boolean supportsEnv(int env){ + return (envEnabled & env) != 0 && (envDisabled & env) == 0 && (envRequired == 0 || (envRequired & env) == envRequired); + } + @Override public void getDependencies(Cons cons){ //units require reconstructors being researched @@ -236,6 +255,7 @@ public class UnitType extends UnlockableContent{ stats.add(Stat.health, health); stats.add(Stat.armor, armor); stats.add(Stat.speed, speed); + stats.add(Stat.size, hitSize / tilesize, StatUnit.blocksSquared); stats.add(Stat.itemCapacity, itemCapacity); stats.add(Stat.range, (int)(maxRange / tilesize), StatUnit.blocks); stats.add(Stat.commandLimit, commandLimit); @@ -258,7 +278,7 @@ public class UnitType extends UnlockableContent{ if(mineTier >= 1){ stats.addPercent(Stat.mineSpeed, mineSpeed); - stats.add(Stat.mineTier, new BlockFilterValue(b -> b instanceof Floor f && f.itemDrop != null && f.itemDrop.hardness <= mineTier && !f.playerUnmineable)); + stats.add(Stat.mineTier, StatValues.blocks(b -> b instanceof Floor f && f.itemDrop != null && f.itemDrop.hardness <= mineTier && !f.playerUnmineable)); } if(buildSpeed > 0){ stats.addPercent(Stat.buildSpeed, buildSpeed); @@ -268,7 +288,7 @@ public class UnitType extends UnlockableContent{ } if(weapons.any()){ - stats.add(Stat.weapons, new WeaponListValue(this, weapons)); + stats.add(Stat.weapons, StatValues.weapons(this, weapons)); } } @@ -286,27 +306,34 @@ public class UnitType extends UnlockableContent{ immunities.add(StatusEffects.wet); } + if(lightRadius == -1){ + lightRadius = Math.max(60f, hitSize * 2.3f); + } + + clipSize = Math.max(clipSize, lightRadius * 1.1f); singleTarget = weapons.size <= 1; if(itemCapacity < 0){ itemCapacity = Math.max(Mathf.round((int)(hitSize * 4.3), 10), 10); } + //assume slight range margin + float margin = 4f; + //set up default range if(range < 0){ range = Float.MAX_VALUE; for(Weapon weapon : weapons){ - range = Math.min(range, weapon.bullet.range() + hitSize / 2f); - maxRange = Math.max(maxRange, weapon.bullet.range() + hitSize / 2f); + range = Math.min(range, weapon.bullet.range() - margin); + maxRange = Math.max(maxRange, weapon.bullet.range() - margin); } } if(maxRange < 0){ - maxRange = 0f; - maxRange = Math.max(maxRange, range); + maxRange = Math.max(0f, range); for(Weapon weapon : weapons){ - maxRange = Math.max(maxRange, weapon.bullet.range() + hitSize / 2f); + maxRange = Math.max(maxRange, weapon.bullet.range() - margin); } } @@ -363,7 +390,7 @@ public class UnitType extends UnlockableContent{ //calculate estimated DPS for one target based on weapons if(dpsEstimate < 0){ - dpsEstimate = weapons.sumf(w -> (w.bullet.estimateDPS() / w.reload) * w.shots * 60f); + dpsEstimate = weapons.sumf(Weapon::dps); //suicide enemy if(weapons.contains(w -> w.bullet.killShooter)){ @@ -376,6 +403,8 @@ public class UnitType extends UnlockableContent{ @CallSuper @Override public void load(){ + super.load(); + weapons.each(Weapon::load); region = Core.atlas.find(name); legRegion = Core.atlas.find(name + "-leg"); @@ -387,12 +416,43 @@ public class UnitType extends UnlockableContent{ cellRegion = Core.atlas.find(name + "-cell", Core.atlas.find("power-cell")); softShadowRegion = Core.atlas.find("circle-shadow"); outlineRegion = Core.atlas.find(name + "-outline"); - shadowRegion = icon(Cicon.full); + shadowRegion = fullIcon; wreckRegions = new TextureRegion[3]; for(int i = 0; i < wreckRegions.length; i++){ wreckRegions[i] = Core.atlas.find(name + "-wreck" + i); } + + clipSize = Math.max(region.width * 2f, clipSize); + } + + private void makeOutline(MultiPacker packer, TextureRegion region){ + if(region instanceof AtlasRegion at && region.found()){ + String name = at.name; + if(!packer.has(name + "-outline")){ + PixmapRegion base = Core.atlas.getPixmap(region); + var result = Pixmaps.outline(base, outlineColor, outlineRadius); + if(Core.settings.getBool("linear")){ + Pixmaps.bleed(result); + } + packer.add(PageType.main, name + "-outline", result); + } + } + } + + @Override + public void createIcons(MultiPacker packer){ + super.createIcons(packer); + + //currently does not create outlines for legs or base regions due to older mods having them outlined by default + if(outlines){ + makeOutline(packer, region); + for(Weapon weapon : weapons){ + if(!weapon.name.isEmpty()){ + makeOutline(packer, weapon.region); + } + } + } } @Override @@ -475,11 +535,10 @@ public class UnitType extends UnlockableContent{ drawSoftShadow(unit); - Draw.z(z - outlineSpace); + Draw.z(z); drawOutline(unit); - - Draw.z(z); + drawWeaponOutlines(unit); if(engineSize > 0) drawEngine(unit); drawBody(unit); if(drawCell) drawCell(unit); @@ -516,7 +575,10 @@ public class UnitType extends UnlockableContent{ public void drawShield(Unit unit){ float alpha = unit.shieldAlpha(); float radius = unit.hitSize() * 1.3f; - Fill.light(unit.x, unit.y, Lines.circleVertices(radius), radius, Tmp.c1.set(Pal.shieldIn), Tmp.c2.set(Pal.shield).lerp(Color.white, Mathf.clamp(unit.hitTime() / 2f)).a(Pal.shield.a * alpha)); + Fill.light(unit.x, unit.y, Lines.circleVertices(radius), radius, + Color.clear, + Tmp.c2.set(unit.team.color).lerp(Color.white, Mathf.clamp(unit.hitTime() / 2f)).a(0.7f * alpha) + ); } public void drawControl(Unit unit){ @@ -539,7 +601,7 @@ public class UnitType extends UnlockableContent{ Draw.color(0, 0, 0, 0.4f); float rad = 1.6f; float size = Math.max(region.width, region.height) * Draw.scl; - Draw.rect(softShadowRegion, unit, size * rad, size * rad); + Draw.rect(softShadowRegion, unit, size * rad * Draw.xscl, size * rad * Draw.yscl); Draw.color(); } @@ -551,7 +613,7 @@ public class UnitType extends UnlockableContent{ float size = (itemSize + Mathf.absin(Time.time, 5f, 1f)) * unit.itemTime; Draw.mixcol(Pal.accent, Mathf.absin(Time.time, 5f, 0.5f)); - Draw.rect(unit.item().icon(Cicon.medium), + Draw.rect(unit.item().fullIcon, unit.x + Angles.trnsx(unit.rotation + 180f, itemOffsetY), unit.y + Angles.trnsy(unit.rotation + 180f, itemOffsetY), size, size, unit.rotation); @@ -606,58 +668,38 @@ public class UnitType extends UnlockableContent{ applyColor(unit); for(WeaponMount mount : unit.mounts){ - Weapon weapon = mount.weapon; + mount.weapon.draw(unit, mount); + } - float rotation = unit.rotation - 90; - float weaponRotation = rotation + (weapon.rotate ? mount.rotation : 0); - float recoil = -((mount.reload) / weapon.reload * weapon.recoil); - float wx = unit.x + Angles.trnsx(rotation, weapon.x, weapon.y) + Angles.trnsx(weaponRotation, 0, recoil), - wy = unit.y + Angles.trnsy(rotation, weapon.x, weapon.y) + Angles.trnsy(weaponRotation, 0, recoil); + Draw.reset(); + } - if(weapon.shadow > 0){ - Drawf.shadow(wx, wy, weapon.shadow); - } + public void drawWeaponOutlines(Unit unit){ + applyColor(unit); + applyOutlineColor(unit); - if(weapon.outlineRegion.found()){ - float z = Draw.z(); - if(!weapon.top) Draw.z(z - outlineSpace); - - Draw.rect(weapon.outlineRegion, - wx, wy, - weapon.outlineRegion.width * Draw.scl * -Mathf.sign(weapon.flipSprite), - weapon.region.height * Draw.scl, - weaponRotation); - - Draw.z(z); - } - - Draw.rect(weapon.region, - wx, wy, - weapon.region.width * Draw.scl * -Mathf.sign(weapon.flipSprite), - weapon.region.height * Draw.scl, - weaponRotation); - - if(weapon.heatRegion.found() && mount.heat > 0){ - Draw.color(weapon.heatColor, mount.heat); - Draw.blend(Blending.additive); - Draw.rect(weapon.heatRegion, - wx, wy, - weapon.heatRegion.width * Draw.scl * -Mathf.sign(weapon.flipSprite), - weapon.heatRegion.height * Draw.scl, - weaponRotation); - Draw.blend(); - Draw.color(); + for(WeaponMount mount : unit.mounts){ + if(!mount.weapon.top){ + mount.weapon.drawOutline(unit, mount); } } Draw.reset(); } + public void applyOutlineColor(Unit unit){ + if(unit.isBoss()){ + Draw.mixcol(unit.team.color, Mathf.absin(7f, 1f)); + } + } + public void drawOutline(Unit unit){ Draw.reset(); if(Core.atlas.isFound(outlineRegion)){ + applyOutlineColor(unit); Draw.rect(outlineRegion, unit.x, unit.y, unit.rotation - 90); + Draw.reset(); } } @@ -678,7 +720,8 @@ public class UnitType extends UnlockableContent{ } public Color cellColor(Unit unit){ - return Tmp.c1.set(Color.black).lerp(unit.team.color, unit.healthf() + Mathf.absin(Time.time, Math.max(unit.healthf() * 5f, 1f), 1f - unit.healthf())); + float f = Mathf.clamp(unit.healthf()); + return Tmp.c1.set(Color.black).lerp(unit.team.color, f + Mathf.absin(Time.time, Math.max(f * 5f, 1f), 1f - f)); } public void drawLight(Unit unit){ @@ -786,7 +829,9 @@ public class UnitType extends UnlockableContent{ public void applyColor(Unit unit){ Draw.color(); - Draw.mixcol(Color.white, unit.hitTime); + Tmp.c1.set(Color.white).lerp(Pal.heal, Mathf.clamp(unit.healTime - unit.hitTime)); + Draw.mixcol(Tmp.c1, Math.max(unit.hitTime, Mathf.clamp(unit.healTime))); + if(unit.drownTime > 0 && unit.floorOn().isDeep()){ Draw.mixcol(unit.floorOn().mapColor, unit.drownTime * 0.8f); } diff --git a/core/src/mindustry/type/Weapon.java b/core/src/mindustry/type/Weapon.java index af5f63050b..1389bd238a 100644 --- a/core/src/mindustry/type/Weapon.java +++ b/core/src/mindustry/type/Weapon.java @@ -2,21 +2,34 @@ package mindustry.type; import arc.*; import arc.audio.*; +import arc.func.*; import arc.graphics.*; import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.util.*; +import mindustry.audio.*; import mindustry.content.*; import mindustry.entities.*; import mindustry.entities.bullet.*; +import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.graphics.*; +import static mindustry.Vars.*; + public class Weapon implements Cloneable{ + /** temporary weapon sequence number */ + static int sequenceNum = 0; + /** displayed weapon region */ public String name = ""; /** bullet shot */ - public BulletType bullet; + public BulletType bullet = Bullets.standardCopper; /** shell ejection effect */ public Effect ejectEffect = Fx.none; + /** whether to consume ammo when ammo is enabled in rules */ + public boolean useAmmo = true; /** whether to create a flipped copy of this weapon upon initialization. default: true */ public boolean mirror = true; /** whether to flip the weapon's sprite when rendering */ @@ -29,6 +42,14 @@ public class Weapon implements Cloneable{ public boolean top = true; /** whether to hold the bullet in place while firing */ public boolean continuous; + /** whether this weapon can be aimed manually by players */ + public boolean controllable = true; + /** whether to automatically target relevant units in update(); only works when controllable = false. */ + public boolean autoTarget = false; + /** whether to perform target trajectory prediction */ + public boolean predictTarget = true; + /** ticks to wait in-between targets */ + public float targetInterval = 40f, targetSwitchInterval = 70f; /** rotation speed of weapon when rotation is enabled, in degrees/t*/ public float rotateSpeed = 20f; /** weapon reload in frames */ @@ -85,6 +106,8 @@ public class Weapon implements Cloneable{ public Color heatColor = Pal.turretHeat; /** status effect applied when shooting */ public StatusEffect shootStatus = StatusEffects.none; + /** type of weapon mount to be used */ + public Func mountType = WeaponMount::new; /** status effect duration when shot */ public float shootStatusDuration = 60f * 5f; @@ -96,6 +119,238 @@ public class Weapon implements Cloneable{ this(""); } + public float dps(){ + return (bullet.estimateDPS() / reload) * shots * 60f; + } + + //TODO copy-pasted code + public void drawOutline(Unit unit, WeaponMount mount){ + float + rotation = unit.rotation - 90, + weaponRotation = rotation + (rotate ? mount.rotation : 0), + recoil = -((mount.reload) / reload * this.recoil), + wx = unit.x + Angles.trnsx(rotation, x, y) + Angles.trnsx(weaponRotation, 0, recoil), + wy = unit.y + Angles.trnsy(rotation, x, y) + Angles.trnsy(weaponRotation, 0, recoil); + + if(outlineRegion.found()){ + Draw.rect(outlineRegion, + wx, wy, + outlineRegion.width * Draw.scl * -Mathf.sign(flipSprite), + region.height * Draw.scl, + weaponRotation); + } + } + + public void draw(Unit unit, WeaponMount mount){ + float + rotation = unit.rotation - 90, + weaponRotation = rotation + (rotate ? mount.rotation : 0), + recoil = -((mount.reload) / reload * this.recoil), + wx = unit.x + Angles.trnsx(rotation, x, y) + Angles.trnsx(weaponRotation, 0, recoil), + wy = unit.y + Angles.trnsy(rotation, x, y) + Angles.trnsy(weaponRotation, 0, recoil); + + if(shadow > 0){ + Drawf.shadow(wx, wy, shadow); + } + + if(outlineRegion.found() && top){ + Draw.rect(outlineRegion, + wx, wy, + outlineRegion.width * Draw.scl * -Mathf.sign(flipSprite), + region.height * Draw.scl, + weaponRotation); + } + + Draw.rect(region, + wx, wy, + region.width * Draw.scl * -Mathf.sign(flipSprite), + region.height * Draw.scl, + weaponRotation); + + if(heatRegion.found() && mount.heat > 0){ + Draw.color(heatColor, mount.heat); + Draw.blend(Blending.additive); + Draw.rect(heatRegion, + wx, wy, + heatRegion.width * Draw.scl * -Mathf.sign(flipSprite), + heatRegion.height * Draw.scl, + weaponRotation); + Draw.blend(); + Draw.color(); + } + } + + public void update(Unit unit, WeaponMount mount){ + boolean can = unit.canShoot(); + mount.reload = Math.max(mount.reload - Time.delta * unit.reloadMultiplier, 0); + + float + weaponRotation = unit.rotation - 90 + (rotate ? mount.rotation : 0), + mountX = unit.x + Angles.trnsx(unit.rotation - 90, x, y), + mountY = unit.y + Angles.trnsy(unit.rotation - 90, x, y), + bulletX = mountX + Angles.trnsx(weaponRotation, this.shootX, this.shootY), + bulletY = mountY + Angles.trnsy(weaponRotation, this.shootX, this.shootY), + shootAngle = rotate ? weaponRotation + 90 : Angles.angle(bulletX, bulletY, mount.aimX, mount.aimY) + (unit.rotation - unit.angleTo(mount.aimX, mount.aimY)); + + //find a new target + if(!controllable && autoTarget){ + if((mount.retarget -= Time.delta) <= 0f){ + mount.target = findTarget(unit, mountX, mountY, bullet.range(), bullet.collidesAir, bullet.collidesGround); + mount.retarget = mount.target == null ? targetInterval : targetSwitchInterval; + } + + if(mount.target != null && checkTarget(unit, mount.target, mountX, mountY, bullet.range())){ + mount.target = null; + } + + boolean shoot = false; + + if(mount.target != null){ + shoot = mount.target.within(mountX, mountY, bullet.range() + Math.abs(shootY) + (mount.target instanceof Sized s ? s.hitSize()/2f : 0f)) && can; + + if(predictTarget){ + Vec2 to = Predict.intercept(unit, mount.target, bullet.speed); + mount.aimX = to.x; + mount.aimY = to.y; + }else{ + mount.aimX = mount.target.x(); + mount.aimY = mount.target.y(); + } + } + + mount.shoot = mount.rotate = shoot; + + //note that shooting state is not affected, as these cannot be controlled + //logic will return shooting as false even if these return true, which is fine + } + + //update continuous state + if(continuous && mount.bullet != null){ + if(!mount.bullet.isAdded() || mount.bullet.time >= mount.bullet.lifetime || mount.bullet.type != bullet){ + mount.bullet = null; + }else{ + mount.bullet.rotation(weaponRotation + 90); + mount.bullet.set(bulletX, bulletY); + mount.reload = reload; + unit.vel.add(Tmp.v1.trns(unit.rotation + 180f, mount.bullet.type.recoil)); + if(shootSound != Sounds.none && !headless){ + if(mount.sound == null) mount.sound = new SoundLoop(shootSound, 1f); + mount.sound.update(x, y, true); + } + } + }else{ + //heat decreases when not firing + mount.heat = Math.max(mount.heat - Time.delta * unit.reloadMultiplier / mount.weapon.cooldownTime, 0); + + if(mount.sound != null){ + mount.sound.update(bulletX, bulletY, false); + } + } + + //flip weapon shoot side for alternating weapons at half reload + if(otherSide != -1 && alternate && mount.side == flipSprite && + mount.reload + Time.delta * unit.reloadMultiplier > reload/2f && mount.reload <= reload/2f){ + unit.mounts[otherSide].side = !unit.mounts[otherSide].side; + mount.side = !mount.side; + } + + //rotate if applicable + if(rotate && (mount.rotate || mount.shoot) && can){ + float axisX = unit.x + Angles.trnsx(unit.rotation - 90, x, y), + axisY = unit.y + Angles.trnsy(unit.rotation - 90, x, y); + + mount.targetRotation = Angles.angle(axisX, axisY, mount.aimX, mount.aimY) - unit.rotation; + mount.rotation = Angles.moveToward(mount.rotation, mount.targetRotation, rotateSpeed * Time.delta); + }else if(!rotate){ + mount.rotation = 0; + mount.targetRotation = unit.angleTo(mount.aimX, mount.aimY); + } + + //shoot if applicable + if(mount.shoot && //must be shooting + can && //must be able to shoot + (!useAmmo || unit.ammo > 0 || !state.rules.unitAmmo || unit.team.rules().infiniteAmmo) && //check ammo + (!alternate || mount.side == flipSprite) && + //TODO checking for velocity this way isn't entirely correct + (unit.vel.len() >= mount.weapon.minShootVelocity || (net.active() && !unit.isLocal())) && //check velocity requirements + mount.reload <= 0.0001f && //reload has to be 0 + Angles.within(rotate ? mount.rotation : unit.rotation, mount.targetRotation, mount.weapon.shootCone) //has to be within the cone + ){ + shoot(unit, mount, bulletX, bulletY, mount.aimX, mount.aimY, mountX, mountY, shootAngle, Mathf.sign(x)); + + mount.reload = reload; + + if(useAmmo){ + unit.ammo--; + if(unit.ammo < 0) unit.ammo = 0; + } + } + } + + protected Teamc findTarget(Unit unit, float x, float y, float range, boolean air, boolean ground){ + return Units.closestTarget(unit.team, x, y, range + Math.abs(shootY), u -> u.checkTarget(air, ground), t -> ground); + } + + protected boolean checkTarget(Unit unit, Teamc target, float x, float y, float range){ + return Units.invalidateTarget(target, unit.team, x, y, range + Math.abs(shootY)); + } + + protected void shoot(Unit unit, WeaponMount mount, float shootX, float shootY, float aimX, float aimY, float mountX, float mountY, float rotation, int side){ + float baseX = unit.x, baseY = unit.y; + boolean delay = firstShotDelay + shotDelay > 0f; + + (delay ? chargeSound : continuous ? Sounds.none : shootSound).at(shootX, shootY, Mathf.random(soundPitchMin, soundPitchMax)); + + BulletType ammo = bullet; + float lifeScl = ammo.scaleVelocity ? Mathf.clamp(Mathf.dst(shootX, shootY, aimX, aimY) / ammo.range()) : 1f; + + sequenceNum = 0; + if(delay){ + Angles.shotgun(shots, spacing, rotation, f -> { + Time.run(sequenceNum * shotDelay + firstShotDelay, () -> { + if(!unit.isAdded()) return; + mount.bullet = bullet(unit, shootX + unit.x - baseX, shootY + unit.y - baseY, f + Mathf.range(inaccuracy), lifeScl); + }); + sequenceNum++; + }); + }else{ + Angles.shotgun(shots, spacing, rotation, f -> mount.bullet = bullet(unit, shootX, shootY, f + Mathf.range(inaccuracy), lifeScl)); + } + + boolean parentize = ammo.keepVelocity; + + if(delay){ + Time.run(firstShotDelay, () -> { + if(!unit.isAdded()) return; + + unit.vel.add(Tmp.v1.trns(rotation + 180f, ammo.recoil)); + Effect.shake(shake, shake, shootX, shootY); + mount.heat = 1f; + if(!continuous){ + shootSound.at(shootX, shootY, Mathf.random(soundPitchMin, soundPitchMax)); + } + }); + }else{ + unit.vel.add(Tmp.v1.trns(rotation + 180f, ammo.recoil)); + Effect.shake(shake, shake, shootX, shootY); + mount.heat = 1f; + } + + ejectEffect.at(mountX, mountY, rotation * side); + ammo.shootEffect.at(shootX, shootY, rotation, parentize ? unit : null); + ammo.smokeEffect.at(shootX, shootY, rotation, parentize ? unit : null); + unit.apply(shootStatus, shootStatusDuration); + } + + protected Bullet bullet(Unit unit, float shootX, float shootY, float angle, float lifescl){ + float xr = Mathf.range(xRand); + + return bullet.create(unit, unit.team, + shootX + Angles.trnsx(angle, 0, xr), + shootY + Angles.trnsy(angle, 0, xr), + angle, (1f - velocityRnd) + Mathf.random(velocityRnd), lifescl); + } + public Weapon copy(){ try{ return (Weapon)clone(); diff --git a/core/src/mindustry/type/Weather.java b/core/src/mindustry/type/Weather.java index e30b6ab0e8..fe4c0d3aa6 100644 --- a/core/src/mindustry/type/Weather.java +++ b/core/src/mindustry/type/Weather.java @@ -20,6 +20,9 @@ import mindustry.world.blocks.*; import static mindustry.Vars.*; public class Weather extends UnlockableContent{ + /** Global random variable used for rendering. */ + public static final Rand rand = new Rand(); + /** Default duration of this weather event in ticks. */ public float duration = 10f * Time.toMinutes; public float opacityMultiplier = 1f; @@ -29,7 +32,6 @@ public class Weather extends UnlockableContent{ public float soundVolOscMag = 0f, soundVolOscScl = 20f; //internals - public Rand rand = new Rand(); public Prov type = WeatherState::create; public StatusEffect status = StatusEffects.none; public float statusDuration = 60f * 2; @@ -108,7 +110,7 @@ public class Weather extends UnlockableContent{ } - public void drawParticles(TextureRegion region, Color color, + public static void drawParticles(TextureRegion region, Color color, float sizeMin, float sizeMax, float density, float intensity, float opacity, float windx, float windy, @@ -147,7 +149,8 @@ public class Weather extends UnlockableContent{ } } - public void drawRain(float sizeMin, float sizeMax, float xspeed, float yspeed, float density, float intensity, float stroke, Color color){ + public static void drawRain(float sizeMin, float sizeMax, float xspeed, float yspeed, float density, float intensity, float stroke, Color color){ + rand.setSeed(0); float padding = sizeMax*0.9f; Tmp.r1.setCentered(Core.camera.position.x, Core.camera.position.y, Core.graphics.getWidth() / renderer.minScale(), Core.graphics.getHeight() / renderer.minScale()); @@ -180,12 +183,13 @@ public class Weather extends UnlockableContent{ } } - public void drawSplashes(TextureRegion[] splashes, float padding, float density, float intensity, float opacity, float timeScale, float stroke, Color color, Liquid splasher){ + public static void drawSplashes(TextureRegion[] splashes, float padding, float density, float intensity, float opacity, float timeScale, float stroke, Color color, Liquid splasher){ Tmp.r1.setCentered(Core.camera.position.x, Core.camera.position.y, Core.graphics.getWidth() / renderer.minScale(), Core.graphics.getHeight() / renderer.minScale()); Tmp.r1.grow(padding); Core.camera.bounds(Tmp.r2); int total = (int)(Tmp.r1.area() / density * intensity) / 2; Lines.stroke(stroke); + rand.setSeed(0); float t = Time.time / timeScale; @@ -226,7 +230,21 @@ public class Weather extends UnlockableContent{ } } - public void drawNoise(Texture noise, Color color, float noisescl, float opacity, float baseSpeed, float intensity, float vwindx, float vwindy, float offset){ + public static void drawNoiseLayers(Texture noise, Color color, float noisescl, float opacity, float baseSpeed, float intensity, float vwindx, float vwindy, + int layers, float layerSpeedM , float layerAlphaM, float layerSclM, float layerColorM){ + float sspeed = 1f, sscl = 1f, salpha = 1f, offset = 0f; + Color col = Tmp.c1.set(color); + for(int i = 0; i < layers; i++){ + drawNoise(noise, col, noisescl * sscl, salpha * opacity, sspeed * baseSpeed, intensity, vwindx, vwindy, offset); + sspeed *= layerSpeedM; + salpha *= layerAlphaM; + sscl *= layerSclM; + offset += 0.29f; + col.mul(layerColorM); + } + } + + public static void drawNoise(Texture noise, Color color, float noisescl, float opacity, float baseSpeed, float intensity, float vwindx, float vwindy, float offset){ Draw.alpha(opacity); Draw.tint(color); @@ -325,14 +343,12 @@ public class Weather extends UnlockableContent{ public void draw(){ if(renderer.weatherAlpha() > 0.0001f && renderer.drawWeather && Core.settings.getBool("showweather")){ Draw.draw(Layer.weather, () -> { - weather.rand.setSeed(0); Draw.alpha(renderer.weatherAlpha() * opacity * weather.opacityMultiplier); weather.drawOver(self()); Draw.reset(); }); Draw.draw(Layer.debris, () -> { - weather.rand.setSeed(0); Draw.alpha(renderer.weatherAlpha() * opacity * weather.opacityMultiplier); weather.drawUnder(self()); Draw.reset(); diff --git a/core/src/mindustry/type/weapons/PointDefenseWeapon.java b/core/src/mindustry/type/weapons/PointDefenseWeapon.java new file mode 100644 index 0000000000..7df83e1148 --- /dev/null +++ b/core/src/mindustry/type/weapons/PointDefenseWeapon.java @@ -0,0 +1,62 @@ +package mindustry.type.weapons; + +import arc.graphics.*; +import arc.math.*; +import arc.math.geom.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.type.*; + +/** + * Note that this requires several things: + * - A bullet with positive maxRange + * - A bullet with positive damage + * - Rotation + * */ +public class PointDefenseWeapon extends Weapon{ + public Color color = Color.white; + public Effect beamEffect = Fx.pointBeam; + + public PointDefenseWeapon(String name){ + super(name); + } + + public PointDefenseWeapon(){ + } + + { + predictTarget = false; + autoTarget = true; + controllable = false; + rotate = true; + useAmmo = false; + } + + @Override + protected Teamc findTarget(Unit unit, float x, float y, float range, boolean air, boolean ground){ + return Groups.bullet.intersect(x - range, y - range, range*2, range*2).min(b -> b.team != unit.team && b.type().hittable, b -> b.dst2(x, y)); + } + + @Override + protected boolean checkTarget(Unit unit, Teamc target, float x, float y, float range){ + return !(target.within(unit, range) && target.team() != unit.team && target instanceof Bullet bullet && bullet.type != null && bullet.type.hittable); + } + + @Override + protected void shoot(Unit unit, WeaponMount mount, float shootX, float shootY, float aimX, float aimY, float mountX, float mountY, float rotation, int side){ + if(!(mount.target instanceof Bullet target)) return; + + if(target.damage() > bullet.damage){ + target.damage(target.damage() - bullet.damage); + }else{ + target.remove(); + } + + beamEffect.at(shootX, shootY, rotation, color, new Vec2().set(target)); + bullet.shootEffect.at(shootX, shootY, rotation, color); + bullet.hitEffect.at(target.x, target.y, color); + shootSound.at(shootX, shootY, Mathf.random(0.9f, 1.1f)); + } +} diff --git a/core/src/mindustry/type/weapons/RepairBeamWeapon.java b/core/src/mindustry/type/weapons/RepairBeamWeapon.java new file mode 100644 index 0000000000..c2938207f0 --- /dev/null +++ b/core/src/mindustry/type/weapons/RepairBeamWeapon.java @@ -0,0 +1,121 @@ +package mindustry.type.weapons; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.util.*; +import mindustry.entities.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.world.blocks.units.*; + +/** + * Note that this weapon requires a bullet with a positive maxRange. + * Rotation must be set to true. Fixed repair points are not supported. + * */ +public class RepairBeamWeapon extends Weapon{ + public boolean targetBuildings = false; + + public float repairSpeed = 0.3f; + public float beamWidth = 1f; + public float pulseRadius = 6f; + public float pulseStroke = 2f; + + public TextureRegion laser, laserEnd, laserTop, laserTopEnd; + + public Color laserColor = Color.valueOf("98ffa9"), laserTopColor = Color.white.cpy(); + + public RepairBeamWeapon(String name){ + super(name); + } + + public RepairBeamWeapon(){ + } + + { + //must be >0 to prevent various bugs + reload = 1f; + predictTarget = false; + autoTarget = true; + controllable = false; + rotate = true; + useAmmo = false; + mountType = HealBeamMount::new; + recoil = 0f; + } + + @Override + public float dps(){ + return 0f; + } + + @Override + public void load(){ + super.load(); + + laser = Core.atlas.find("laser-white"); + laserEnd = Core.atlas.find("laser-white-end"); + laserTop = Core.atlas.find("laser-top"); + laserTopEnd = Core.atlas.find("laser-top-end"); + } + + @Override + protected Teamc findTarget(Unit unit, float x, float y, float range, boolean air, boolean ground){ + var out = Units.closest(unit.team, x, y, range, u -> u != unit && u.damaged()); + if(out != null || !targetBuildings) return out; + //TODO maybe buildings shouldn't be allowed at all + return Units.findAllyTile(unit.team, x, y, range, Building::damaged); + } + + @Override + protected boolean checkTarget(Unit unit, Teamc target, float x, float y, float range){ + return !(target.within(unit, range + unit.hitSize/2f) && target.team() == unit.team && target instanceof Healthc u && u.damaged() && u.isValid()); + } + + @Override + protected void shoot(Unit unit, WeaponMount mount, float shootX, float shootY, float aimX, float aimY, float mountX, float mountY, float rotation, int side){ + //does nothing, shooting is handled in update() + } + + @Override + public void update(Unit unit, WeaponMount mount){ + super.update(unit, mount); + + HealBeamMount heal = (HealBeamMount)mount; + heal.strength = Mathf.lerpDelta(heal.strength, Mathf.num(mount.target != null), 0.2f); + + if(mount.target instanceof Healthc u){ + u.heal(repairSpeed * heal.strength * Time.delta); + } + } + + @Override + public void draw(Unit unit, WeaponMount mount){ + super.draw(unit, mount); + + HealBeamMount heal = (HealBeamMount)mount; + + float + weaponRotation = unit.rotation - 90, + wx = unit.x + Angles.trnsx(weaponRotation, x, y), + wy = unit.y + Angles.trnsy(weaponRotation, x, y); + + float z = Draw.z(); + RepairPoint.drawBeam(wx, wy, unit.rotation + mount.rotation, shootY, unit.id, mount.target == null ? null : (Sized)mount.target, unit.team, heal.strength, + pulseStroke, pulseRadius, beamWidth, heal.lastEnd, heal.offset, laserColor, laserTopColor, + laser, laserEnd, laserTop, laserTopEnd); + Draw.z(z); + } + + public static class HealBeamMount extends WeaponMount{ + public Vec2 offset = new Vec2(), lastEnd = new Vec2(); + public float strength; + + public HealBeamMount(Weapon weapon){ + super(weapon); + } + } +} diff --git a/core/src/mindustry/type/weather/MagneticStorm.java b/core/src/mindustry/type/weather/MagneticStorm.java new file mode 100644 index 0000000000..c1d45b2529 --- /dev/null +++ b/core/src/mindustry/type/weather/MagneticStorm.java @@ -0,0 +1,11 @@ +package mindustry.type.weather; + +import mindustry.type.*; + +//TODO +public class MagneticStorm extends Weather{ + + public MagneticStorm(String name){ + super(name); + } +} diff --git a/core/src/mindustry/type/weather/SolarFlare.java b/core/src/mindustry/type/weather/SolarFlare.java new file mode 100644 index 0000000000..47694d0e65 --- /dev/null +++ b/core/src/mindustry/type/weather/SolarFlare.java @@ -0,0 +1,11 @@ +package mindustry.type.weather; + +import mindustry.type.*; + +//TODO +public class SolarFlare extends Weather{ + + public SolarFlare(String name){ + super(name); + } +} diff --git a/core/src/mindustry/ui/Bar.java b/core/src/mindustry/ui/Bar.java index 8de089041a..6bab3a146d 100644 --- a/core/src/mindustry/ui/Bar.java +++ b/core/src/mindustry/ui/Bar.java @@ -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,7 +28,7 @@ public class Bar extends Element{ setColor(color); } - public Bar(Prov name, Prov color, Floatp fraction){ + public Bar(Prov name, Prov color, Floatp fraction){ this.fraction = fraction; try{ lastValue = value = Mathf.clamp(fraction.get()); @@ -61,6 +62,12 @@ public class Bar extends Element{ update(() -> this.name = name.get()); } + public Bar outline(Color color, float stroke){ + outlineColor.set(color); + outlineRadius = Scl.scl(stroke); + return this; + } + public Bar blink(Color color){ blinkColor.set(color); return this; @@ -94,6 +101,11 @@ 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); bar.draw(x, y, width, height); Draw.color(color, blinkColor, blink); diff --git a/core/src/mindustry/ui/Cicon.java b/core/src/mindustry/ui/Cicon.java index 2fc95294fa..846b7d136f 100644 --- a/core/src/mindustry/ui/Cicon.java +++ b/core/src/mindustry/ui/Cicon.java @@ -1,23 +1,12 @@ package mindustry.ui; -import java.util.*; - -/** Defines sizes of a content's preview icon. */ +/** Use content icon fields directly instead. This will be removed. */ +@Deprecated public enum Cicon{ - /** Full size. */ - full(0), - tiny(8 * 2), - small(8 * 3), - medium(8 * 4), - large(8 * 5), - xlarge(8 * 6); + tiny, small, medium, large, xlarge, full; + @Deprecated + public final int size = 32; 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; - } + public static final Cicon[] scaled = values(); } diff --git a/core/src/mindustry/ui/CoreItemsDisplay.java b/core/src/mindustry/ui/CoreItemsDisplay.java index 319c0213fd..5bfcdc51ee 100644 --- a/core/src/mindustry/ui/CoreItemsDisplay.java +++ b/core/src/mindustry/ui/CoreItemsDisplay.java @@ -41,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(); if(++i % 4 == 0){ row(); diff --git a/core/src/mindustry/ui/Fonts.java b/core/src/mindustry/ui/Fonts.java index 6bd969fe4b..3c6c1b175c 100644 --- a/core/src/mindustry/ui/Fonts.java +++ b/core/src/mindustry/ui/Fonts.java @@ -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.*; @@ -56,6 +53,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())); @@ -160,10 +161,9 @@ public class Fonts{ 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 scaled = new ObjectSet<>(); @Override @@ -197,7 +197,7 @@ public class Fonts{ size = 18; }})).loaded = f -> { Fonts.tech = (Font)f; - ((Font)f).getData().down *= 1.5f; + Fonts.tech.getData().down *= 1.5f; }; } @@ -214,13 +214,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 diff --git a/core/src/mindustry/ui/ItemDisplay.java b/core/src/mindustry/ui/ItemDisplay.java index d0cedc5ba1..faf2f3cc27 100644 --- a/core/src/mindustry/ui/ItemDisplay.java +++ b/core/src/mindustry/ui/ItemDisplay.java @@ -1,8 +1,12 @@ package mindustry.ui; +import arc.graphics.*; import arc.scene.ui.layout.*; +import arc.util.*; import mindustry.type.*; +import mindustry.world.meta.*; +//TODO replace with static methods? /** An item image with text. */ public class ItemDisplay extends Table{ public final Item item; @@ -23,4 +27,14 @@ public class ItemDisplay extends Table{ public ItemDisplay(Item item, int amount){ this(item, amount, true); } + + /** Displays the item with a "/sec" qualifier based on the time period, in ticks. */ + public ItemDisplay(Item item, int amount, float timePeriod, boolean showName){ + add(new ItemImage(item.uiIcon, amount)); + add(Strings.autoFixed(amount / (timePeriod / 60f), 2) + StatUnit.perSecond.localized()).padLeft(2).padRight(5).color(Color.lightGray).style(Styles.outlineLabel); + if(showName) add(item.localizedName).padLeft(4 + amount > 99 ? 4 : 0); + + this.item = item; + this.amount = amount; + } } diff --git a/core/src/mindustry/ui/ItemImage.java b/core/src/mindustry/ui/ItemImage.java index 437360829e..e0a4ce0b17 100644 --- a/core/src/mindustry/ui/ItemImage.java +++ b/core/src/mindustry/ui/ItemImage.java @@ -33,7 +33,7 @@ public class ItemImage extends Stack{ add(new Table(o -> { o.left(); - o.add(new Image(stack.item.icon(Cicon.medium))).size(32f); + o.add(new Image(stack.item.uiIcon)).size(32f); })); if(stack.amount != 0){ diff --git a/core/src/mindustry/ui/ItemsDisplay.java b/core/src/mindustry/ui/ItemsDisplay.java index a6dec71bc7..375aaffe57 100644 --- a/core/src/mindustry/ui/ItemsDisplay.java +++ b/core/src/mindustry/ui/ItemsDisplay.java @@ -41,7 +41,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(); diff --git a/core/src/mindustry/ui/LiquidDisplay.java b/core/src/mindustry/ui/LiquidDisplay.java index 0a3d7b1716..416a865336 100644 --- a/core/src/mindustry/ui/LiquidDisplay.java +++ b/core/src/mindustry/ui/LiquidDisplay.java @@ -7,6 +7,8 @@ import arc.util.*; import mindustry.type.*; import mindustry.world.meta.*; +import static mindustry.Vars.*; + /** An ItemDisplay, but for liquids. */ public class LiquidDisplay extends Table{ public final Liquid liquid; @@ -19,14 +21,14 @@ public class LiquidDisplay extends Table{ this.perSecond = perSecond; add(new Stack(){{ - add(new Image(liquid.icon(Cicon.medium))); + add(new Image(liquid.uiIcon)); if(amount != 0){ Table t = new Table().left().bottom(); t.add(Strings.autoFixed(amount, 2)).style(Styles.outlineLabel); add(t); } - }}).size(8 * 4).padRight(3 + (amount != 0 && Strings.autoFixed(amount, 2).length() > 2 ? 8 : 0)); + }}).size(iconMed).padRight(3 + (amount != 0 && Strings.autoFixed(amount, 2).length() > 2 ? 8 : 0)); if(perSecond){ add(StatUnit.perSecond.localized()).padLeft(2).padRight(5).color(Color.lightGray).style(Styles.outlineLabel); diff --git a/core/src/mindustry/ui/Styles.java b/core/src/mindustry/ui/Styles.java index ff234bf4c4..90ffbaae89 100644 --- a/core/src/mindustry/ui/Styles.java +++ b/core/src/mindustry/ui/Styles.java @@ -9,15 +9,16 @@ 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.*; +import mindustry.ui.dialogs.*; import static mindustry.gen.Tex.*; @@ -28,13 +29,14 @@ public class Styles{ public static ButtonStyle defaultb, waveb, modsb; public static TextButtonStyle defaultt, squaret, nodet, cleart, discordt, nonet, infot, clearPartialt, clearTogglet, logicTogglet, 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 ScrollPaneStyle defaultPane, horizontalPane, smallPane, nonePane; + public static KeybindDialog.KeybindDialogStyle defaultKeybindDialog; public static SliderStyle defaultSlider, vSlider; 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(){ black = whiteui.tint(0f, 0f, 0f, 1f); @@ -214,6 +216,7 @@ public class Styles{ }}; emptyi = new ImageButtonStyle(){{ imageDownColor = Pal.accent; + imageOverColor = Color.lightGray; imageUpColor = Color.white; }}; emptytogglei = new ImageButtonStyle(){{ @@ -306,8 +309,9 @@ public class Styles{ vScroll = clear; vScrollKnob = scrollKnobVerticalThin; }}; + nonePane = new ScrollPaneStyle(); - defaultKeybindDialog = new KeybindDialogStyle(){{ + defaultKeybindDialog = new KeybindDialog.KeybindDialogStyle(){{ keyColor = Pal.accent; keyNameColor = Color.white; controllerColor = Color.lightGray; @@ -411,6 +415,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(){ diff --git a/core/src/mindustry/ui/dialogs/BaseDialog.java b/core/src/mindustry/ui/dialogs/BaseDialog.java index b99b164ca4..42fb7ac7ff 100644 --- a/core/src/mindustry/ui/dialogs/BaseDialog.java +++ b/core/src/mindustry/ui/dialogs/BaseDialog.java @@ -4,7 +4,6 @@ import arc.*; import arc.scene.ui.*; import arc.util.*; import mindustry.core.GameState.*; -import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.graphics.*; @@ -44,12 +43,7 @@ public class BaseDialog extends Dialog{ } protected void onResize(Runnable run){ - Events.on(ResizeEvent.class, event -> { - if(isShown() && Core.scene.getDialog() == this){ - run.run(); - updateScrollFocus(); - } - }); + resized(run); } public void addCloseListener(){ diff --git a/core/src/mindustry/ui/dialogs/ContentInfoDialog.java b/core/src/mindustry/ui/dialogs/ContentInfoDialog.java index 1d03d360f3..82939448c3 100644 --- a/core/src/mindustry/ui/dialogs/ContentInfoDialog.java +++ b/core/src/mindustry/ui/dialogs/ContentInfoDialog.java @@ -8,6 +8,8 @@ import mindustry.ctype.*; import mindustry.graphics.*; import mindustry.world.meta.*; +import static mindustry.Vars.*; + public class ContentInfoDialog extends BaseDialog{ public ContentInfoDialog(){ @@ -26,9 +28,7 @@ public class ContentInfoDialog extends BaseDialog{ content.checkStats(); table.table(title1 -> { - var size = content.prefDatabaseIcon(); - - title1.image(content.icon(size)).size(size.size).scaling(Scaling.fit); + title1.image(content.uiIcon).size(iconXLarge).scaling(Scaling.fit); title1.add("[accent]" + content.localizedName).padLeft(5); }); diff --git a/core/src/mindustry/ui/dialogs/ControlsDialog.java b/core/src/mindustry/ui/dialogs/ControlsDialog.java deleted file mode 100644 index 400d5f086a..0000000000 --- a/core/src/mindustry/ui/dialogs/ControlsDialog.java +++ /dev/null @@ -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(210f, 64f); - - keyDown(key -> { - if(key == KeyCode.escape || key == KeyCode.back) hide(); - }); - } -} diff --git a/core/src/mindustry/ui/dialogs/CustomGameDialog.java b/core/src/mindustry/ui/dialogs/CustomGameDialog.java index efe5a157ea..1e9055a8e9 100644 --- a/core/src/mindustry/ui/dialogs/CustomGameDialog.java +++ b/core/src/mindustry/ui/dialogs/CustomGameDialog.java @@ -4,6 +4,7 @@ import arc.*; import arc.graphics.g2d.*; import arc.scene.style.*; import arc.scene.ui.*; +import arc.scene.ui.ImageButton.*; import arc.scene.ui.layout.*; import arc.util.*; import mindustry.*; @@ -31,14 +32,20 @@ public class CustomGameDialog extends BaseDialog{ cont.clear(); Table maps = new Table(); - maps.marginRight(14); - maps.marginBottom(55f); + maps.marginBottom(55f).marginRight(-20f); ScrollPane pane = new ScrollPane(maps); pane.setFadeScrollBars(false); int maxwidth = Math.max((int)(Core.graphics.getWidth() / Scl.scl(210)), 1); float images = 146f; + ImageButtonStyle style = new ImageButtonStyle(){{ + up = Styles.none; + down = Styles.flatOver; + over = Styles.flatOver; + disabled = Styles.none; + }}; + int i = 0; maps.defaults().width(170).fillY().top().pad(4f); for(Map map : Vars.maps.all()){ @@ -47,7 +54,7 @@ public class CustomGameDialog extends BaseDialog{ maps.row(); } - ImageButton image = new ImageButton(new TextureRegion(map.safeTexture()), Styles.cleari); + ImageButton image = new ImageButton(new TextureRegion(map.safeTexture()), style); image.margin(5); image.top(); @@ -86,6 +93,6 @@ public class CustomGameDialog extends BaseDialog{ maps.add("@maps.none").pad(50); } - cont.add(pane).uniformX(); + cont.add(pane).grow(); } } diff --git a/core/src/mindustry/ui/dialogs/CustomRulesDialog.java b/core/src/mindustry/ui/dialogs/CustomRulesDialog.java index 94ea454d21..bf150bcd6a 100644 --- a/core/src/mindustry/ui/dialogs/CustomRulesDialog.java +++ b/core/src/mindustry/ui/dialogs/CustomRulesDialog.java @@ -72,7 +72,7 @@ public class CustomRulesDialog extends BaseDialog{ 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.image(block.uiIcon).size(iconMed).padRight(3); b.add(block.localizedName).color(Color.lightGray).padLeft(3).growX().left().wrap(); b.button(Icon.cancel, Styles.clearPartiali, () -> { @@ -94,11 +94,11 @@ public class CustomRulesDialog extends BaseDialog{ 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, () -> { + t.button(new TextureRegionDrawable(b.uiIcon), Styles.cleari, iconMed, () -> { rules.bannedBlocks.add(b); rebuildBanned(); dialog.hide(); - }).size(60f).get().resizeImage(Cicon.medium.size); + }).size(60f); if(++i[0] % cols == 0){ t.row(); @@ -149,7 +149,7 @@ public class CustomRulesDialog extends BaseDialog{ number("@rules.blockdamagemultiplier", f -> rules.blockDamageMultiplier = f, () -> rules.blockDamageMultiplier); main.button("@configure", - () -> loadoutDialog.show(Blocks.coreShard.itemCapacity, rules.loadout, + () -> loadoutDialog.show(999999, rules.loadout, i -> true, () -> rules.loadout.clear().add(new ItemStack(Items.copper, 100)), () -> {}, () -> {} @@ -169,6 +169,7 @@ public class CustomRulesDialog extends BaseDialog{ title("@rules.title.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); + check("@rules.corecapture", b -> rules.coreCapture = b, () -> rules.coreCapture); number("@rules.enemycorebuildradius", f -> rules.enemyCoreBuildRadius = f * tilesize, () -> Math.min(rules.enemyCoreBuildRadius / tilesize, 200)); title("@rules.title.environment"); diff --git a/core/src/mindustry/ui/dialogs/DatabaseDialog.java b/core/src/mindustry/ui/dialogs/DatabaseDialog.java index e936c418fc..5c2aa76d17 100644 --- a/core/src/mindustry/ui/dialogs/DatabaseDialog.java +++ b/core/src/mindustry/ui/dialogs/DatabaseDialog.java @@ -56,7 +56,7 @@ public class DatabaseDialog extends BaseDialog{ for(int i = 0; i < array.size; i++){ UnlockableContent unlock = (UnlockableContent)array.get(i); - Image image = unlocked(unlock) ? new Image(unlock.icon(Cicon.medium)).setScaling(Scaling.fit) : new Image(Icon.lock, Pal.gray); + Image image = unlocked(unlock) ? new Image(unlock.uiIcon).setScaling(Scaling.fit) : new Image(Icon.lock, Pal.gray); list.add(image).size(8 * 4).pad(3); ClickListener listener = new ClickListener(); image.addListener(listener); diff --git a/core/src/mindustry/ui/dialogs/GameOverDialog.java b/core/src/mindustry/ui/dialogs/GameOverDialog.java index 8c69756a3c..083e49c116 100644 --- a/core/src/mindustry/ui/dialogs/GameOverDialog.java +++ b/core/src/mindustry/ui/dialogs/GameOverDialog.java @@ -66,7 +66,7 @@ public class GameOverDialog extends BaseDialog{ 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); + items.image(item.uiIcon).size(8 * 3).pad(4); }).left().row(); } } diff --git a/core/src/mindustry/ui/dialogs/HostDialog.java b/core/src/mindustry/ui/dialogs/HostDialog.java index 682f0a010f..05cd5375ce 100644 --- a/core/src/mindustry/ui/dialogs/HostDialog.java +++ b/core/src/mindustry/ui/dialogs/HostDialog.java @@ -64,7 +64,7 @@ public class HostDialog extends BaseDialog{ Time.runTask(5f, () -> { try{ net.host(Vars.port); - player.admin(true); + player.admin = true; if(steam){ Core.app.post(() -> Core.settings.getBoolOnce("steampublic3", () -> { diff --git a/core/src/mindustry/ui/dialogs/JoinDialog.java b/core/src/mindustry/ui/dialogs/JoinDialog.java index 2077d5e091..636a91d212 100644 --- a/core/src/mindustry/ui/dialogs/JoinDialog.java +++ b/core/src/mindustry/ui/dialogs/JoinDialog.java @@ -50,9 +50,7 @@ public class JoinDialog extends BaseDialog{ addCloseButton(); buttons.add().growX().width(-1); - if(!steam){ - buttons.button("?", () -> ui.showInfo("@join.info")).size(60f, 64f).width(-1); - } + if(!steam) buttons.button("?", () -> ui.showInfo("@join.info")).size(60f, 64f); add = new BaseDialog("@joingame.title"); add.cont.add("@joingame.ip").padRight(5f).left(); @@ -97,8 +95,12 @@ 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 + if(Math.min(Core.graphics.getWidth(), Core.graphics.getHeight()) / Scl.scl() * 0.9f < 500f){ + setup(); + refreshAll(); + } }); } @@ -402,6 +404,7 @@ public class JoinDialog extends BaseDialog{ global.background(null); float w = targetWidth(); + //TODO looks bad container.button(b -> buildServer(host, b), Styles.cleart, () -> { Events.fire(new ClientPreConnectEvent(host)); if(!Core.settings.getBool("server-disclaimer", false)){ diff --git a/core/src/mindustry/ui/dialogs/KeybindDialog.java b/core/src/mindustry/ui/dialogs/KeybindDialog.java new file mode 100644 index 0000000000..4674c8f60e --- /dev/null +++ b/core/src/mindustry/ui/dialogs/KeybindDialog.java @@ -0,0 +1,262 @@ +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.style.*; +import arc.scene.ui.*; +import arc.scene.ui.layout.*; +import arc.struct.*; +import arc.util.*; +import mindustry.gen.*; +import mindustry.graphics.*; + +import static arc.Core.*; + +public class KeybindDialog extends Dialog{ + protected KeybindDialogStyle style; + protected Section section; + protected KeyBind rebindKey = null; + protected boolean rebindAxis = false; + protected boolean rebindMin = true; + protected KeyCode minKey = null; + protected Dialog rebindDialog; + protected ObjectIntMap
sectionControls = new ObjectIntMap<>(); + + public KeybindDialog(){ + super(bundle.get("keybind.title", "Rebind Keys")); + KeybindDialog.this.style = scene.getStyle(KeybindDialogStyle.class); + KeybindDialog.this.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(); + }); + } + + public void setStyle(KeybindDialogStyle style){ + this.style = style; + setup(); + } + + private void setup(){ + cont.clear(); + + Section[] sections = Core.keybinds.getSections(); + + Stack stack = new Stack(); + ButtonGroup group = new ButtonGroup<>(); + ScrollPane pane = new ScrollPane(stack); + pane.setFadeScrollBars(false); + 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){ + //TODO toggle style + TextButton button = new TextButton(bundle.get("section." + section.name + ".name", Strings.capitalize(section.name))/*, "toggle"*/); + 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 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); + + table.add(stable).colspan(4); + + table.row(); + table.add().height(10); + table.row(); + if(section.device.type() == DeviceType.controller){ + table.table(info -> info.add("Controller Type: [#" + style.controllerColor.toString().toUpperCase() + "]" + + Strings.capitalize(section.device.name())).left()); + } + table.row(); + + String lastCategory = null; + + 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(); + } + + Axis axis = keybinds.get(section, keybind); + + if(keybind.defaultValue(section.device.type()) instanceof Axis){ + table.add(bundle.get("keybind." + keybind.name() + ".name", Strings.capitalize(keybind.name())), style.keyNameColor).left().padRight(40).padLeft(8); + + if(axis.key != null){ + table.add(axis.key.toString(), style.keyColor).left().minWidth(90).padRight(20); + }else{ + Table axt = new Table(); + axt.left(); + axt.labelWrap(axis.min.toString() + " [red]/[] " + axis.max.toString()).color(style.keyColor).width(140f).padRight(5); + table.add(axt).left().minWidth(90).padRight(20); + } + + table.button(bundle.get("settings.rebind", "Rebind"), () -> { + rebindAxis = true; + rebindMin = true; + openDialog(section, keybind); + }).width(130f); + }else{ + table.add(bundle.get("keybind." + keybind.name() + ".name", Strings.capitalize(keybind.name())), + style.keyNameColor).left().padRight(40).padLeft(8); + table.add(keybinds.get(section, keybind).key.toString(), + style.keyColor).left().minWidth(90).padRight(20); + + table.button(bundle.get("settings.rebind", "Rebind"), () -> { + rebindAxis = false; + rebindMin = false; + openDialog(section, keybind); + }).width(130f); + } + table.button(bundle.get("settings.resetKey", "Reset"), () -> { + keybinds.resetToDefault(section, keybind); + setup(); + }).width(130f); + table.row(); + } + + table.visible(() -> this.section.equals(section)); + + table.button(bundle.get("settings.reset", "Reset to Defaults"), () -> { + keybinds.resetToDefaults(); + setup(); + }).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; + setup(); + } + } + + private void openDialog(Section section, KeyBind name){ + rebindDialog = new Dialog(rebindAxis ? bundle.get("keybind.press.axis", "Press an axis or key...") : bundle.get("keybind.press", "Press a key...")); + + rebindKey = name; + + rebindDialog.titleTable.getCells().first().pad(4); + + if(section.device.type() == DeviceType.keyboard){ + rebindDialog.keyDown(i -> setup()); + + 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(); + if(keycode == KeyCode.escape) return false; + 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)); + } + + public static class KeybindDialogStyle extends Style{ + public Color keyColor = Color.white; + public Color keyNameColor = Color.white; + public Color controllerColor = Color.white; + } +} diff --git a/core/src/mindustry/ui/dialogs/LanguageDialog.java b/core/src/mindustry/ui/dialogs/LanguageDialog.java index 41519c32fd..6504bbe60c 100644 --- a/core/src/mindustry/ui/dialogs/LanguageDialog.java +++ b/core/src/mindustry/ui/dialogs/LanguageDialog.java @@ -13,9 +13,41 @@ import static mindustry.Vars.*; public class LanguageDialog extends BaseDialog{ private Locale lastLocale; - private ObjectMap displayNames = ObjectMap.of( - Locale.TRADITIONAL_CHINESE, "正體中文", - Locale.SIMPLIFIED_CHINESE, "简体中文" + private ObjectMap displayNames = ObjectMap.of( + "in_ID", "Bahasa Indonesia (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", "Русский", + "uk_UA", "Українська (Україна)", + "th", "ไทย", + "zh_CN", "简体中文", + "zh_TW", "正體中文", + "ja", "日本語", + "ko", "한국어", + "router", "router" ); public LanguageDialog(){ @@ -33,7 +65,7 @@ public class LanguageDialog extends BaseDialog{ ButtonGroup 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(displayNames.get(loc.toString(), loc.getDisplayName(Locale.ROOT)), Styles.clearTogglet); button.clicked(() -> { if(getLocale().equals(loc)) return; Core.settings.put("locale", loc.toString()); diff --git a/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java b/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java index b9e05308e3..d91ae8530a 100644 --- a/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java +++ b/core/src/mindustry/ui/dialogs/LaunchLoadoutDialog.java @@ -64,7 +64,7 @@ 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); + table.image(s.item.uiIcon).left().size(iconSmall); int as = schems.get(s.item), al = launches.get(s.item); String amountStr = (al + as) + "[gray] (" + (al + " + " + as + ")"); diff --git a/core/src/mindustry/ui/dialogs/LoadoutDialog.java b/core/src/mindustry/ui/dialogs/LoadoutDialog.java index 311e4d2ea7..eff8f24164 100644 --- a/core/src/mindustry/ui/dialogs/LoadoutDialog.java +++ b/core/src/mindustry/ui/dialogs/LoadoutDialog.java @@ -111,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(); diff --git a/core/src/mindustry/ui/dialogs/MapsDialog.java b/core/src/mindustry/ui/dialogs/MapsDialog.java index e0783b7965..ee25c0ae28 100644 --- a/core/src/mindustry/ui/dialogs/MapsDialog.java +++ b/core/src/mindustry/ui/dialogs/MapsDialog.java @@ -49,7 +49,7 @@ public class MapsDialog extends BaseDialog{ Runnable show = () -> ui.loadAnd(() -> { hide(); ui.editor.show(); - ui.editor.editor.tags.put("name", text); + editor.tags.put("name", text); Events.fire(new MapMakeEvent()); }); diff --git a/core/src/mindustry/ui/dialogs/ModsDialog.java b/core/src/mindustry/ui/dialogs/ModsDialog.java index 0328422bb0..de974ce8d8 100644 --- a/core/src/mindustry/ui/dialogs/ModsDialog.java +++ b/core/src/mindustry/ui/dialogs/ModsDialog.java @@ -147,11 +147,12 @@ public class ModsDialog extends BaseDialog{ } void setup(){ + boolean squish = Core.graphics.isPortrait(); float h = 110f; - float w = mobile ? 440f : 524f; + float w = squish ? 410f : 524f; cont.clear(); - cont.defaults().width(mobile ? 500 : 560f).pad(4); + cont.defaults().width(squish ? 480 : 560f).pad(4); cont.add("@mod.reloadrequired").visible(mods::requiresReload).center().get().setAlignment(Align.center); cont.row(); @@ -365,7 +366,7 @@ public class ModsDialog extends BaseDialog{ 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.cleari, iconMed, () -> { ui.content.show(c); }).size(50f).with(im -> { var click = im.getClickListener(); diff --git a/core/src/mindustry/ui/dialogs/PausedDialog.java b/core/src/mindustry/ui/dialogs/PausedDialog.java index 5e9b5d1666..1226d59324 100644 --- a/core/src/mindustry/ui/dialogs/PausedDialog.java +++ b/core/src/mindustry/ui/dialogs/PausedDialog.java @@ -93,12 +93,19 @@ public class PausedDialog extends BaseDialog{ } void showQuitConfirm(){ - ui.showConfirm("@confirm", "@quit.confirm", () -> { + Runnable quit = () -> { wasClient = net.client(); if(net.client()) netClient.disconnectQuietly(); runExitSave(); hide(); - }); + }; + + if(confirmExit){ + ui.showConfirm("@confirm", "@quit.confirm", quit); + }else{ + quit.run(); + } + } public void runExitSave(){ diff --git a/core/src/mindustry/ui/dialogs/PlanetDialog.java b/core/src/mindustry/ui/dialogs/PlanetDialog.java index 18d9a8c706..1e671c43b1 100644 --- a/core/src/mindustry/ui/dialogs/PlanetDialog.java +++ b/core/src/mindustry/ui/dialogs/PlanetDialog.java @@ -10,6 +10,7 @@ import arc.math.*; import arc.math.geom.*; import arc.scene.*; import arc.scene.event.*; +import arc.scene.style.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.struct.*; @@ -36,6 +37,13 @@ import static mindustry.graphics.g3d.PlanetRenderer.*; import static mindustry.ui.dialogs.PlanetDialog.Mode.*; public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ + static final String[] defaultIcons = { + "effect", "power", "logic", "units", "liquid", "production", "defense", "turret", "distribution", "crafting", + "settings", "cancel", "zoom", "ok", "star", "home", "pencil", "up", "down", "left", "right", + "hammer", "warning", "tree", "admin", "map", "modePvp", "terrain", + "modeSurvival", "commandRally", "commandAttack", + }; + //if true, enables launching anywhere for testing public static boolean debugSelect = false; public static float sectorShowDuration = 60f * 2.4f; @@ -61,6 +69,8 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ super("", Styles.fullDialog); shouldPause = true; + planets.planet = content.getByName(ContentType.planet, Core.settings.getString("lastplanet", "serpulo")); + if(planets.planet == null) planets.planet = Planets.serpulo; keyDown(key -> { if(key == KeyCode.escape || key == KeyCode.back || key == Core.keybinds.get(Binding.planet_map).key){ @@ -163,12 +173,13 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ launchSector = state.getSector(); presetShow = 0f; showed = false; + listener = s -> {}; newPresets.clear(); //announce new presets for(SectorPreset preset : content.sectors()){ - if(preset.unlocked() && !preset.alwaysUnlocked && !preset.sector.info.shown && !preset.sector.hasBase()){ + if(preset.unlocked() && !preset.alwaysUnlocked && !preset.sector.info.shown && !preset.sector.hasBase() && preset.planet == planets.planet){ newPresets.add(preset.sector); preset.sector.info.shown = true; preset.sector.saveInfo(); @@ -229,6 +240,24 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ dialog.add("@sectors.captured"); } + //TODO + public void showPlanetLaunch(Sector sector, Cons listener){ + selected = null; + hovered = null; + launching = false; + this.listener = listener; + launchSector = sector; + + //update view to sector + zoom = 1f; + planets.zoom = 1f; + selectAlpha = 0f; + + mode = planetLaunch; + + super.show(); + } + public void showSelect(Sector sector, Cons listener){ selected = null; hovered = null; @@ -253,7 +282,9 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ boolean canSelect(Sector sector){ if(mode == select) return sector.hasBase(); - if(sector.hasBase()) return true; + //cannot launch to existing sector w/ accelerator + if(mode == planetLaunch) return !sector.hasBase(); + if(sector.hasBase() || sector.id == sector.planet.startSector) return true; //preset sectors can only be selected once unlocked if(sector.preset != null){ TechNode node = sector.preset.node(); @@ -369,7 +400,8 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ if(icon != null){ planets.drawPlane(sec, () -> { - Draw.color(color, selectAlpha); + //use white for content icons + Draw.color(preficon == icon && sec.info.contentIcon != null ? Color.white : color, selectAlpha); Draw.rect(icon, 0, 0, iw, iw * icon.height / icon.width); }); } @@ -395,6 +427,12 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ Draw.reset(); } + boolean selectable(Planet planet){ + //TODO what if any sector is selectable? + if(mode == planetLaunch) return launchSector != null && planet != launchSector.planet; + return planet == planets.planet || planet.alwaysUnlocked || planet.sectors.contains(Sector::hasBase); + } + void setup(){ zoom = planets.zoom = 1f; selectAlpha = 1f; @@ -447,7 +485,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ //planet selection new Table(t -> { t.right(); - if(content.planets().count(p -> p.accessible) > 1){ + if(content.planets().count(this::selectable) > 1){ t.table(Styles.black6, pt -> { pt.add("@planets").color(Pal.accent); pt.row(); @@ -455,11 +493,12 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ pt.row(); for(int i = 0; i < content.planets().size; i++){ Planet planet = content.planets().get(i); - if(planet.accessible){ + if(selectable(planet)){ pt.button(planet.localizedName, Styles.clearTogglet, () -> { selected = null; launchSector = null; renderer.planets.planet = planet; + Core.settings.put("lastplanet", planet.name); }).width(200).height(40).growX().update(bb -> bb.setChecked(renderer.planets.planet == planet)); pt.row(); } @@ -516,7 +555,11 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ if(hovered != null){ StringBuilder tx = hoverLabel.getText(); if(!canSelect(hovered)){ - tx.append("[gray]").append(Iconc.lock).append(" ").append(Core.bundle.get("locked")); + if(mode == planetLaunch){ + tx.append("[gray]").append(Iconc.cancel); + }else{ + tx.append("[gray]").append(Iconc.lock).append(" ").append(Core.bundle.get("locked")); + } }else{ tx.append("[accent][[ [white]").append(hovered.name()).append("[accent] ]"); } @@ -574,7 +617,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ stats.each((item, stat) -> { int total = (int)(stat.mean * 60 * scl); if(total > 1){ - t.image(item.icon(Cicon.small)).padRight(3); + t.image(item.uiIcon).padRight(3); t.add(UI.formatAmount(total) + " " + Core.bundle.get("unit.perminute")).color(Color.lightGray).padRight(3); if(++i[0] % 3 == 0){ t.row(); @@ -604,7 +647,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ c.add("@sectors.resources").left().row(); c.table(t -> { for(UnlockableContent uc : sector.info.resources){ - t.image(uc.icon(Cicon.small)).padRight(3).size(Cicon.small.size); + t.image(uc.uiIcon).padRight(3).size(iconSmall); } }).padLeft(10f).left().row(); } @@ -628,7 +671,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ int i = 0; for(ItemStack stack : items){ - res.image(stack.item.icon(Cicon.small)).padRight(3); + res.image(stack.item.uiIcon).padRight(3); res.add(UI.formatAmount(Math.max(stack.amount, 0))).color(Color.lightGray); if(++i % 4 == 0){ res.row(); @@ -671,41 +714,69 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ }).size(40f).padLeft(4); } - var icon = Icon.icons.get(sector.info.icon + "Small"); + var icon = sector.info.contentIcon != null ? + new TextureRegionDrawable(sector.info.contentIcon.uiIcon) : + Icon.icons.get(sector.info.icon + "Small"); - title.button(icon == null ? Icon.noneSmall : icon, Styles.clearPartiali, () -> { + title.button(icon == null ? Icon.noneSmall : icon, Styles.clearPartiali, iconSmall, () -> { new Dialog(""){{ closeOnBack(); setFillParent(true); cont.pane(t -> { - t.marginRight(19f); - t.defaults().size(48f); + resized(true, () -> { + t.clearChildren(); + t.marginRight(19f); + t.defaults().size(48f); - t.button(Icon.none, Styles.clearTogglei, () -> { - sector.info.icon = null; - sector.saveInfo(); - hide(); - updateSelected(); - }).checked(sector.info.icon == null); - - int i = 1; - for(var entry : Icon.icons.entries()){ - if(entry.key.endsWith("Small") || entry.key.contains("none")) continue; - String key = entry.key; - - t.button(entry.value, Styles.cleari, () -> { - sector.info.icon = key; + t.button(Icon.none, Styles.clearTogglei, () -> { + sector.info.icon = null; sector.saveInfo(); hide(); updateSelected(); - }).checked(entry.key.equals(sector.info.icon)); + }).checked(sector.info.icon == null); - if(++i % 8 == 0) t.row(); - } + int cols = (int)Math.min(20, Core.graphics.getWidth() / 52f); + + int i = 1; + for(var key : defaultIcons){ + var value = Icon.icons.get(key); + + t.button(value, Styles.cleari, () -> { + sector.info.icon = key; + sector.info.contentIcon = null; + sector.saveInfo(); + hide(); + updateSelected(); + }).checked(key.equals(sector.info.icon)); + + if(++i % cols == 0) t.row(); + } + + for(ContentType ctype : defaultContentIcons){ + t.row(); + t.image().colspan(cols).growX().width(Float.NEGATIVE_INFINITY).height(3f).color(Pal.accent); + t.row(); + + i = 0; + for(UnlockableContent u : content.getBy(ctype).as()){ + if(!u.isHidden() && u.unlocked()){ + t.button(new TextureRegionDrawable(u.uiIcon), Styles.cleari, iconMed, () -> { + sector.info.icon = null; + sector.info.contentIcon = u; + sector.saveInfo(); + hide(); + updateSelected(); + }).checked(sector.info.contentIcon == u); + + if(++i % cols == 0) t.row(); + } + } + } + }); }); buttons.button("@back", Icon.left, this::hide).size(210f, 64f); }}.show(); - }).size(40f); + }).size(40f).tooltip("@sector.changeicon"); }).row(); stable.image().color(Pal.accent).fillX().height(3f).pad(3f).row(); @@ -752,7 +823,7 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ t.add("@sectors.resources").padRight(4); for(UnlockableContent c : sector.info.resources){ if(c == null) continue; //apparently this is possible. - t.image(c.icon(Cicon.small)).padRight(3).size(Cicon.small.size); + t.image(c.uiIcon).padRight(3).size(iconSmall); } }).padLeft(10f).fillX().row(); } @@ -825,7 +896,12 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ if(mode == look && !sector.hasBase()){ shouldHide = false; Sector from = findLauncher(sector); - if(from == null){ + if(from == null || mode == planetLaunch){ + //TODO use the standard nucleus core schematic. + if(mode == planetLaunch){ + listener.get(sector); + } + //clear loadout information, so only the basic loadout gets used universe.clearLoadoutInfo(); //free launch. @@ -857,6 +933,8 @@ public class PlanetDialog extends BaseDialog implements PlanetInterfaceRenderer{ /** Look around for existing sectors. Can only deploy. */ look, /** Select a sector for some purpose. */ - select + select, + /** Launch between planets. */ + planetLaunch } } diff --git a/core/src/mindustry/ui/dialogs/ResearchDialog.java b/core/src/mindustry/ui/dialogs/ResearchDialog.java index f78d98e77b..dbe7caa19f 100644 --- a/core/src/mindustry/ui/dialogs/ResearchDialog.java +++ b/core/src/mindustry/ui/dialogs/ResearchDialog.java @@ -109,6 +109,9 @@ public class ResearchDialog extends BaseDialog{ checkNodes(root); treeLayout(); + view.hoverNode = null; + view.infoTable.remove(); + view.infoTable.clear(); }); hidden(ui.planet::setup); @@ -292,7 +295,7 @@ public class ResearchDialog extends BaseDialog{ 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.visible(() -> node.visible); button.clicked(() -> { if(moved) return; @@ -339,7 +342,7 @@ public class ResearchDialog extends BaseDialog{ 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; - ((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); }); @@ -528,7 +531,7 @@ public class ResearchDialog extends BaseDialog{ int reqAmount = req.amount - completed.amount; list.left(); - list.image(req.item.icon(Cicon.small)).size(8 * 3).padRight(3); + 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)) + " / " diff --git a/core/src/mindustry/ui/dialogs/SchematicsDialog.java b/core/src/mindustry/ui/dialogs/SchematicsDialog.java index 44b54df434..a4616af643 100644 --- a/core/src/mindustry/ui/dialogs/SchematicsDialog.java +++ b/core/src/mindustry/ui/dialogs/SchematicsDialog.java @@ -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,11 @@ 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,18 +24,27 @@ 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 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); + tags = Core.settings.getJson("schematic-tags", Seq.class, String.class, Seq::new); + shouldPause = true; addCloseButton(); buttons.button("@schematic.import", Icon.download, this::showImport); @@ -40,8 +53,12 @@ public class SchematicsDialog extends BaseDialog{ } void setup(){ + if(!checkedTags){ + checkTags(); + checkedTags = true; + } + search = ""; - Runnable[] rebuildPane = {null}; cont.top(); cont.clear(); @@ -51,15 +68,46 @@ public class SchematicsDialog extends BaseDialog{ s.image(Icon.zoom); searchField = s.field(search, res -> { search = res; - rebuildPane[0].run(); + rebuildPane.run(); }).growX().get(); }).fillX().padBottom(4); cont.row(); + cont.table(in -> { + in.left(); + in.add("@schematic.tags").padRight(4); + + //tags (no scroll pane visible) + in.pane(Styles.nonePane, 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).get().setScrollingDisabled(false, true); + + in.button(Icon.pencilSmall, () -> { + 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){ @@ -72,18 +120,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}; @@ -106,20 +156,27 @@ public class SchematicsDialog extends BaseDialog{ buttons.button(Icon.pencil, style, () -> { new Dialog("@schematic.rename"){{ + setFillParent(true); + + 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).addInputDialog().get(); + TextField nameField = cont.field(s.name(), null).size(400f, 55f).addInputDialog().left().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(); + TextField descField = cont.area(s.description(), Styles.areaField, t -> {}).size(400f, 140f).left().addInputDialog().get(); Runnable accept = () -> { s.tags.put("name", nameField.getText()); - s.tags.put("description", descripionField.getText()); + s.tags.put("description", descField.getText()); s.save(); hide(); - rebuildPane[0].run(); + rebuildPane.run(); }; buttons.defaults().size(120, 54).pad(4); @@ -127,7 +184,7 @@ public class SchematicsDialog extends BaseDialog{ buttons.button("@cancel", this::hide); keyDown(KeyCode.enter, () -> { - if(!nameField.getText().isEmpty() && Core.scene.getKeyboardFocus() != descripionField){ + if(!nameField.getText().isEmpty() && Core.scene.getKeyboardFocus() != descField){ accept.run(); } }); @@ -146,7 +203,7 @@ public class SchematicsDialog extends BaseDialog{ }else{ ui.showConfirm("@confirm", "@schematic.delete.confirm", () -> { schematics.remove(s); - rebuildPane[0].run(); + rebuildPane.run(); }); } }); @@ -188,8 +245,8 @@ public class SchematicsDialog extends BaseDialog{ } }; - rebuildPane[0].run(); - }).get().setScrollingDisabled(true, false); + rebuildPane.run(); + }).grow().get().setScrollingDisabled(true, false); } public void showInfo(Schematic schematic){ @@ -212,6 +269,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 +285,7 @@ public class SchematicsDialog extends BaseDialog{ schematics.add(s); setup(); showInfo(s); + checkTags(s); }catch(Exception e){ ui.showException(e); } @@ -281,6 +340,315 @@ public class SchematicsDialog extends BaseDialog{ Core.scene.setKeyboardFocus(searchField); } + + //adds all new tags to the global list of tags + //alternatively, unknown tags could be discarded on import? + void checkTags(){ + ObjectSet 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 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 cons){ + new Dialog(){{ + closeOnBack(); + setFillParent(true); + + cont.pane(t -> { + resized(true, () -> { + t.clearChildren(); + t.marginRight(19f); + t.defaults().size(48f); + + int cols = (int)Math.min(20, Core.graphics.getWidth() / 52f); + + /* + int i = 0; + for(var key : defaultIcons){ + var value = Icon.icons.get(key); + + t.button(value, Styles.cleari, () -> { + sector.info.icon = key; + sector.info.contentIcon = null; + sector.saveInfo(); + hide(); + updateSelected(); + }).checked(key.equals(sector.info.icon)); + + if(++i % cols == 0) t.row(); + }*/ + + int i = 0; + + for(ContentType ctype : defaultContentIcons){ + t.row(); + t.image().colspan(cols).growX().width(Float.NEGATIVE_INFINITY).height(3f).color(Pal.accent); + t.row(); + + i = 0; + for(UnlockableContent u : content.getBy(ctype).as()){ + if(!u.isHidden() && u.unlockedNow() && u.hasEmoji() && !tags.contains(u.emoji())){ + t.button(new TextureRegionDrawable(u.uiIcon), Styles.cleari, iconMed, () -> { + String out = u.emoji() + ""; + + tags.add(out); + tagsChanged(); + cons.get(out); + + hide(); + }); + + if(++i % cols == 0) t.row(); + } + } + } + }); + }); + 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.defaults().fillX().left(); + + float sum = 0f; + Table current = new Table().left(); + + for(var tag : tags){ + + var next = new Table(Tex.button, n -> { + n.add(tag).padRight(4); + + n.add().growX(); + n.defaults().size(30f); + + //delete + n.button(Icon.cancelSmall, 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(); + }); + }); + //rename + n.button(Icon.pencilSmall, 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(); + } + }); + }); + //move + n.button(Icon.upSmall, Styles.emptyi, () -> { + int idx = tags.indexOf(tag); + if(idx > 0){ + tags.swap(idx, idx - 1); + tagsChanged(); + rebuild[0].run(); + } + }); + }); + + 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(); + } + + 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]); + }); + 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).get().setScrollingDisabled(false, true); + + t.button(Icon.addSmall, () -> { + var dialog = new BaseDialog("@schematic.addtag"); + dialog.addCloseButton(); + dialog.cont.pane(p -> resized(true, () -> { + p.clearChildren(); + + 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 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(); @@ -298,6 +666,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 +689,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,13 +717,13 @@ 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(""); @@ -366,6 +737,8 @@ public class SchematicsDialog extends BaseDialog{ cont.add(Core.bundle.format("schematic.info", schem.width, schem.height, schem.tiles.size)).color(Color.lightGray); cont.row(); + cont.table(tags -> buildTags(schem, tags)).fillX().left().row(); + cont.row(); cont.add(new SchematicImage(schem)).maxSize(800f); cont.row(); @@ -373,7 +746,7 @@ public class SchematicsDialog extends BaseDialog{ 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 + ""; diff --git a/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java b/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java index 5290b6c5aa..2b89f153aa 100644 --- a/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java +++ b/core/src/mindustry/ui/dialogs/SettingsMenuDialog.java @@ -2,11 +2,10 @@ 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.ui.*; import arc.scene.ui.TextButton.*; import arc.scene.ui.layout.*; @@ -31,10 +30,11 @@ import static arc.Core.*; import static mindustry.Vars.net; import static mindustry.Vars.*; -public class SettingsMenuDialog extends SettingsDialog{ +public class SettingsMenuDialog extends Dialog{ public SettingsTable graphics; public SettingsTable game; public SettingsTable sound; + public SettingsTable main; private Table prefs; private Table menu; @@ -42,6 +42,11 @@ public class SettingsMenuDialog extends SettingsDialog{ private boolean wasPaused; public SettingsMenuDialog(){ + super(bundle.get("settings", "Settings")); + addCloseButton(); + + cont.add(main = new SettingsTable()); + hidden(() -> { Sounds.back.play(); if(state.isGame()){ @@ -60,6 +65,15 @@ public class SettingsMenuDialog extends SettingsDialog{ rebuildMenu(); }); + Events.on(ResizeEvent.class, event -> { + if(isShown() && Core.scene.getDialog() == this){ + graphics.rebuild(); + sound.rebuild(); + game.rebuild(); + updateScrollFocus(); + } + }); + setFillParent(true); title.setAlignment(Align.center); titleTable.row(); @@ -222,29 +236,8 @@ public class SettingsMenuDialog extends SettingsDialog{ }).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(); @@ -296,9 +289,13 @@ public class SettingsMenuDialog extends SettingsDialog{ game.screenshakePref(); if(mobile){ game.checkPref("autotarget", true); - game.checkPref("keyboard", false, val -> control.setInput(val ? new DesktopInput() : new MobileInput())); + 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); } } //the issue with touchscreen support on desktop is that: @@ -346,10 +343,11 @@ public class SettingsMenuDialog extends SettingsDialog{ } } + int[] lastUiScale = {settings.getInt("uiscale", 100)}; + graphics.sliderPref("uiscale", 100, 25, 300, 25, s -> { - if(ui.settings != null){ - Core.settings.put("uiscalechanged", true); - } + //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))); @@ -412,15 +410,12 @@ public class SettingsMenuDialog extends SettingsDialog{ 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){ @@ -538,4 +533,174 @@ public class SettingsMenuDialog extends SettingsDialog{ } }); } + + public interface StringProcessor{ + String get(int i); + } + + public static class SettingsTable extends Table{ + protected Seq list = new Seq<>(); + + public SettingsTable(){ + left(); + } + + public Seq getSettings(){ + return list; + } + + public void pref(Setting setting){ + list.add(setting); + rebuild(); + } + + public void screenshakePref(){ + sliderPref("screenshake", bundle.get("setting.screenshake.name", "Screen Shake"), 4, 0, 8, i -> (i / 4f) + "x"); + } + + public SliderSetting sliderPref(String name, String title, int def, int min, int max, StringProcessor s){ + return sliderPref(name, title, def, min, max, 1, s); + } + + public SliderSetting sliderPref(String name, String title, int def, int min, int max, int step, StringProcessor s){ + SliderSetting res; + list.add(res = new SliderSetting(name, title, def, min, max, step, s)); + settings.defaults(name, def); + rebuild(); + return res; + } + + 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, bundle.get("setting." + name + ".name"), def, min, max, step, s)); + settings.defaults(name, def); + rebuild(); + return res; + } + + public void checkPref(String name, String title, boolean def){ + list.add(new CheckSetting(name, title, def, null)); + settings.defaults(name, def); + rebuild(); + } + + public void checkPref(String name, String title, boolean def, Boolc changed){ + list.add(new CheckSetting(name, title, def, changed)); + settings.defaults(name, def); + rebuild(); + } + + /** Localized title. */ + public void checkPref(String name, boolean def){ + list.add(new CheckSetting(name, bundle.get("setting." + name + ".name"), def, null)); + settings.defaults(name, def); + rebuild(); + } + + /** Localized title. */ + public void checkPref(String name, boolean def, Boolc changed){ + list.add(new CheckSetting(name, bundle.get("setting." + name + ".name"), def, changed)); + settings.defaults(name, def); + rebuild(); + } + + 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.put(setting.name, settings.getDefault(setting.name)); + } + rebuild(); + }).margin(14).width(240f).pad(6); + } + + public abstract static class Setting{ + public String name; + public String title; + + public abstract void add(SettingsTable table); + } + + public static class CheckSetting extends Setting{ + boolean def; + Boolc changed; + + CheckSetting(String name, String title, boolean def, Boolc changed){ + this.name = name; + this.title = title; + 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(); + table.add(box).left().padTop(3f); + table.row(); + } + } + + public static class SliderSetting extends Setting{ + int def, min, max, step; + StringProcessor sp; + + SliderSetting(String name, String title, int def, int min, int max, int step, StringProcessor s){ + this.name = name; + this.title = title; + 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 label = new Label(title); + slider.changed(() -> { + settings.put(name, (int)slider.getValue()); + label.setText(title + ": " + sp.get((int)slider.getValue())); + }); + + slider.change(); + + table.table(t -> { + t.left().defaults().left(); + t.add(label).minWidth(label.getPrefWidth() / Scl.scl(1f) + 50); + if(Core.graphics.isPortrait()){ + t.row(); + } + t.add(slider).width(180); + }).left().padTop(3); + + table.row(); + } + } + + } } diff --git a/core/src/mindustry/ui/fragments/BlockInventoryFragment.java b/core/src/mindustry/ui/fragments/BlockInventoryFragment.java index b6b2729e2b..e63acaac4e 100644 --- a/core/src/mindustry/ui/fragments/BlockInventoryFragment.java +++ b/core/src/mindustry/ui/fragments/BlockInventoryFragment.java @@ -17,7 +17,6 @@ import arc.util.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.type.*; -import mindustry.ui.*; import java.util.*; @@ -147,7 +146,7 @@ public class BlockInventoryFragment extends Fragment{ HandCursorListener l = new HandCursorListener(); l.enabled = canPick; - Element image = itemImage(item.icon(Cicon.xlarge), () -> { + Element image = itemImage(item.uiIcon, () -> { if(tile == null || !tile.isValid()){ return ""; } diff --git a/core/src/mindustry/ui/fragments/HintsFragment.java b/core/src/mindustry/ui/fragments/HintsFragment.java index 9a89b21a91..f4f677e720 100644 --- a/core/src/mindustry/ui/fragments/HintsFragment.java +++ b/core/src/mindustry/ui/fragments/HintsFragment.java @@ -156,6 +156,7 @@ public class HintsFragment extends Fragment{ schematicSelect(visibleDesktop, () -> ui.hints.placedBlocks.contains(Blocks.router), () -> 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)), + blockInfo(() -> true, () -> ui.content.isShown()), 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()), diff --git a/core/src/mindustry/ui/fragments/HudFragment.java b/core/src/mindustry/ui/fragments/HudFragment.java index 0884707e9e..704cf0ec61 100644 --- a/core/src/mindustry/ui/fragments/HudFragment.java +++ b/core/src/mindustry/ui/fragments/HudFragment.java @@ -17,6 +17,7 @@ import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.core.GameState.*; +import mindustry.core.*; import mindustry.ctype.*; import mindustry.game.EventType.*; import mindustry.game.*; @@ -30,7 +31,7 @@ import mindustry.ui.*; import static mindustry.Vars.*; public class HudFragment extends Fragment{ - private static final float dsize = 65f; + private static final float dsize = 65f, pauseHeight = 36f; public final PlacementFragment blockfrag = new PlacementFragment(); public boolean shown = true; @@ -60,7 +61,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; @@ -90,7 +91,9 @@ public class HudFragment extends Fragment{ 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.table(Styles.black6, top -> top.label(() -> netServer.isWaitingForPlayers() ? "@waiting.players" : 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 }); //minimap + position @@ -208,10 +211,7 @@ public class HudFragment extends Fragment{ 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 -> { @@ -244,6 +244,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 +258,83 @@ 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(); + + t.name = "coreinfo"; + + t.collapser(v -> v.add().height(pauseHeight), () -> state.isPaused()).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() || !state.teams.get(player.team()).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).padTop(7); }); //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"; @@ -343,7 +359,6 @@ public class HudFragment extends Fragment{ //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); @@ -353,7 +368,7 @@ public class HudFragment extends Fragment{ for(Item item : content.items()){ if(state.rules.sector != null && state.rules.sector.info.getExport(item) >= 1){ - c.image(item.icon(Cicon.small)); + c.image(item.uiIcon); c.label(() -> (int)state.rules.sector.info.getExport(item) + " /s").color(Color.lightGray); c.row(); } @@ -412,6 +427,10 @@ public class HudFragment extends Fragment{ } 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 +443,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 +482,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 +536,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); @@ -758,6 +778,12 @@ public class HudFragment extends Fragment{ table.row(); + return table; + } + + private void addInfoTable(Table table){ + table.left(); + var count = new float[]{-1}; table.table().update(t -> { if(player.unit() instanceof Payloadc payload){ @@ -770,8 +796,29 @@ public class HudFragment extends Fragment{ 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.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); + } + } + }).left(); } private boolean canSkipWave(){ diff --git a/core/src/mindustry/ui/fragments/PlacementFragment.java b/core/src/mindustry/ui/fragments/PlacementFragment.java index cf51957878..b81f6a34f8 100644 --- a/core/src/mindustry/ui/fragments/PlacementFragment.java +++ b/core/src/mindustry/ui/fragments/PlacementFragment.java @@ -98,7 +98,7 @@ public class PlacementFragment extends Fragment{ if(Core.input.keyTap(Binding.pick) && player.isBuilder()){ //mouse eyedropper select var build = world.buildWorld(Core.input.mouseWorld().x, Core.input.mouseWorld().y); - Block tryRecipe = build == null ? null : build instanceof ConstructBuild c ? c.cblock : build.block; + 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()){ @@ -224,7 +224,7 @@ 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){ Core.app.setClipboardText((char)Fonts.getUnicode(block.name) + ""); @@ -235,7 +235,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(); @@ -309,7 +309,7 @@ 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(); @@ -328,7 +328,7 @@ 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(); @@ -349,7 +349,7 @@ public class PlacementFragment extends Fragment{ 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.environment) ? "@unsupported.environment" : "@banned").width(190f).wrap(); b.left(); }).padTop(2).left(); } @@ -471,7 +471,7 @@ public class PlacementFragment extends Fragment{ } //if the tile has a drop, display the drop - if(hoverTile.drop() != null){ + if(hoverTile.drop() != null || hoverTile.wallDrop() != null){ return hoverTile; } } diff --git a/core/src/mindustry/world/Block.java b/core/src/mindustry/world/Block.java index 1d8e1db2a9..8c480ea545 100644 --- a/core/src/mindustry/world/Block.java +++ b/core/src/mindustry/world/Block.java @@ -29,7 +29,6 @@ import mindustry.world.blocks.environment.*; import mindustry.world.blocks.power.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import java.lang.reflect.*; import java.util.*; @@ -45,6 +44,7 @@ public class Block extends UnlockableContent{ public boolean consumesPower = true; public boolean outputsPower = false; public boolean outputsPayload = false; + public boolean acceptsPayload = false; public boolean outputFacing = true; public boolean acceptsItems = false; @@ -77,6 +77,8 @@ public class Block extends UnlockableContent{ public boolean solidifes; /** whether this is rotateable */ public boolean rotate; + /** whether to draw a rotation arrow - this does not apply to lines of blocks */ + public boolean drawArrow = true; /** for static blocks only: if true, tile data() is saved in world data. */ public boolean saveData; /** whether you can break this with rightclick */ @@ -105,6 +107,8 @@ public class Block extends UnlockableContent{ public boolean noUpdateDisabled = false; /** Whether to use this block's color in the minimap. Only used for overlays. */ public boolean useColor = true; + /** item that drops from this block, used for drills */ + public @Nullable Item itemDrop = null; /** tile entity health */ public int health = -1; /** base block explosiveness */ @@ -115,8 +119,12 @@ public class Block extends UnlockableContent{ public int size = 1; /** multiblock offset */ public float offset = 0f; - /** Whether to draw this block in the expanded draw range. */ + /** Deprecated for removal: Use clipSize instead. + * This does nothing, and is kept to preserve compatibility with v6 mods. */ + @Deprecated public boolean expanded = false; + /** Clipping size of this block. Should be as large as the block will draw. */ + public float clipSize = -1f; /** Max of timers used. */ public int timers = 0; /** Cache layer. Only used for 'cached' rendering. */ @@ -144,6 +152,12 @@ public class Block extends UnlockableContent{ public boolean consumesTap; /** Whether to draw the glow of the liquid for this block, if it has one. */ public boolean drawLiquidLight = true; + /** Environmental flags that are *all* required for this block to function. 0 = any environment */ + public int envRequired = 0; + /** The environment flags that this block can function in. If the env matches any of these, it will be enabled. */ + public int envEnabled = Env.terrestrial; + /** The environment flags that this block *cannot* function in. If the env matches any of these, it will be *disabled*. */ + public int envDisabled = 0; /** Whether to periodically sync this block across the network. */ public boolean sync; /** Whether this block uses conveyor-type placement mode. */ @@ -167,7 +181,9 @@ public class Block extends UnlockableContent{ public Color outlineColor = Color.valueOf("404049"); /** Whether any icon region has an outline added. */ public boolean outlineIcon = false; - /** Which of the icon regions gets the outline added. */ + /** Outline icon radius. */ + public int outlineRadius = 4; + /** Which of the icon regions gets the outline added. Uses last icon if <= 0. */ public int outlinedIcon = -1; /** Whether this block has a shadow under it. */ public boolean hasShadow = true; @@ -179,7 +195,7 @@ public class Block extends UnlockableContent{ public Color lightColor = Color.white.cpy(); /** * Whether this environmental block passively emits light. - * Not valid for non-environmental blocks. */ + * Does not change behavior for non-environmental blocks, but still updates clipSize. */ public boolean emitLight = false; /** Radius of the light emitted by this block. */ public float lightRadius = 60f; @@ -198,19 +214,23 @@ public class Block extends UnlockableContent{ public ItemStack[] requirements = {}; /** Category in place menu. */ public Category category = Category.distribution; - /** Cost of building this block; do not modify directly! */ - public float buildCost; + /** Time to build this block in ticks; do not modify directly! */ + public float buildCost = 20f; /** Whether this block is visible and can currently be built. */ public BuildVisibility buildVisibility = BuildVisibility.hidden; /** Multiplier for speed of building this block. */ public float buildCostMultiplier = 1f; /** Build completion at which deconstruction finishes. */ public float deconstructThreshold = 0f; + /** If true, this block deconstructs immediately. Instant deconstruction implies no resource refund. */ + public boolean instantDeconstruct = false; + /** Effect for breaking the block. Passes size as rotation. */ + public Effect breakEffect = Fx.breakBlock; /** Multiplier for cost of research in tech tree. */ public float researchCostMultiplier = 1; /** Whether this block has instant transfer.*/ public boolean instantTransfer = false; - /** Whether you can rotate this block with Keybind rotateplaced + Scroll Wheel. */ + /** Whether you can rotate this block after it is placed. */ public boolean quickRotate = true; /** Main subclass. Non-anonymous. */ public @Nullable Class subclass; @@ -318,7 +338,7 @@ public class Block extends UnlockableContent{ } public TextureRegion getDisplayIcon(Tile tile){ - return tile.build == null ? icon(Cicon.medium) : tile.build.getDisplayIcon(); + return tile.build == null ? uiIcon : tile.build.getDisplayIcon(); } public String getDisplayName(Tile tile){ @@ -363,7 +383,7 @@ public class Block extends UnlockableContent{ if(canBeBuilt()){ stats.add(Stat.buildTime, buildCost / 60, StatUnit.seconds); - stats.add(Stat.buildCost, new ItemListValue(false, requirements)); + stats.add(Stat.buildCost, StatValues.items(false, requirements)); } if(instantTransfer){ @@ -405,7 +425,9 @@ public class Block extends UnlockableContent{ bars.add("items", entity -> new Bar(() -> Core.bundle.format("bar.items", entity.items.total()), () -> Pal.items, () -> (float)entity.items.total() / itemCapacity)); } - if(flags.contains(BlockFlag.unitModifier)) stats.add(Stat.maxUnits, (unitCapModifier < 0 ? "-" : "+") + Math.abs(unitCapModifier)); + if(unitCapModifier != 0){ + stats.add(Stat.maxUnits, (unitCapModifier < 0 ? "-" : "+") + Math.abs(unitCapModifier)); + } } public boolean canReplace(Block other){ @@ -470,7 +492,7 @@ public class Block extends UnlockableContent{ } public TextureRegion getRequestRegion(BuildPlan req, Eachable list){ - return icon(Cicon.full); + return fullIcon; } public void drawRequestConfig(BuildPlan req, Eachable list){ @@ -554,6 +576,11 @@ public class Block extends UnlockableContent{ return editorVariantRegions; } + /** @return special icons to outline and save with an -outline variant. Vanilla only. */ + public TextureRegion[] makeIconRegions(){ + return new TextureRegion[0]; + } + protected TextureRegion[] icons(){ //use team region in vanilla team blocks return teamRegion.found() && minfo.mod == null ? new TextureRegion[]{region, teamRegions[Team.sharded.id]} : new TextureRegion[]{region}; @@ -564,7 +591,7 @@ public class Block extends UnlockableContent{ } public TextureRegion[] variantRegions(){ - return variantRegions == null ? (variantRegions = new TextureRegion[]{icon(Cicon.full)}) : variantRegions; + return variantRegions == null ? (variantRegions = new TextureRegion[]{fullIcon}) : variantRegions; } public boolean hasBuilding(){ @@ -588,7 +615,12 @@ public class Block extends UnlockableContent{ } public boolean isPlaceable(){ - return isVisible() && (!state.rules.bannedBlocks.contains(this) || state.rules.editor); + return isVisible() && (!state.rules.bannedBlocks.contains(this) || state.rules.editor) && supportsEnv(state.rules.environment); + } + + /** @return whether this block supports a specific environment. */ + public boolean supportsEnv(int env){ + return (envEnabled & env) != 0 && (envDisabled & env) == 0 && (envRequired == 0 || (envRequired & env) == envRequired); } /** Called when building of this block begins. */ @@ -737,16 +769,25 @@ public class Block extends UnlockableContent{ health = size * size * 40; } + clipSize = Math.max(clipSize, size * tilesize); + + if(emitLight){ + clipSize = Math.max(clipSize, lightRadius * 2f); + } + if(group == BlockGroup.transportation || consumes.has(ConsumeType.item) || category == Category.distribution){ acceptsItems = true; } offset = ((size + 1) % 2) * tilesize / 2f; - buildCost = 0f; - for(ItemStack stack : requirements){ - buildCost += stack.amount * stack.item.cost; + if(requirements.length > 0){ + buildCost = 0f; + for(ItemStack stack : requirements){ + buildCost += stack.amount * stack.item.cost; + } } + buildCost *= buildCostMultiplier; if(consumes.has(ConsumeType.power)) hasPower = true; @@ -772,9 +813,10 @@ public class Block extends UnlockableContent{ } } - @CallSuper @Override public void load(){ + super.load(); + region = Core.atlas.find(name); ContentRegions.loadRegions(this); @@ -795,62 +837,41 @@ public class Block extends UnlockableContent{ public void createIcons(MultiPacker packer){ super.createIcons(packer); - packer.add(PageType.editor, name + "-icon-editor", Core.atlas.getPixmap((AtlasRegion)icon(Cicon.full))); - if(!synthetic()){ - PixmapRegion image = Core.atlas.getPixmap((AtlasRegion)icon(Cicon.full)); - mapColor.set(image.getPixel(image.width/2, image.height/2)); + PixmapRegion image = Core.atlas.getPixmap(fullIcon); + mapColor.set(image.get(image.width/2, image.height/2)); } - getGeneratedIcons(); - Pixmap last = null; + var gen = icons(); + if(outlineIcon){ - final int radius = 4; - PixmapRegion region = Core.atlas.getPixmap(getGeneratedIcons()[outlinedIcon >= 0 ? outlinedIcon : getGeneratedIcons().length -1]); - Pixmap out = new Pixmap(region.width, region.height); - Color color = new Color(); - for(int x = 0; x < region.width; x++){ - for(int y = 0; y < region.height; y++){ - - region.getPixel(x, y, color); - out.draw(x, y, color); - if(color.a < 1f){ - boolean found = false; - outer: - for(int rx = -radius; rx <= radius; rx++){ - for(int ry = -radius; ry <= radius; ry++){ - if(Structs.inBounds(rx + x, ry + y, region.width, region.height) && Mathf.within(rx, ry, radius) && color.set(region.getPixel(rx + x, ry + y)).a > 0.01f){ - found = true; - break outer; - } - } - } - if(found){ - out.draw(x, y, outlineColor); - } - } - } + PixmapRegion region = Core.atlas.getPixmap(gen[outlinedIcon >= 0 ? outlinedIcon : gen.length -1]); + Pixmap out = last = Pixmaps.outline(region, outlineColor, outlineRadius); + if(Core.settings.getBool("linear")){ + Pixmaps.bleed(out); } - last = out; - packer.add(PageType.main, name, out); } - if(generatedIcons.length > 1){ - Pixmap base = Core.atlas.getPixmap(generatedIcons[0]).crop(); - for(int i = 1; i < generatedIcons.length; i++){ - if(i == generatedIcons.length - 1 && last != null){ - base.drawPixmap(last); + var editorBase = Core.atlas.getPixmap(fullIcon); + + if(gen.length > 1){ + Pixmap base = Core.atlas.getPixmap(gen[0]).crop(); + for(int i = 1; i < gen.length; i++){ + if(i == gen.length - 1 && last != null){ + base.draw(last, 0, 0, true); }else{ - base.draw(Core.atlas.getPixmap(generatedIcons[i])); + base.draw(Core.atlas.getPixmap(gen[i]), true); } } packer.add(PageType.main, "block-" + name + "-full", base); - generatedIcons = null; - Arrays.fill(cicons, null); + + editorBase = new PixmapRegion(base); } + + packer.add(PageType.editor, name + "-icon-editor", editorBase); } } diff --git a/core/src/mindustry/world/Build.java b/core/src/mindustry/world/Build.java index f3eb5a40e4..b873546340 100644 --- a/core/src/mindustry/world/Build.java +++ b/core/src/mindustry/world/Build.java @@ -35,6 +35,13 @@ public class Build{ int rotation = tile.build != null ? tile.build.rotation : 0; Block previous = tile.block(); + + //instantly deconstruct if necessary + if(previous.instantDeconstruct){ + ConstructBlock.deconstructFinish(tile, previous, unit); + return; + } + Block sub = ConstructBlock.get(previous.size); Seq prevBuild = new Seq<>(1); @@ -75,6 +82,14 @@ public class Build{ return; } + //break all props in the way + tile.getLinkedTilesAs(result, out -> { + if(out.block != Blocks.air && out.block.alwaysReplace){ + out.block.breakEffect.at(out.drawx(), out.drawy(), out.block.size, out.block.mapColor); + out.remove(); + } + }); + Block previous = tile.block(); Block sub = ConstructBlock.get(result.size); Seq prevBuild = new Seq<>(9); @@ -159,7 +174,7 @@ public class Build{ //this could be buggy and abuse-able, so I'm not enabling it yet //note that this requires a change in BuilderComp as well //(type == check.block() && check.centerX() == x && check.centerY() == y && check.build != null && check.build.health < check.build.maxHealth - 0.0001f) || - (check.build instanceof ConstructBuild build && build.cblock == type && check.centerX() == tile.x && check.centerY() == tile.y)) && //same type in construction + (check.build instanceof ConstructBuild build && build.current == type && check.centerX() == tile.x && check.centerY() == tile.y)) && //same type in construction type.bounds(tile.x, tile.y, Tmp.r1).grow(0.01f).contains(check.block.bounds(check.centerX(), check.centerY(), Tmp.r2))) || //no replacement (type.requiresWater && check.floor().liquidDrop != Liquids.water) //requires water but none found ) return false; diff --git a/core/src/mindustry/world/Tile.java b/core/src/mindustry/world/Tile.java index 79abb65ea3..b231cd2079 100644 --- a/core/src/mindustry/world/Tile.java +++ b/core/src/mindustry/world/Tile.java @@ -1,5 +1,6 @@ package mindustry.world; +import arc.*; import arc.func.*; import arc.math.*; import arc.math.geom.*; @@ -11,6 +12,7 @@ import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.game.*; +import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.type.*; import mindustry.ui.*; @@ -19,7 +21,9 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; public class Tile implements Position, QuadTreeObject, Displayable{ - static final ObjectSet tileSet = new ObjectSet<>(); + private static final TileChangeEvent tileChange = new TileChangeEvent(); + private static final TilePreChangeEvent preChange = new TilePreChangeEvent(); + private static final ObjectSet tileSet = new ObjectSet<>(); /** Extra data for very specific blocks. */ public byte data; @@ -108,16 +112,18 @@ public class Tile implements Position, QuadTreeObject, Displayable{ * Takes flammability of floor liquid into account. */ public float getFlammability(){ - if(!block.hasItems){ - if(floor.liquidDrop != null && !block.solid){ - return floor.liquidDrop.flammability; - } + if(block == Blocks.air){ + if(floor.liquidDrop != null) return floor.liquidDrop.flammability; return 0; }else if(build != null){ - float result = build.items.sum((item, amount) -> item.flammability * amount); + float result = 0f; + + if(block.hasItems){ + result += build.items.sum((item, amount) -> item.flammability * amount) / block.itemCapacity * Mathf.clamp(block.itemCapacity / 2.4f, 1f, 3f); + } if(block.hasLiquids){ - result += build.liquids.sum((liquid, amount) -> liquid.flammability * amount / 3f); + result += build.liquids.sum((liquid, amount) -> liquid.flammability * amount / 1.6f) / block.liquidCapacity * Mathf.clamp(block.liquidCapacity / 30f, 1f, 2f); } return result; @@ -166,6 +172,7 @@ public class Tile implements Position, QuadTreeObject, Displayable{ return build == null ? Team.derelict : build.team; } + /** Do not call unless you know what you are doing! This does not update the indexer! */ public void setTeam(Team team){ if(build != null){ build.team(team); @@ -197,10 +204,12 @@ public class Tile implements Position, QuadTreeObject, Displayable{ if(type.isStatic() || this.block.isStatic()){ recache(); + recacheWall(); } - this.block = type; preChanged(); + + this.block = type; changeBuild(team, entityprov, (byte)Mathf.mod(rotation, 4)); if(build != null){ @@ -287,10 +296,17 @@ public class Tile implements Position, QuadTreeObject, Displayable{ circle(radius, (x, y) -> cons.get(world.rawTile(x, y))); } + public void recacheWall(){ + if(!headless && !world.isGenerating()){ + renderer.blocks.recacheWall(this); + } + } + public void recache(){ if(!headless && !world.isGenerating()){ renderer.blocks.floor.recacheTile(this); renderer.minimap.update(this); + //update neighbor tiles as well for(int i = 0; i < 8; i++){ Tile other = world.tile(x + Geometry.d8[i].x, y + Geometry.d8[i].y); if(other != null){ @@ -487,6 +503,13 @@ public class Tile implements Position, QuadTreeObject, Displayable{ return overlay == Blocks.air || overlay.itemDrop == null ? floor.itemDrop : overlay.itemDrop; } + public @Nullable Item wallDrop(){ + return block.solid ? + block.itemDrop != null ? block.itemDrop : + overlay instanceof WallOreBlock ? overlay.itemDrop : + null : null; + } + public int staticDarkness(){ return block.solid && block.fillsTile && !block.synthetic() ? data : 0; } @@ -497,6 +520,10 @@ public class Tile implements Position, QuadTreeObject, Displayable{ } protected void preChanged(){ + if(!world.isGenerating()){ + Events.fire(preChange.set(this)); + } + if(build != null){ //only call removed() for the center block - this only gets called once. build.onRemoved(); @@ -514,6 +541,9 @@ public class Tile implements Position, QuadTreeObject, Displayable{ if(other != null){ //reset entity and block *manually* - thus, preChanged() will not be called anywhere else, for multiblocks if(other != this){ //do not remove own entity so it can be processed in changed() + //manually call pre-change event for other tile + Events.fire(preChange.set(other)); + other.build = null; other.block = Blocks.air; @@ -525,12 +555,6 @@ public class Tile implements Position, QuadTreeObject, Displayable{ } } } - - - //recache when static blocks get changed - if(block.isStatic()){ - recache(); - } } protected void changeBuild(Team team, Prov entityprov, int rotation){ @@ -584,12 +608,18 @@ public class Tile implements Position, QuadTreeObject, Displayable{ } protected void fireChanged(){ - world.notifyChanged(this); + if(!world.isGenerating()){ + Events.fire(tileChange.set(this)); + } } @Override public void display(Table table){ - Block toDisplay = overlay.itemDrop != null ? overlay : floor; + + Block toDisplay = + block.itemDrop != null ? block : + overlay.itemDrop != null || wallDrop() != null ? overlay : + floor; table.table(t -> { t.left(); @@ -635,8 +665,7 @@ public class Tile implements Position, QuadTreeObject, Displayable{ @Remote(called = Loc.server) public static void setTeam(Building build, Team team){ if(build != null){ - build.team = team; - indexer.updateIndices(build.tile); + build.changeTeam(team); } } diff --git a/core/src/mindustry/world/blocks/Attributes.java b/core/src/mindustry/world/blocks/Attributes.java index 38a39bd2e3..25dd3b51bb 100644 --- a/core/src/mindustry/world/blocks/Attributes.java +++ b/core/src/mindustry/world/blocks/Attributes.java @@ -7,27 +7,31 @@ import mindustry.world.meta.*; import java.util.*; public class Attributes implements JsonSerializable{ - private final float[] arr = new float[Attribute.all.length]; + private float[] arr = new float[Attribute.all.length]; public void clear(){ Arrays.fill(arr, 0); } public float get(Attribute attr){ - return arr[attr.ordinal()]; + check(); + return arr[attr.id]; } public void set(Attribute attr, float value){ - arr[attr.ordinal()] = value; + check(); + arr[attr.id] = value; } public void add(Attributes other){ + check(); for(int i = 0; i < arr.length; i++){ arr[i] += other.arr[i]; } } public void add(Attributes other, float scl){ + check(); for(int i = 0; i < arr.length; i++){ arr[i] += other.arr[i] * scl; } @@ -35,17 +39,23 @@ public class Attributes implements JsonSerializable{ @Override public void write(Json json){ + check(); for(Attribute at : Attribute.all){ - if(arr[at.ordinal()] != 0){ - json.writeValue(at.name(), arr[at.ordinal()]); + if(arr[at.id] != 0){ + json.writeValue(at.name, arr[at.id]); } } } @Override public void read(Json json, JsonValue data){ + check(); for(Attribute at : Attribute.all){ - arr[at.ordinal()] = data.getFloat(at.name(), 0); + arr[at.id] = data.getFloat(at.name, 0); } } + + private void check(){ + if(arr.length != Attribute.all.length) arr = new float[Attribute.all.length]; + } } diff --git a/core/src/mindustry/world/blocks/ConstructBlock.java b/core/src/mindustry/world/blocks/ConstructBlock.java index 112569dd71..5e31f6df00 100644 --- a/core/src/mindustry/world/blocks/ConstructBlock.java +++ b/core/src/mindustry/world/blocks/ConstructBlock.java @@ -16,8 +16,8 @@ import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.storage.CoreBlock.*; import mindustry.world.modules.*; @@ -54,7 +54,7 @@ public class ConstructBlock extends Block{ @Remote(called = Loc.server) public static void deconstructFinish(Tile tile, Block block, Unit builder){ Team team = tile.team(); - Fx.breakBlock.at(tile.drawx(), tile.drawy(), block.size); + block.breakEffect.at(tile.drawx(), tile.drawy(), block.size, block.mapColor); Events.fire(new BlockBuildEndEvent(tile, builder, team, true, null)); tile.remove(); if(shouldPlay()) Sounds.breaks.at(tile, calcPitch(false)); @@ -133,43 +133,36 @@ public class ConstructBlock extends Block{ } public class ConstructBuild extends Building{ - /** - * The recipe of the block that is being constructed. - * If there is no recipe for this block, as is the case with rocks, 'previous' is used. - */ - public @Nullable Block cblock; + /** The recipe of the block that is being (de)constructed. Never null. */ + public Block current = Blocks.air; + /** The block that used to be here. Never null. */ + public Block previous = Blocks.air; + /** Buildings that previously occupied this location. */ public @Nullable Seq prevBuild; public float progress = 0; public float buildCost; - /** - * The block that used to be here. - * If a non-recipe block is being deconstructed, this is the block that is being deconstructed. - */ - public Block previous; public @Nullable Object lastConfig; + public @Nullable Unit lastBuilder; public boolean wasConstructing, activeDeconstruct; public float constructColor; - @Nullable - public Unit lastBuilder; - private float[] accumulator; private float[] totalAccumulator; @Override public String getDisplayName(){ - return Core.bundle.format("block.constructing", cblock == null ? previous.localizedName : cblock.localizedName); + return Core.bundle.format("block.constructing", current.localizedName); } @Override public TextureRegion getDisplayIcon(){ - return (cblock == null ? previous : cblock).icon(Cicon.full); + return current.fullIcon; } @Override public boolean checkSolid(){ - return (cblock != null && cblock.solid) || previous == null || previous.solid; + return current.solid || previous.solid; } @Override @@ -180,14 +173,20 @@ public class ConstructBlock extends Block{ @Override public void tapped(){ //if the target is constructable, begin constructing - if(cblock != null){ + if(current.isPlaceable()){ if(control.input.buildWasAutoPaused && !control.input.isBuilding && player.isBuilder()){ control.input.isBuilding = true; } - player.unit().addBuild(new BuildPlan(tile.x, tile.y, rotation, cblock, lastConfig), false); + player.unit().addBuild(new BuildPlan(tile.x, tile.y, rotation, current, lastConfig), false); } } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(progress); + return super.sense(sensor); + } + @Override public void onDestroyed(){ Fx.blockExplosionSmoke.at(tile); @@ -199,29 +198,33 @@ public class ConstructBlock extends Block{ @Override public void updateTile(){ + //auto-remove air blocks + if(current == Blocks.air){ + remove(); + } + constructColor = Mathf.lerpDelta(constructColor, activeDeconstruct ? 1f : 0f, 0.2f); activeDeconstruct = false; } @Override public void draw(){ - if(!(previous == null || cblock == null || previous == cblock) && Core.atlas.isFound(previous.icon(Cicon.full))){ - Draw.rect(previous.icon(Cicon.full), x, y, previous.rotate ? rotdeg() : 0); + //do not draw air + if(current == Blocks.air) return; + + if(previous != current && previous != Blocks.air && previous.fullIcon.found()){ + Draw.rect(previous.fullIcon, x, y, previous.rotate ? rotdeg() : 0); } Draw.draw(Layer.blockBuilding, () -> { Draw.color(Pal.accent, Pal.remove, constructColor); - Block target = cblock == null ? previous : cblock; + for(TextureRegion region : current.getGeneratedIcons()){ + Shaders.blockbuild.region = region; + Shaders.blockbuild.progress = progress; - if(target != null){ - for(TextureRegion region : target.getGeneratedIcons()){ - Shaders.blockbuild.region = region; - Shaders.blockbuild.progress = progress; - - Draw.rect(region, x, y, target.rotate ? rotdeg() : 0); - Draw.flush(); - } + Draw.rect(region, x, y, current.rotate ? rotdeg() : 0); + Draw.flush(); } Draw.color(); @@ -231,10 +234,6 @@ public class ConstructBlock extends Block{ public void construct(Unit builder, @Nullable Building core, float amount, Object config){ wasConstructing = true; activeDeconstruct = false; - if(cblock == null){ - kill(); - return; - } if(builder.isPlayer()){ lastBuilder = builder; @@ -242,14 +241,14 @@ public class ConstructBlock extends Block{ lastConfig = config; - if(cblock.requirements.length != accumulator.length || totalAccumulator.length != cblock.requirements.length){ - setConstruct(previous, cblock); + if(current.requirements.length != accumulator.length || totalAccumulator.length != current.requirements.length){ + setConstruct(previous, current); } float maxProgress = core == null || team.rules().infiniteResources ? amount : checkRequired(core.items, amount, false); - for(int i = 0; i < cblock.requirements.length; i++){ - int reqamount = Math.round(state.rules.buildCostMultiplier * cblock.requirements[i].amount); + for(int i = 0; i < current.requirements.length; i++){ + int reqamount = Math.round(state.rules.buildCostMultiplier * current.requirements[i].amount); accumulator[i] += Math.min(reqamount * maxProgress, reqamount - totalAccumulator[i] + 0.00001f); //add min amount progressed to the accumulator totalAccumulator[i] = Math.min(totalAccumulator[i] + reqamount * maxProgress, reqamount); } @@ -260,16 +259,17 @@ public class ConstructBlock extends Block{ if(progress >= 1f || state.rules.infiniteResources){ if(lastBuilder == null) lastBuilder = builder; - constructed(tile, cblock, lastBuilder, (byte)rotation, builder.team, config); + constructed(tile, current, lastBuilder, (byte)rotation, builder.team, config); } } - public void deconstruct(Unit builder, @Nullable Building core, float amount){ + public void deconstruct(Unit builder, @Nullable CoreBuild core, float amount){ //reset accumulated resources when switching modes if(wasConstructing){ Arrays.fill(accumulator, 0); Arrays.fill(totalAccumulator, 0); } + wasConstructing = false; activeDeconstruct = true; float deconstructMultiplier = state.rules.deconstructRefundMultiplier; @@ -278,55 +278,53 @@ public class ConstructBlock extends Block{ lastBuilder = builder; } - if(cblock != null){ - ItemStack[] requirements = cblock.requirements; - if(requirements.length != accumulator.length || totalAccumulator.length != requirements.length){ - setDeconstruct(cblock); - } + ItemStack[] requirements = current.requirements; + if(requirements.length != accumulator.length || totalAccumulator.length != requirements.length){ + setDeconstruct(current); + } - //make sure you take into account that you can't deconstruct more than there is deconstructed - float clampedAmount = Math.min(amount, progress); + //make sure you take into account that you can't deconstruct more than there is deconstructed + float clampedAmount = Math.min(amount, progress); - for(int i = 0; i < requirements.length; i++){ - int reqamount = Math.round(state.rules.buildCostMultiplier * requirements[i].amount); - accumulator[i] += Math.min(clampedAmount * deconstructMultiplier * reqamount, deconstructMultiplier * reqamount - totalAccumulator[i]); //add scaled amount progressed to the accumulator - totalAccumulator[i] = Math.min(totalAccumulator[i] + reqamount * clampedAmount * deconstructMultiplier, reqamount); + for(int i = 0; i < requirements.length; i++){ + int reqamount = Math.round(state.rules.buildCostMultiplier * requirements[i].amount); + accumulator[i] += Math.min(clampedAmount * deconstructMultiplier * reqamount, deconstructMultiplier * reqamount - totalAccumulator[i]); //add scaled amount progressed to the accumulator + totalAccumulator[i] = Math.min(totalAccumulator[i] + reqamount * clampedAmount * deconstructMultiplier, reqamount); - int accumulated = (int)(accumulator[i]); //get amount + int accumulated = (int)(accumulator[i]); //get amount - if(clampedAmount > 0 && accumulated > 0){ //if it's positive, add it to the core - if(core != null && requirements[i].item.unlockedNow()){ //only accept items that are unlocked - int accepting = Math.min(accumulated, ((CoreBuild)core).storageCapacity - core.items.get(requirements[i].item)); - //transfer items directly, as this is not production. - core.items.add(requirements[i].item, accepting); - accumulator[i] -= accepting; - }else{ - accumulator[i] -= accumulated; - } + if(clampedAmount > 0 && accumulated > 0){ //if it's positive, add it to the core + if(core != null && requirements[i].item.unlockedNow()){ //only accept items that are unlocked + int accepting = Math.min(accumulated, core.storageCapacity - core.items.get(requirements[i].item)); + //transfer items directly, as this is not production. + core.items.add(requirements[i].item, accepting); + accumulator[i] -= accepting; + }else{ + accumulator[i] -= accumulated; } } } progress = Mathf.clamp(progress - amount); - if(progress <= (previous == null ? 0 : previous.deconstructThreshold) || state.rules.infiniteResources){ + if(progress <= current.deconstructThreshold || state.rules.infiniteResources){ if(lastBuilder == null) lastBuilder = builder; - Call.deconstructFinish(tile, this.cblock == null ? previous : this.cblock, lastBuilder); + Call.deconstructFinish(tile, this.current, lastBuilder); } } private float checkRequired(ItemModule inventory, float amount, boolean remove){ float maxProgress = amount; - for(int i = 0; i < cblock.requirements.length; i++){ - int sclamount = Math.round(state.rules.buildCostMultiplier * cblock.requirements[i].amount); + for(int i = 0; i < current.requirements.length; i++){ + int sclamount = Math.round(state.rules.buildCostMultiplier * current.requirements[i].amount); int required = (int)(accumulator[i]); //calculate items that are required now - if(inventory.get(cblock.requirements[i].item) == 0 && sclamount != 0){ + if(inventory.get(current.requirements[i].item) == 0 && sclamount != 0){ maxProgress = 0f; }else if(required > 0){ //if this amount is positive... //calculate how many items it can actually use - int maxUse = Math.min(required, inventory.get(cblock.requirements[i].item)); + int maxUse = Math.min(required, inventory.get(current.requirements[i].item)); //get this as a fraction float fraction = maxUse / (float)required; @@ -337,7 +335,7 @@ public class ConstructBlock extends Block{ //remove stuff that is actually used if(remove){ - inventory.remove(cblock.requirements[i].item, maxUse); + inventory.remove(current.requirements[i].item, maxUse); } } //else, no items are required yet, so just keep going @@ -351,13 +349,15 @@ public class ConstructBlock extends Block{ } public void setConstruct(Block previous, Block block){ + if(block == null) return; + this.constructColor = 0f; this.wasConstructing = true; - this.cblock = block; + this.current = block; this.previous = previous; + this.buildCost = block.buildCost * state.rules.buildCostMultiplier; this.accumulator = new float[block.requirements.length]; this.totalAccumulator = new float[block.requirements.length]; - this.buildCost = block.buildCost * state.rules.buildCostMultiplier; } public void setDeconstruct(Block previous){ @@ -367,12 +367,8 @@ public class ConstructBlock extends Block{ this.wasConstructing = false; this.previous = previous; this.progress = 1f; - if(previous.buildCost >= 0.01f){ - this.cblock = previous; - this.buildCost = previous.buildCost * state.rules.buildCostMultiplier; - }else{ - this.buildCost = 20f; //default no-requirement build cost is 20 - } + this.current = previous; + this.buildCost = previous.buildCost * state.rules.buildCostMultiplier; this.accumulator = new float[previous.requirements.length]; this.totalAccumulator = new float[previous.requirements.length]; } @@ -381,8 +377,8 @@ public class ConstructBlock extends Block{ public void write(Writes write){ super.write(write); write.f(progress); - write.s(previous == null ? -1 : previous.id); - write.s(cblock == null ? -1 : cblock.id); + write.s(previous.id); + write.s(current.id); if(accumulator == null){ write.b(-1); @@ -413,13 +409,12 @@ public class ConstructBlock extends Block{ } if(pid != -1) previous = content.block(pid); - if(rid != -1) cblock = content.block(rid); + if(rid != -1) current = content.block(rid); - if(cblock != null){ - buildCost = cblock.buildCost * state.rules.buildCostMultiplier; - }else{ - buildCost = 20f; - } + if(previous == null) previous = Blocks.air; + if(current == null) current = Blocks.air; + + buildCost = current.buildCost * state.rules.buildCostMultiplier; } } } diff --git a/core/src/mindustry/world/blocks/ItemSelection.java b/core/src/mindustry/world/blocks/ItemSelection.java index f2c0b73084..523fb01325 100644 --- a/core/src/mindustry/world/blocks/ItemSelection.java +++ b/core/src/mindustry/world/blocks/ItemSelection.java @@ -34,7 +34,7 @@ public class ItemSelection{ if(closeSelect) control.input.frag.config.hideConfig(); }).group(group).get(); button.changed(() -> consumer.get(button.isChecked() ? item : null)); - button.getStyle().imageUp = new TextureRegionDrawable(item.icon(Cicon.small)); + button.getStyle().imageUp = new TextureRegionDrawable(item.uiIcon); button.update(() -> button.setChecked(holder.get() == item)); if(i++ % 4 == 3){ diff --git a/core/src/mindustry/world/blocks/campaign/Accelerator.java b/core/src/mindustry/world/blocks/campaign/Accelerator.java index 7160c6291a..6ca4ee3f0a 100644 --- a/core/src/mindustry/world/blocks/campaign/Accelerator.java +++ b/core/src/mindustry/world/blocks/campaign/Accelerator.java @@ -109,7 +109,10 @@ public class Accelerator extends Block{ if(!state.isCampaign() || !consValid()) return; - ui.showInfo("@indev.campaign"); + ui.planet.showPlanetLaunch(state.rules.sector, sector -> { + //TODO cutscene, etc... + consume(); + }); Events.fire(Trigger.acceleratorUse); } diff --git a/core/src/mindustry/world/blocks/campaign/LaunchPad.java b/core/src/mindustry/world/blocks/campaign/LaunchPad.java index f8f3ce53b0..03f7343044 100644 --- a/core/src/mindustry/world/blocks/campaign/LaunchPad.java +++ b/core/src/mindustry/world/blocks/campaign/LaunchPad.java @@ -1,9 +1,9 @@ package mindustry.world.blocks.campaign; import arc.*; -import arc.audio.*; import arc.Graphics.*; import arc.Graphics.Cursor.*; +import arc.audio.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; @@ -11,12 +11,14 @@ import arc.math.geom.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; +import arc.util.io.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.entities.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.*; @@ -26,9 +28,8 @@ import mindustry.world.meta.*; import static mindustry.Vars.*; public class LaunchPad extends Block{ - public final int timerLaunch = timers++; /** Time inbetween launches. */ - public float launchTime; + public float launchTime = 1f; public Sound launchSound = Sounds.none; public @Load("@-light") TextureRegion lightRegion; @@ -42,6 +43,7 @@ public class LaunchPad extends Block{ update = true; configurable = true; drawDisabled = false; + flags = EnumSet.of(BlockFlag.launchPad); } @Override @@ -64,6 +66,7 @@ public class LaunchPad extends Block{ } public class LaunchPadBuild extends Building{ + public float launchCounter; @Override public Cursor getCursor(){ @@ -81,6 +84,12 @@ public class LaunchPad extends Block{ return true; } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(launchCounter / launchTime); + return super.sense(sensor); + } + @Override public void draw(){ super.draw(); @@ -89,7 +98,7 @@ public class LaunchPad extends Block{ if(lightRegion.found()){ Draw.color(lightColor); - float progress = Math.min((float)items.total() / itemCapacity, timer.getTime(timerLaunch) / (launchTime / timeScale)); + float progress = Math.min((float)items.total() / itemCapacity, launchCounter / launchTime); int steps = 3; float step = 1f; @@ -106,7 +115,7 @@ public class LaunchPad extends Block{ Draw.reset(); } - float cooldown = Mathf.clamp(timer.getTime(timerLaunch) / (90f / timeScale)); + float cooldown = Mathf.clamp(launchCounter / (90f)); Draw.mixcol(lightColor, 1f - cooldown); @@ -125,7 +134,7 @@ public class LaunchPad extends Block{ if(!state.isCampaign()) return; //launch when full and base conditions are met - if(items.total() >= itemCapacity && efficiency() >= 1f && timer(timerLaunch, launchTime / timeScale)){ + if(items.total() >= itemCapacity && efficiency() >= 1f && (launchCounter += edelta()) >= launchTime){ launchSound.at(x, y); LaunchPayload entity = LaunchPayload.create(); items.each((item, amount) -> entity.stacks.add(new ItemStack(item, amount))); @@ -136,6 +145,7 @@ public class LaunchPad extends Block{ Fx.launchPod.at(this); items.clear(); Effect.shake(3f, 3f, this); + launchCounter = 0f; } } @@ -171,6 +181,25 @@ public class LaunchPad extends Block{ deselect(); }).size(40f); } + + @Override + public byte version(){ + return 1; + } + + @Override + public void write(Writes write){ + super.write(write); + write.f(launchCounter); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + if(revision >= 1){ + launchCounter = read.f(); + } + } } @EntityDef(LaunchPayloadc.class) diff --git a/core/src/mindustry/world/blocks/campaign/PayloadLaunchPad.java b/core/src/mindustry/world/blocks/campaign/PayloadLaunchPad.java new file mode 100644 index 0000000000..e5acad5bf7 --- /dev/null +++ b/core/src/mindustry/world/blocks/campaign/PayloadLaunchPad.java @@ -0,0 +1,280 @@ +package mindustry.world.blocks.campaign; + +import arc.*; +import arc.Graphics.*; +import arc.Graphics.Cursor.*; +import arc.audio.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.scene.ui.layout.*; +import arc.struct.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.logic.*; +import mindustry.type.*; +import mindustry.ui.*; +import mindustry.world.blocks.payloads.*; +import mindustry.world.consumers.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +//TODO very much unfinished +public class PayloadLaunchPad extends PayloadBlock{ + /** Time between launches. */ + public float launchTime; + public Sound launchSound = Sounds.none; + + public @Load("@-light") TextureRegion lightRegion; + public @Load(value = "@-pod", fallback = "launchpod") TextureRegion podRegion; + public Color lightColor = Color.valueOf("eab678"); + + public PayloadLaunchPad(String name){ + super(name); + solid = true; + update = true; + configurable = true; + drawDisabled = false; + flags = EnumSet.of(BlockFlag.launchPad); + acceptsPayload = true; + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.launchTime, launchTime / 60f, StatUnit.seconds); + } + + public class PayloadLaunchPadBuild extends PayloadBlockBuild{ + public float launchCounter; + + @Override + public Cursor getCursor(){ + return !state.isCampaign() || net.client() ? SystemCursor.arrow : super.getCursor(); + } + + //cannot be disabled + @Override + public float efficiency(){ + return power != null && (block.consumes.has(ConsumeType.power) && !block.consumes.getPower().buffered) ? power.status : 1f; + } + + @Override + public boolean shouldConsume(){ + return true; + } + + @Override + public void draw(){ + super.draw(); + + if(!state.isCampaign()) return; + + if(lightRegion.found()){ + Draw.color(lightColor); + float progress = launchCounter / launchTime; + int steps = 3; + float step = 1f; + + for(int i = 0; i < 4; i++){ + for(int j = 0; j < steps; j++){ + float alpha = Mathf.curve(progress, (float)j / steps, (j+1f) / steps); + float offset = -(j - 1f) * step; + + Draw.color(Pal.metalGrayDark, lightColor, alpha); + Draw.rect(lightRegion, x + Geometry.d8edge(i).x * offset, y + Geometry.d8edge(i).y * offset, i * 90); + } + } + + Draw.reset(); + } + + drawPayload(); + + float cooldown = Mathf.clamp(launchCounter / (90f)); + + Draw.mixcol(lightColor, 1f - cooldown); + + Draw.rect(podRegion, x, y); + + Draw.reset(); + } + + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(launchCounter / launchTime); + return super.sense(sensor); + } + + @Override + public void updateTile(){ + if(!state.isCampaign()) return; + + //launch when full and base conditions are met + if(payload != null && moveInPayload() && efficiency() >= 1f && (launchCounter += edelta()) >= launchTime){ + launchSound.at(x, y); + LargeLaunchPayload entity = LargeLaunchPayload.create(); + entity.payload = payload; + entity.set(this); + entity.lifetime(120f); + entity.team(team); + entity.add(); + Fx.launchPod.at(this); + payload = null; + Effect.shake(3f, 3f, this); + launchCounter = 0f; + } + } + + @Override + public void display(Table table){ + super.display(table); + + if(!state.isCampaign()) return; + + table.row(); + table.label(() -> { + Sector dest = state.rules.sector == null ? null : state.rules.sector.info.getRealDestination(); + + return Core.bundle.format("launch.destination", + dest == null ? Core.bundle.get("sectors.nonelaunch") : + "[accent]" + dest.name()); + }).pad(4).wrap().width(200f).left(); + } + + @Override + public void buildConfiguration(Table table){ + if(!state.isCampaign() || net.client()){ + deselect(); + return; + } + + table.button(Icon.upOpen, Styles.clearTransi, () -> { + ui.planet.showSelect(state.rules.sector, other -> { + if(state.isCampaign()){ + state.rules.sector.info.destination = other; + } + }); + deselect(); + }).size(40f); + } + + @Override + public byte version(){ + return 1; + } + + @Override + public void write(Writes write){ + super.write(write); + write.f(launchCounter); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + if(revision >= 1){ + launchCounter = read.f(); + } + } + } + + @EntityDef(LargeLaunchPayloadc.class) + @Component(base = true) + static abstract class LargeLaunchPayloadComp implements Drawc, Timedc, Teamc{ + @Import float x,y; + + Payload payload; + transient Interval in = new Interval(); + + @Override + public void draw(){ + float alpha = fout(Interp.pow5Out); + float scale = (1f - alpha) * 1.3f + 1f; + float cx = cx(), cy = cy(); + float rotation = fin() * (130f + Mathf.randomSeedRange(id(), 50f)); + + Draw.z(Layer.effect + 0.001f); + + Draw.color(Pal.engine); + + float rad = 0.2f + fslope(); + + Fill.light(cx, cy, 10, 25f * (rad + scale-1f), Tmp.c2.set(Pal.engine).a(alpha), Tmp.c1.set(Pal.engine).a(0f)); + + Draw.alpha(alpha); + for(int i = 0; i < 4; i++){ + Drawf.tri(cx, cy, 6f, 40f * (rad + scale-1f), i * 90f + rotation); + } + + Draw.color(); + + Draw.z(Layer.weather - 1); + + TextureRegion region = blockOn() instanceof mindustry.world.blocks.campaign.PayloadLaunchPad p ? p.podRegion : Core.atlas.find("launchpod"); + float rw = region.width * Draw.scl * scale, rh = region.height * Draw.scl * scale; + + Draw.alpha(alpha); + Draw.rect(region, cx, cy, rw, rh, rotation); + + Tmp.v1.trns(225f, fin(Interp.pow3In) * 250f); + + Draw.z(Layer.flyingUnit + 1); + Draw.color(0, 0, 0, 0.22f * alpha); + Draw.rect(region, cx + Tmp.v1.x, cy + Tmp.v1.y, rw, rh, rotation); + + Draw.reset(); + } + + float cx(){ + return x + fin(Interp.pow2In) * (12f + Mathf.randomSeedRange(id() + 3, 4f)); + } + + float cy(){ + return y + fin(Interp.pow5In) * (100f + Mathf.randomSeedRange(id() + 2, 30f)); + } + + @Override + public void update(){ + float r = 3f; + if(in.get(4f - fin()*2f)){ + Fx.rocketSmoke.at(cx() + Mathf.range(r), cy() + Mathf.range(r), fin()); + } + } + + @Override + public void remove(){ + if(!state.isCampaign()) return; + + Sector destsec = state.rules.sector.info.getRealDestination(); + + //actually launch the items upon removal + if(team() == state.rules.defaultTeam){ + if(destsec != null && (destsec != state.rules.sector || net.client())){ + /* + ItemSeq dest = new ItemSeq(); + + for(ItemStack stack : stacks){ + dest.add(stack); + + //update export + state.rules.sector.info.handleItemExport(stack); + Events.fire(new LaunchItemEvent(stack)); + } + + if(!net.client()){ + destsec.addItems(dest); + }*/ + } + } + } + } +} diff --git a/core/src/mindustry/world/blocks/defense/ForceProjector.java b/core/src/mindustry/world/blocks/defense/ForceProjector.java index 8a11429eec..5c464f59c9 100644 --- a/core/src/mindustry/world/blocks/defense/ForceProjector.java +++ b/core/src/mindustry/world/blocks/defense/ForceProjector.java @@ -31,7 +31,6 @@ public class ForceProjector extends Block{ public float cooldownNormal = 1.75f; public float cooldownLiquid = 1.5f; public float cooldownBrokenBase = 0.35f; - public float basePowerDraw = 0.2f; public @Load("@-top") TextureRegion topRegion; static ForceBuild paramEntity; @@ -40,7 +39,7 @@ public class ForceProjector extends Block{ trait.absorb(); Fx.absorb.at(trait); paramEntity.hit = 1f; - paramEntity.buildup += trait.damage() * paramEntity.warmup; + paramEntity.buildup += trait.damage(); } }; @@ -57,6 +56,12 @@ public class ForceProjector extends Block{ consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, 0.1f)).boost().update(false); } + @Override + public void init(){ + clipSize = Math.max(clipSize, (radius + phaseRadiusBoost + 3f) * 2f); + super.init(); + } + @Override public void setBars(){ super.setBars(); @@ -70,10 +75,10 @@ public class ForceProjector extends Block{ @Override public void setStats(){ + stats.timePeriod = phaseUseTime; super.setStats(); stats.add(Stat.shieldHealth, shieldHealth, StatUnit.none); stats.add(Stat.cooldownTime, (int) (shieldHealth / cooldownBrokenBase / 60f), StatUnit.seconds); - stats.add(Stat.powerUse, basePowerDraw * 60f, StatUnit.powerSecond); stats.add(Stat.boostEffect, phaseRadiusBoost / tilesize, StatUnit.blocks); stats.add(Stat.boostEffect, phaseShieldBoost, StatUnit.shieldHealth); } @@ -94,22 +99,12 @@ public class ForceProjector extends Block{ public class ForceBuild extends Building implements Ranged{ public boolean broken = true; public float buildup, radscl, hit, warmup, phaseHeat; - public ForceDraw drawer; @Override public float range(){ return realRadius(); } - @Override - public void created(){ - super.created(); - drawer = ForceDraw.create(); - drawer.build = this; - drawer.set(x, y); - drawer.add(); - } - @Override public boolean shouldAmbientSound(){ return !broken && realRadius() > 1f; @@ -120,7 +115,6 @@ public class ForceProjector extends Block{ float radius = realRadius(); if(!broken && radius > 1f) Fx.forceShrink.at(x, y, radius, team.color); super.onRemoved(); - drawer.remove(); } @Override @@ -188,10 +182,6 @@ public class ForceProjector extends Block{ public void draw(){ super.draw(); - if(drawer != null){ - drawer.set(x, y); - } - if(buildup > 0f){ Draw.alpha(buildup / shieldHealth * 0.75f); Draw.blend(Blending.additive); @@ -199,6 +189,8 @@ public class ForceProjector extends Block{ Draw.blend(); Draw.reset(); } + + drawShield(); } public void drawShield(){ @@ -244,21 +236,4 @@ public class ForceProjector extends Block{ phaseHeat = read.f(); } } - - @EntityDef(value = {ForceDrawc.class}, serialize = false) - @Component(base = true) - abstract class ForceDrawComp implements Drawc{ - transient ForceBuild build; - - @Override - public void draw(){ - build.drawShield(); - } - - @Replace - @Override - public float clipSize(){ - return build.realRadius() * 3f; - } - } } diff --git a/core/src/mindustry/world/blocks/defense/MendProjector.java b/core/src/mindustry/world/blocks/defense/MendProjector.java index 6fb362f7d3..6fbf281918 100644 --- a/core/src/mindustry/world/blocks/defense/MendProjector.java +++ b/core/src/mindustry/world/blocks/defense/MendProjector.java @@ -43,6 +43,7 @@ public class MendProjector extends Block{ @Override public void setStats(){ + stats.timePeriod = useTime; super.setStats(); stats.add(Stat.repairTime, (int)(100f / healPercent * reload / 60f), StatUnit.seconds); @@ -90,11 +91,17 @@ public class MendProjector extends Block{ indexer.eachBlock(this, realRange, other -> other.damaged(), other -> { other.heal(other.maxHealth() * (healPercent + phaseHeat * phaseBoost) / 100f * efficiency()); - Fx.healBlockFull.at(other.x, other.y, other.block.size, Tmp.c1.set(baseColor).lerp(phaseColor, phaseHeat)); + Fx.healBlockFull.at(other.x, other.y, other.block.size, baseColor); }); } } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(charge / reload); + return super.sense(sensor); + } + @Override public void drawSelect(){ float realRange = range + phaseHeat * phaseRangeBoost; diff --git a/core/src/mindustry/world/blocks/defense/OverdriveProjector.java b/core/src/mindustry/world/blocks/defense/OverdriveProjector.java index 499165f1bc..d11ef77db5 100644 --- a/core/src/mindustry/world/blocks/defense/OverdriveProjector.java +++ b/core/src/mindustry/world/blocks/defense/OverdriveProjector.java @@ -57,6 +57,7 @@ public class OverdriveProjector extends Block{ @Override public void setStats(){ + stats.timePeriod = useTime; super.setStats(); stats.add(Stat.speedIncrease, (int)(100f * speedBoost), StatUnit.percent); diff --git a/core/src/mindustry/world/blocks/defense/turrets/ItemTurret.java b/core/src/mindustry/world/blocks/defense/turrets/ItemTurret.java index 38237b8e40..58d0aed084 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/ItemTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/ItemTurret.java @@ -14,7 +14,6 @@ import mindustry.type.*; import mindustry.ui.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -28,7 +27,21 @@ public class ItemTurret extends Turret{ /** Initializes accepted ammo map. Format: [item1, bullet1, item2, bullet2...] */ public void ammo(Object... objects){ - ammoTypes = OrderedMap.of(objects); + ammoTypes = ObjectMap.of(objects); + } + + /** Makes copies of all bullets and limits their range. */ + public void limitRange(){ + limitRange(1f); + } + + /** Makes copies of all bullets and limits their range. */ + public void limitRange(float margin){ + for(var entry : ammoTypes.copy().entries()){ + var copy = entry.value.copy(); + copy.lifetime = (range + margin) / copy.speed; + ammoTypes.put(entry.key, copy); + } } @Override @@ -36,7 +49,7 @@ public class ItemTurret extends Turret{ super.setStats(); stats.remove(Stat.itemCapacity); - stats.add(Stat.ammo, new AmmoListValue<>(ammoTypes)); + stats.add(Stat.ammo, StatValues.ammo(ammoTypes)); } @Override @@ -45,7 +58,7 @@ public class ItemTurret extends Turret{ @Override public void build(Building tile, Table table){ MultiReqImage image = new MultiReqImage(); - content.items().each(i -> filter.get(i) && i.unlockedNow(), item -> image.add(new ReqImage(new ItemImage(item.icon(Cicon.medium)), + content.items().each(i -> filter.get(i) && i.unlockedNow(), item -> image.add(new ReqImage(new ItemImage(item.uiIcon), () -> tile instanceof ItemTurretBuild it && !it.ammo.isEmpty() && ((ItemEntry)it.ammo.peek()).item == item))); table.add(image).size(8 * 4); diff --git a/core/src/mindustry/world/blocks/defense/turrets/LaserTurret.java b/core/src/mindustry/world/blocks/defense/turrets/LaserTurret.java index 22220e5644..a708b39bc7 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/LaserTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/LaserTurret.java @@ -4,10 +4,10 @@ import arc.math.*; import arc.util.*; import mindustry.entities.bullet.*; import mindustry.gen.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -34,7 +34,7 @@ public class LaserTurret extends PowerTurret{ super.setStats(); stats.remove(Stat.booster); - stats.add(Stat.input, new BoosterListValue(reloadTime, consumes.get(ConsumeType.liquid).amount, coolantMultiplier, false, l -> consumes.liquidfilters.get(l.id))); + stats.add(Stat.input, StatValues.boosters(reloadTime, consumes.get(ConsumeType.liquid).amount, coolantMultiplier, false, l -> consumes.liquidfilters.get(l.id))); } public class LaserTurretBuild extends PowerTurretBuild{ @@ -67,8 +67,8 @@ public class LaserTurret extends PowerTurret{ Liquid liquid = liquids.current(); float maxUsed = consumes.get(ConsumeType.liquid).amount; - float used = (cheating() ? maxUsed * Time.delta : Math.min(liquids.get(liquid), maxUsed * Time.delta)) * liquid.heatCapacity * coolantMultiplier; - reload -= used; + float used = (cheating() ? maxUsed * Time.delta : Math.min(liquids.get(liquid), maxUsed * Time.delta)); + reload -= used * liquid.heatCapacity * coolantMultiplier; liquids.remove(liquid, used); if(Mathf.chance(0.06 * used)){ @@ -77,6 +77,13 @@ public class LaserTurret extends PowerTurret{ } } + @Override + public double sense(LAccess sensor){ + //reload reversed for laser turrets + if(sensor == LAccess.progress) return Mathf.clamp(1f - reload / reloadTime); + return super.sense(sensor); + } + @Override protected void updateShooting(){ if(bulletLife > 0 && bullet != null){ diff --git a/core/src/mindustry/world/blocks/defense/turrets/LiquidTurret.java b/core/src/mindustry/world/blocks/defense/turrets/LiquidTurret.java index bb53932b4c..46f94bd0ee 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/LiquidTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/LiquidTurret.java @@ -3,6 +3,7 @@ package mindustry.world.blocks.defense.turrets; import arc.graphics.g2d.*; import arc.struct.*; import mindustry.annotations.Annotations.*; +import mindustry.content.*; import mindustry.entities.*; import mindustry.entities.bullet.*; import mindustry.gen.*; @@ -11,7 +12,6 @@ import mindustry.type.*; import mindustry.world.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -32,14 +32,14 @@ public class LiquidTurret extends Turret{ /** Initializes accepted ammo map. Format: [liquid1, bullet1, liquid2, bullet2...] */ public void ammo(Object... objects){ - ammoTypes = OrderedMap.of(objects); + ammoTypes = ObjectMap.of(objects); } @Override public void setStats(){ super.setStats(); - stats.add(Stat.ammo, new AmmoListValue<>(ammoTypes)); + stats.add(Stat.ammo, StatValues.ammo(ammoTypes)); } @Override @@ -88,7 +88,9 @@ public class LiquidTurret extends Turret{ @Override public void updateTile(){ - unit.ammo(unit.type().ammoCapacity * liquids.currentAmount() / liquidCapacity); + if(unit != null){ + unit.ammo(unit.type().ammoCapacity * liquids.currentAmount() / liquidCapacity); + } super.updateTile(); } @@ -116,8 +118,11 @@ public class LiquidTurret extends Turret{ protected void effects(){ BulletType type = peekAmmo(); - type.shootEffect.at(x + tr.x, y + tr.y, rotation, liquids.current().color); - type.smokeEffect.at(x + tr.x, y + tr.y, rotation, liquids.current().color); + Effect fshootEffect = shootEffect == Fx.none ? type.shootEffect : shootEffect; + Effect fsmokeEffect = smokeEffect == Fx.none ? type.smokeEffect : smokeEffect; + + fshootEffect.at(x + tr.x, y + tr.y, rotation, liquids.current().color); + fsmokeEffect.at(x + tr.x, y + tr.y, rotation, liquids.current().color); shootSound.at(tile); if(shootShake > 0){ diff --git a/core/src/mindustry/world/blocks/defense/turrets/PowerTurret.java b/core/src/mindustry/world/blocks/defense/turrets/PowerTurret.java index fe568dedfc..be1d3c25b8 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/PowerTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/PowerTurret.java @@ -4,7 +4,6 @@ import arc.struct.*; import mindustry.entities.bullet.*; import mindustry.logic.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; public class PowerTurret extends Turret{ public BulletType shootType; @@ -18,7 +17,7 @@ public class PowerTurret extends Turret{ @Override public void setStats(){ super.setStats(); - stats.add(Stat.ammo, new AmmoListValue<>(OrderedMap.of(this, shootType))); + stats.add(Stat.ammo, StatValues.ammo(ObjectMap.of(this, shootType))); } @Override @@ -31,7 +30,9 @@ public class PowerTurret extends Turret{ @Override public void updateTile(){ - unit.ammo(power.status * unit.type().ammoCapacity); + if(unit != null){ + unit.ammo(power.status * unit.type().ammoCapacity); + } super.updateTile(); } diff --git a/core/src/mindustry/world/blocks/defense/turrets/ReloadTurret.java b/core/src/mindustry/world/blocks/defense/turrets/ReloadTurret.java index bc172e9c46..a4bb4b7d8f 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/ReloadTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/ReloadTurret.java @@ -5,7 +5,6 @@ import arc.util.*; import mindustry.type.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -21,7 +20,7 @@ public class ReloadTurret extends BaseTurret{ super.setStats(); if(acceptCoolant){ - stats.add(Stat.booster, new BoosterListValue(reloadTime, consumes.get(ConsumeType.liquid).amount, coolantMultiplier, true, l -> consumes.liquidfilters.get(l.id))); + stats.add(Stat.booster, StatValues.boosters(reloadTime, consumes.get(ConsumeType.liquid).amount, coolantMultiplier, true, l -> consumes.liquidfilters.get(l.id))); } } @@ -29,16 +28,17 @@ public class ReloadTurret extends BaseTurret{ public float reload; protected void updateCooling(){ - float maxUsed = consumes.get(ConsumeType.liquid).amount; + if(reload < reloadTime){ + float maxUsed = consumes.get(ConsumeType.liquid).amount; + Liquid liquid = liquids.current(); - Liquid liquid = liquids.current(); + float used = Math.min(liquids.get(liquid), maxUsed * Time.delta) * baseReloadSpeed(); + reload += used * liquid.heatCapacity * coolantMultiplier; + liquids.remove(liquid, used); - float used = Math.min(Math.min(liquids.get(liquid), maxUsed * Time.delta), Math.max(0, ((reloadTime - reload) / coolantMultiplier) / liquid.heatCapacity)) * baseReloadSpeed(); - reload += used * liquid.heatCapacity * coolantMultiplier; - liquids.remove(liquid, used); - - if(Mathf.chance(0.06 * used)){ - coolEffect.at(x + Mathf.range(size * tilesize / 2f), y + Mathf.range(size * tilesize / 2f)); + if(Mathf.chance(0.06 * used)){ + coolEffect.at(x + Mathf.range(size * tilesize / 2f), y + Mathf.range(size * tilesize / 2f)); + } } } diff --git a/core/src/mindustry/world/blocks/defense/turrets/TractorBeamTurret.java b/core/src/mindustry/world/blocks/defense/turrets/TractorBeamTurret.java index a7a94a4ad1..50d0f39894 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/TractorBeamTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/TractorBeamTurret.java @@ -47,7 +47,6 @@ public class TractorBeamTurret extends BaseTurret{ //disabled due to version mismatch problems acceptCoolant = false; - expanded = true; } @Override @@ -64,6 +63,13 @@ public class TractorBeamTurret extends BaseTurret{ stats.add(Stat.damage, damage * 60f, StatUnit.perSecond); } + @Override + public void init(){ + super.init(); + + clipSize = Math.max(clipSize, (range + tilesize) * 2); + } + public class TractorBeamBuild extends BaseTurretBuild{ public @Nullable Unit target; public float lastX, lastY, strength; diff --git a/core/src/mindustry/world/blocks/defense/turrets/Turret.java b/core/src/mindustry/world/blocks/defense/turrets/Turret.java index cf793ad64d..5f551748d2 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/Turret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/Turret.java @@ -106,7 +106,7 @@ public class Turret extends ReloadTurret{ super.setStats(); stats.add(Stat.inaccuracy, (int)inaccuracy, StatUnit.degrees); - stats.add(Stat.reload, 60f / (reloadTime + 1) * (alternate ? 1 : shots), StatUnit.none); + stats.add(Stat.reload, 60f / (reloadTime) * (alternate ? 1 : shots), StatUnit.none); stats.add(Stat.targetsAir, targetAir); stats.add(Stat.targetsGround, targetGround); if(ammoPerShot != 1) stats.add(Stat.ammoUse, ammoPerShot, StatUnit.perShot); @@ -144,7 +144,7 @@ public class Turret extends ReloadTurret{ public boolean logicShooting = false; public @Nullable Posc target; public Vec2 targetPos = new Vec2(); - public BlockUnitc unit = Nulls.blockUnit; + public @Nullable BlockUnitc unit; public boolean wasShooting, charging; @Override @@ -155,7 +155,7 @@ public class Turret extends ReloadTurret{ @Override public void control(LAccess type, double p1, double p2, double p3, double p4){ - if(type == LAccess.shoot && !unit.isPlayer()){ + if(type == LAccess.shoot && (unit == null || !unit.isPlayer())){ targetPos.set(World.unconv((float)p1), World.unconv((float)p2)); logicControlTime = logicControlCooldown; logicShooting = !Mathf.zero(p3); @@ -166,7 +166,7 @@ public class Turret extends ReloadTurret{ @Override public void control(LAccess type, Object p1, double p2, double p3, double p4){ - if(type == LAccess.shootp && !unit.isPlayer()){ + if(type == LAccess.shootp && (unit == null || !unit.isPlayer())){ logicControlTime = logicControlCooldown; logicShooting = !Mathf.zero(p2); @@ -187,16 +187,21 @@ public class Turret extends ReloadTurret{ case shootX -> World.conv(targetPos.x); case shootY -> World.conv(targetPos.y); case shooting -> isShooting() ? 1 : 0; + case progress -> Mathf.clamp(reload / reloadTime); default -> super.sense(sensor); }; } public boolean isShooting(){ - return (isControlled() ? unit.isShooting() : logicControlled() ? logicShooting : target != null); + return (isControlled() ? (unit != null && unit.isShooting()) : logicControlled() ? logicShooting : target != null); } @Override public Unit unit(){ + if(unit == null){ + unit = (BlockUnitc)UnitTypes.block.create(team); + unit.tile(this); + } return (Unit)unit; } @@ -205,7 +210,7 @@ public class Turret extends ReloadTurret{ } public boolean isActive(){ - return target != null || wasShooting; + return (target != null || wasShooting) && enabled; } public void targetPosition(Posc pos){ @@ -247,10 +252,12 @@ public class Turret extends ReloadTurret{ recoil = Mathf.lerpDelta(recoil, 0f, restitution); heat = Mathf.lerpDelta(heat, 0f, cooldown); - unit.health(health); - unit.rotation(rotation); - unit.team(team); - unit.set(x, y); + if(unit != null){ + unit.health(health); + unit.rotation(rotation); + unit.team(team); + unit.set(x, y); + } if(logicControlTime > 0){ logicControlTime -= Time.delta; @@ -353,14 +360,14 @@ public class Turret extends ReloadTurret{ } protected void updateShooting(){ + reload += delta() * peekAmmo().reloadMultiplier * baseReloadSpeed(); + if(reload >= reloadTime && !charging){ BulletType type = peekAmmo(); shoot(type); - reload = 0f; - }else{ - reload += delta() * peekAmmo().reloadMultiplier * baseReloadSpeed(); + reload %= reloadTime; } } diff --git a/core/src/mindustry/world/blocks/distribution/BufferedItemBridge.java b/core/src/mindustry/world/blocks/distribution/BufferedItemBridge.java index a0eddc20c1..a87fb0348a 100644 --- a/core/src/mindustry/world/blocks/distribution/BufferedItemBridge.java +++ b/core/src/mindustry/world/blocks/distribution/BufferedItemBridge.java @@ -38,6 +38,11 @@ public class BufferedItemBridge extends ExtendingItemBridge{ } } + @Override + public void doDump(){ + dump(); + } + @Override public void write(Writes write){ super.write(write); diff --git a/core/src/mindustry/world/blocks/distribution/Conveyor.java b/core/src/mindustry/world/blocks/distribution/Conveyor.java index c1f6f53ef6..a5608bd1f6 100644 --- a/core/src/mindustry/world/blocks/distribution/Conveyor.java +++ b/core/src/mindustry/world/blocks/distribution/Conveyor.java @@ -106,8 +106,7 @@ public class Conveyor extends Block implements Autotiler{ public class ConveyorBuild extends Building implements ChainedBuilding{ //parallel array data public Item[] ids = new Item[capacity]; - public float[] xs = new float[capacity]; - public float[] ys = new float[capacity]; + public float[] xs = new float[capacity], ys = new float[capacity]; //amount of items, always < capacity public int len = 0; //next entity @@ -150,7 +149,7 @@ public class Conveyor extends Block implements Autotiler{ tr1.trns(rotation * 90, tilesize, 0); tr2.trns(rotation * 90, -tilesize / 2f, xs[i] * tilesize / 2f); - Draw.rect(item.icon(Cicon.medium), + Draw.rect(item.fullIcon, (tile.x * tilesize + tr1.x * ys[i] + tr2.x), (tile.y * tilesize + tr1.y * ys[i] + tr2.y), itemSize, itemSize); diff --git a/core/src/mindustry/world/blocks/distribution/Duct.java b/core/src/mindustry/world/blocks/distribution/Duct.java new file mode 100644 index 0000000000..6e29f879b5 --- /dev/null +++ b/core/src/mindustry/world/blocks/distribution/Duct.java @@ -0,0 +1,187 @@ +package mindustry.world.blocks.distribution; + +import arc.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.struct.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.input.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.blocks.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +public class Duct extends Block implements Autotiler{ + public float speed = 5f; + + public @Load(value = "@-top-#", length = 5) TextureRegion[] topRegions; + public @Load(value = "@-bottom-#", length = 5, fallback = "duct-bottom-#") TextureRegion[] botRegions; + + public Duct(String name){ + super(name); + + group = BlockGroup.transportation; + update = true; + solid = false; + hasItems = true; + conveyorPlacement = true; + unloadable = false; + itemCapacity = 1; + noUpdateDisabled = true; + rotate = true; + envEnabled = Env.space | Env.terrestrial | Env.underwater; + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.itemsMoved, 60f / speed, StatUnit.itemsSecond); + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + int[] bits = getTiling(req, list); + + if(bits == null) return; + + Draw.scl(bits[1], bits[2]); + Draw.alpha(0.5f); + Draw.rect(botRegions[bits[0]], req.drawx(), req.drawy(), req.rotation * 90); + Draw.color(); + Draw.rect(topRegions[bits[0]], req.drawx(), req.drawy(), req.rotation * 90); + Draw.scl(); + } + + @Override + public boolean blends(Tile tile, int rotation, int otherx, int othery, int otherrot, Block otherblock){ + return otherblock.outputsItems() && blendsArmored(tile, rotation, otherx, othery, otherrot, otherblock); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{Core.atlas.find("duct-bottom"), topRegions[0]}; + } + + @Override + public void handlePlacementLine(Seq plans){ + Placement.calculateDuctBridges(plans, (DuctBridge)Blocks.ductBridge); + } + + public class DuctBuild extends Building{ + public float progress; + public @Nullable Item current; + public int recDir = 0; + public int blendbits, xscl, yscl, blending; + public @Nullable Building next; + public @Nullable DuctBuild nextc; + + @Override + public void draw(){ + float rotation = rotdeg(); + int r = this.rotation; + + //draw extra ducts facing this one for tiling purposes + for(int i = 0; i < 4; i++){ + if((blending & (1 << i)) != 0 && !(i == 0 && nextc != null)){ + int dir = r - i; + float rot = i == 0 ? rotation : (dir)*90; + drawAt(x + Geometry.d4x(dir) * tilesize*0.75f, y + Geometry.d4y(dir) * tilesize*0.75f, 0, rot, i != 0 ? SliceMode.bottom : SliceMode.top); + } + } + + //draw item + if(current != null){ + Draw.z(Layer.blockUnder + 0.1f); + Tmp.v1.set(Geometry.d4x(recDir) * tilesize / 2f, Geometry.d4y(recDir) * tilesize / 2f) + .lerp(Geometry.d4x(r) * tilesize / 2f, Geometry.d4y(r) * tilesize / 2f, + Mathf.clamp((progress + 1f) / 2f)); + + Draw.rect(current.fullIcon, x + Tmp.v1.x, y + Tmp.v1.y, itemSize, itemSize); + } + + Draw.scl(xscl, yscl); + drawAt(x, y, blendbits, rotation, SliceMode.none); + Draw.reset(); + } + + protected void drawAt(float x, float y, int bits, float rotation, SliceMode slice){ + Draw.z(Layer.blockUnder); + Draw.rect(sliced(botRegions[bits], slice), x, y, rotation); + + Draw.z(Layer.blockUnder + 0.2f); + Draw.color(0.4f, 0.4f, 0.4f, 0.4f); + Draw.rect(sliced(botRegions[bits], slice), x, y, rotation); + Draw.color(); + Draw.rect(sliced(topRegions[bits], slice), x, y, rotation); + } + + @Override + public void updateTile(){ + progress += edelta() / speed * 2f; + + if(current != null && next != null){ + if(progress >= (1f - 1f/speed) && moveForward(current)){ + items.remove(current, 1); + current = null; + progress %= (1f - 1f/speed); + } + }else{ + progress = 0; + } + + if(current == null && items.total() > 0){ + current = items.first(); + } + } + + @Override + public boolean acceptItem(Building source, Item item){ + return current == null && items.total() == 0 && + (source.block instanceof Duct || Edges.getFacingEdge(source.tile(), tile).relativeTo(tile) == rotation); + } + + @Override + public int removeStack(Item item, int amount){ + int removed = super.removeStack(item, amount); + if(item == current) current = null; + return removed; + } + + @Override + public void handleStack(Item item, int amount, Teamc source){ + super.handleStack(item, amount, source); + current = item; + } + + @Override + public void handleItem(Building source, Item item){ + current = item; + progress = -1f; + recDir = relativeToEdge(source.tile); + items.add(item, 1); + noSleep(); + } + + @Override + public void onProximityUpdate(){ + super.onProximityUpdate(); + + int[] bits = buildBlending(tile, rotation, null, true); + blendbits = bits[0]; + xscl = bits[1]; + yscl = bits[2]; + blending = bits[4]; + next = front(); + nextc = next instanceof DuctBuild d ? d : null; + } + } +} diff --git a/core/src/mindustry/world/blocks/distribution/DuctBridge.java b/core/src/mindustry/world/blocks/distribution/DuctBridge.java new file mode 100644 index 0000000000..e040f83879 --- /dev/null +++ b/core/src/mindustry/world/blocks/distribution/DuctBridge.java @@ -0,0 +1,149 @@ +package mindustry.world.blocks.distribution; + +import arc.graphics.g2d.*; +import arc.math.geom.*; +import arc.struct.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.core.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.input.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +//TODO display range +public class DuctBridge extends Block{ + public @Load("@-bridge") TextureRegion bridgeRegion; + public @Load("@-bridge-bottom") TextureRegion bridgeBotRegion; + //public @Load("@-bridge-top") TextureRegion bridgeTopRegion; + public @Load("@-arrow") TextureRegion arrowRegion; + public @Load("@-dir") TextureRegion dirRegion; + + public int range = 4; + public float moveDelay = 5f; + + public DuctBridge(String name){ + super(name); + update = true; + solid = true; + rotate = true; + itemCapacity = 4; + hasItems = true; + group = BlockGroup.transportation; + noUpdateDisabled = true; + envEnabled = Env.space | Env.terrestrial | Env.underwater; + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(region, req.drawx(), req.drawy()); + Draw.rect(dirRegion, req.drawx(), req.drawy(), req.rotation * 90); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, dirRegion}; + } + + @Override + public void changePlacementPath(Seq points, int rotation){ + Placement.calculateNodes(points, this, rotation, (point, other) -> Math.max(Math.abs(point.x - other.x), Math.abs(point.y - other.y)) <= range); + } + + public boolean positionsValid(int x1, int y1, int x2, int y2){ + if(x1 == x2){ + return Math.abs(y1 - y2) <= range; + }else if(y1 == y2){ + return Math.abs(x1 - x2) <= range; + }else{ + return false; + } + } + + public class DuctBridgeBuild extends Building{ + public Building[] occupied = new Building[4]; + public float progress = 0f; + + @Override + public void draw(){ + Draw.rect(block.region, x, y); + Draw.rect(dirRegion, x, y, rotdeg()); + var link = findLink(); + if(link != null){ + Draw.z(Layer.power); + Draw.alpha(Renderer.bridgeOpacity); + float + angle = angleTo(link), + cx = (x + link.x)/2f, + cy = (y + link.y)/2f, + len = Math.max(Math.abs(x - link.x), Math.abs(y - link.y)) - size * tilesize; + + Draw.rect(bridgeRegion, cx, cy, len, tilesize, angle); + Draw.color(0.4f, 0.4f, 0.4f, 0.4f * Renderer.bridgeOpacity); + Draw.rect(bridgeBotRegion, cx, cy, len, tilesize, angle); + Draw.reset(); + Draw.alpha(Renderer.bridgeOpacity); + //Draw.rect(bridgeTopRegion, cx, cy, len, tilesize, angle); + + for(float i = 6f; i <= len + size * tilesize - 5f; i += 5f){ + Draw.rect(arrowRegion, x + Geometry.d4x(rotation) * i, y + Geometry.d4y(rotation) * i, angle); + } + + Draw.reset(); + } + } + + @Nullable + public DuctBridgeBuild findLink(){ + for(int i = 1; i <= range; i++){ + Tile other = tile.nearby(Geometry.d4x(rotation) * i, Geometry.d4y(rotation) * i); + if(other.build instanceof DuctBridgeBuild build && build.team == team){ + return build; + } + } + return null; + } + + @Override + public void updateTile(){ + var link = findLink(); + if(link != null){ + link.occupied[rotation % 4] = this; + if(items.any() && link.items.total() < link.block.itemCapacity){ + progress += edelta(); + while(progress > moveDelay){ + Item next = items.take(); + if(next != null && link.items.total() < link.block.itemCapacity){ + link.handleItem(this, next); + } + progress -= moveDelay; + } + } + } + + if(link == null && items.any()){ + Item next = items.first(); + if(moveForward(next)){ + items.remove(next, 1); + } + } + + for(int i = 0; i < 4; i++){ + if(occupied[i] == null || occupied[i].rotation != i || !occupied[i].isValid()){ + occupied[i] = null; + } + } + } + + @Override + public boolean acceptItem(Building source, Item item){ + int rel = this.relativeTo(source); + return items.total() < itemCapacity && rel != rotation && occupied[(rel + 2) % 4] == null; + } + } +} diff --git a/core/src/mindustry/world/blocks/distribution/DuctRouter.java b/core/src/mindustry/world/blocks/distribution/DuctRouter.java new file mode 100644 index 0000000000..b4c78d7ab8 --- /dev/null +++ b/core/src/mindustry/world/blocks/distribution/DuctRouter.java @@ -0,0 +1,125 @@ +package mindustry.world.blocks.distribution; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.meta.*; + +public class DuctRouter extends Block{ + public float speed = 5f; + + public @Load(value = "@-top") TextureRegion topRegion; + + public DuctRouter(String name){ + super(name); + + group = BlockGroup.transportation; + update = true; + solid = false; + hasItems = true; + conveyorPlacement = true; + unloadable = false; + itemCapacity = 1; + noUpdateDisabled = true; + rotate = true; + envEnabled = Env.space | Env.terrestrial | Env.underwater; + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.itemsMoved, 60f / speed, StatUnit.itemsSecond); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, topRegion}; + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(region, req.drawx(), req.drawy()); + Draw.rect(topRegion, req.drawx(), req.drawy(), req.rotation * 90); + } + + public class DuctBuild extends Building{ + public float progress; + public @Nullable Item current; + + @Override + public void draw(){ + Draw.rect(region, x, y); + Draw.rect(topRegion, x, y, rotdeg()); + } + + @Override + public void updateTile(){ + progress += edelta() / speed * 2f; + + if(current != null){ + if(progress >= (1f - 1f/speed)){ + var target = target(); + if(target != null){ + target.handleItem(this, current); + cdump = (byte)((cdump + 1) % 3); + items.remove(current, 1); + current = null; + progress %= (1f - 1f/speed); + } + } + }else{ + progress = 0; + } + + if(current == null && items.total() > 0){ + current = items.first(); + } + } + + @Nullable + public Building target(){ + if(current == null) return null; + + for(int i = -1; i <= 1; i++){ + Building other = nearby(Mathf.mod(rotation + i + cdump, 4)); + if(other != null && other.team == team && other.acceptItem(this, current)){ + return other; + } + } + return null; + } + + @Override + public boolean acceptItem(Building source, Item item){ + return current == null && items.total() == 0 && + (Edges.getFacingEdge(source.tile(), tile).relativeTo(tile) == rotation); + } + + @Override + public int removeStack(Item item, int amount){ + int removed = super.removeStack(item, amount); + if(item == current) current = null; + return removed; + } + + @Override + public void handleStack(Item item, int amount, Teamc source){ + super.handleStack(item, amount, source); + current = item; + } + + @Override + public void handleItem(Building source, Item item){ + current = item; + progress = -1f; + items.add(item, 1); + noSleep(); + } + } +} diff --git a/core/src/mindustry/world/blocks/distribution/ItemBridge.java b/core/src/mindustry/world/blocks/distribution/ItemBridge.java index e711d76c1c..53f69b5aef 100644 --- a/core/src/mindustry/world/blocks/distribution/ItemBridge.java +++ b/core/src/mindustry/world/blocks/distribution/ItemBridge.java @@ -23,7 +23,6 @@ import static mindustry.Vars.*; public class ItemBridge extends Block{ private static BuildPlan otherReq; - public final int timerTransport = timers++; public int range; public float transportTime = 2f; public @Load("@-end") TextureRegion endRegion; @@ -38,7 +37,6 @@ public class ItemBridge extends Block{ update = true; solid = true; hasPower = true; - expanded = true; itemCapacity = 10; configurable = true; hasItems = true; @@ -95,14 +93,12 @@ public class ItemBridge extends Block{ Tile link = findLink(x, y); - Lines.stroke(2f, Pal.placing); for(int i = 0; i < 4; i++){ - Lines.dashLine( + Drawf.dashLine(Pal.placing, x * tilesize + Geometry.d4[i].x * (tilesize / 2f + 2), y * tilesize + Geometry.d4[i].y * (tilesize / 2f + 2), - x * tilesize + Geometry.d4[i].x * (range + 0.5f) * tilesize, - y * tilesize + Geometry.d4[i].y * (range + 0.5f) * tilesize, - range); + x * tilesize + Geometry.d4[i].x * (range) * tilesize, + y * tilesize + Geometry.d4[i].y * (range) * tilesize); } Draw.reset(); @@ -149,6 +145,12 @@ public class ItemBridge extends Block{ return null; } + @Override + public void init(){ + super.init(); + clipSize = Math.max(clipSize, (range + 0.5f) * tilesize * 2); + } + @Override public void handlePlacementLine(Seq plans){ for(int i = 0; i < plans.size - 1; i++){ @@ -167,18 +169,20 @@ public class ItemBridge extends Block{ public class ItemBridgeBuild extends Building{ public int link = -1; + //TODO awful public IntSet incoming = new IntSet(); public float uptime; public float time; public float time2; public float cycleSpeed = 1f; + public float transportCounter; @Override public void playerPlaced(Object config){ super.playerPlaced(config); Tile link = findLink(tile.x, tile.y); - if(linkValid(tile, link) && !proximity.contains(link.build)){ + if(linkValid(tile, link) && this.link != link.pos() && !proximity.contains(link.build)){ link.build.configure(tile.pos()); } @@ -286,7 +290,7 @@ public class ItemBridge extends Block{ Tile other = world.tile(link); if(!linkValid(tile, other)){ - dump(); + doDump(); uptime = 0f; }else{ ((ItemBridgeBuild)other.build).incoming.add(tile.pos()); @@ -301,20 +305,27 @@ public class ItemBridge extends Block{ } } + public void doDump(){ + //allow dumping multiple times per frame + dumpAccumulate(); + } + public void updateTransport(Building other){ - if(uptime >= 0.5f && timer(timerTransport, transportTime)){ + boolean any = false; + transportCounter += edelta(); + while(transportCounter >= transportTime){ Item item = items.take(); if(item != null && other.acceptItem(this, item)){ other.handleItem(this, item); - cycleSpeed = Mathf.lerpDelta(cycleSpeed, 4f, 0.05f); //TODO this is kinda broken, because lerping only happens on a timer - }else{ - cycleSpeed = Mathf.lerpDelta(cycleSpeed, 1f, 0.01f); - if(item != null){ - items.add(item, 1); - items.undoFlow(item); - } + any = true; + }else if(item != null){ + items.add(item, 1); + items.undoFlow(item); } + transportCounter -= transportTime; } + + cycleSpeed = Mathf.lerpDelta(cycleSpeed, any ? 4f : 1f, any ? 0.05f : 0.01f); } @Override diff --git a/core/src/mindustry/world/blocks/distribution/MassDriver.java b/core/src/mindustry/world/blocks/distribution/MassDriver.java index cf3a1d9cd2..b8659c3d89 100644 --- a/core/src/mindustry/world/blocks/distribution/MassDriver.java +++ b/core/src/mindustry/world/blocks/distribution/MassDriver.java @@ -14,6 +14,7 @@ import mindustry.content.*; import mindustry.entities.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.meta.*; @@ -22,7 +23,7 @@ import static mindustry.Vars.*; public class MassDriver extends Block{ public float range; - public float rotateSpeed = 0.04f; + public float rotateSpeed = 5f; public float translation = 7f; public int minDistribute = 10; public float knockback = 4f; @@ -73,7 +74,7 @@ public class MassDriver extends Block{ //check if a mass driver is selected while placing this driver if(!control.input.frag.config.isShown()) return; Building selected = control.input.frag.config.getSelectedTile(); - if(selected == null || !(selected.block instanceof MassDriver) || !(selected.within(x * tilesize, y * tilesize, range))) return; + if(selected == null || selected.block != this || !selected.within(x * tilesize, y * tilesize, range)) return; //if so, draw a dotted line towards it while it is in range float sin = Mathf.absin(Time.time, 6f, 1f); @@ -89,7 +90,7 @@ public class MassDriver extends Block{ Draw.reset(); } - public class DriverBulletData implements Poolable{ + public static class DriverBulletData implements Poolable{ public MassDriverBuild from, to; public int[] items = new int[content.items().size]; @@ -105,9 +106,10 @@ public class MassDriver extends Block{ public float rotation = 90; public float reload = 0f; public DriverState state = DriverState.idle; - public OrderedSet waitingShooters = new OrderedSet<>(); + //TODO use queue? this array usually holds about 3 shooters max anyway + public OrderedSet waitingShooters = new OrderedSet<>(); - public Tile currentShooter(){ + public Building currentShooter(){ return waitingShooters.isEmpty() ? null : waitingShooters.first(); } @@ -125,9 +127,11 @@ public class MassDriver extends Block{ reload = Mathf.clamp(reload - edelta() / reloadTime); } + var current = currentShooter(); + //cleanup waiting shooters that are not valid - if(!shooterValid(currentShooter())){ - waitingShooters.remove(currentShooter()); + if(current != null && !shooterValid(current)){ + waitingShooters.remove(current); } //switch states @@ -142,7 +146,7 @@ public class MassDriver extends Block{ //dump when idle or accepting if(state == DriverState.idle || state == DriverState.accepting){ - dump(); + dumpAccumulate(); } //skip when there's no power @@ -158,7 +162,7 @@ public class MassDriver extends Block{ } //align to shooter rotation - rotation = Mathf.slerpDelta(rotation, tile.angleTo(currentShooter()), rotateSpeed * efficiency()); + rotation = Angles.moveToward(rotation, tile.angleTo(currentShooter()), rotateSpeed * efficiency()); }else if(state == DriverState.shooting){ //if there's nothing to shoot at OR someone wants to shoot at this thing, bail if(!hasLink || (!waitingShooters.isEmpty() && (itemCapacity - items.total() >= minDistribute))){ @@ -173,15 +177,15 @@ public class MassDriver extends Block{ link.block.itemCapacity - link.items.total() >= minDistribute //must have minimum amount of space ){ MassDriverBuild other = (MassDriverBuild)link; - other.waitingShooters.add(tile); + other.waitingShooters.add(this); if(reload <= 0.0001f){ //align to target location - rotation = Mathf.slerpDelta(rotation, targetRotation, rotateSpeed * efficiency()); + rotation = Angles.moveToward(rotation, targetRotation, rotateSpeed * efficiency()); //fire when it's the first in the queue and angles are ready. - if(other.currentShooter() == tile && + if(other.currentShooter() == this && other.state == DriverState.accepting && Angles.near(rotation, targetRotation, 2f) && Angles.near(other.rotation, targetRotation + 180f, 2f)){ //actually fire @@ -189,7 +193,7 @@ public class MassDriver extends Block{ float timeToArrive = Math.min(bulletLifetime, dst(other) / bulletSpeed); Time.run(timeToArrive, () -> { //remove waiting shooters, it's done firing - other.waitingShooters.remove(tile); + other.waitingShooters.remove(this); other.state = DriverState.idle; }); //driver is immediately idle @@ -200,6 +204,12 @@ public class MassDriver extends Block{ } } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(1f - reload / reloadTime); + return super.sense(sensor); + } + @Override public void draw(){ Draw.rect(baseRegion, x, y); @@ -222,9 +232,9 @@ public class MassDriver extends Block{ Lines.stroke(1f); Drawf.circles(x, y, (tile.block().size / 2f + 1) * tilesize + sin - 2f, Pal.accent); - for(Tile shooter : waitingShooters){ - Drawf.circles(shooter.drawx(), shooter.drawy(), (tile.block().size / 2f + 1) * tilesize + sin - 2f, Pal.place); - Drawf.arrow(shooter.drawx(), shooter.drawy(), x, y, size * tilesize + sin, 4f + sin, Pal.place); + for(var shooter : waitingShooters){ + Drawf.circles(shooter.x, shooter.y, (tile.block().size / 2f + 1) * tilesize + sin - 2f, Pal.place); + Drawf.arrow(shooter.x, shooter.y, x, y, size * tilesize + sin, 4f + sin, Pal.place); } if(linkValid()){ @@ -246,7 +256,7 @@ public class MassDriver extends Block{ if(link == other.pos()){ configure(-1); return false; - }else if(other.block instanceof MassDriver && other.dst(tile) <= range && other.team == team){ + }else if(other.block == block && other.dst(tile) <= range && other.team == team){ configure(other.pos()); return false; } @@ -281,11 +291,8 @@ public class MassDriver extends Block{ x + Angles.trnsx(angle, translation), y + Angles.trnsy(angle, translation), angle, -1f, bulletSpeed, bulletLifetime, data); - shootEffect.at(x + Angles.trnsx(angle, translation), - y + Angles.trnsy(angle, translation), angle); - - smokeEffect.at(x + Angles.trnsx(angle, translation), - y + Angles.trnsy(angle, translation), angle); + shootEffect.at(x + Angles.trnsx(angle, translation), y + Angles.trnsy(angle, translation), angle); + smokeEffect.at(x + Angles.trnsx(angle, translation), y + Angles.trnsy(angle, translation), angle); Effect.shake(shake, shake, this); @@ -314,16 +321,13 @@ public class MassDriver extends Block{ bullet.remove(); } - protected boolean shooterValid(Tile other){ - if(other == null) return true; - if(!(other.build instanceof MassDriverBuild entity)) return false; - return entity.link == tile.pos() && tile.dst(other) <= range; + protected boolean shooterValid(Building other){ + return other instanceof MassDriverBuild entity && other.consValid() && entity.block == block && entity.link == pos() && within(other, range); } protected boolean linkValid(){ if(link == -1) return false; - Building link = world.build(this.link); - return link instanceof MassDriverBuild && link.team == team && within(link, range); + return world.build(this.link) instanceof MassDriverBuild other && other.block == block && other.team == team && within(other, range); } @Override @@ -352,8 +356,7 @@ public class MassDriver extends Block{ public enum DriverState{ idle, //nothing is shooting at this mass driver and it does not have any target accepting, //currently getting shot at, unload items - shooting, - unloading; + shooting; public static final DriverState[] all = values(); } diff --git a/core/src/mindustry/world/blocks/distribution/PayloadConveyor.java b/core/src/mindustry/world/blocks/distribution/PayloadConveyor.java index df51bef7ae..7e1697f401 100644 --- a/core/src/mindustry/world/blocks/distribution/PayloadConveyor.java +++ b/core/src/mindustry/world/blocks/distribution/PayloadConveyor.java @@ -11,7 +11,6 @@ import mindustry.gen.*; import mindustry.graphics.*; import mindustry.world.*; import mindustry.world.blocks.payloads.*; -import mindustry.world.blocks.production.*; import mindustry.world.meta.*; import static mindustry.Vars.*; @@ -21,7 +20,7 @@ public class PayloadConveyor extends Block{ public @Load("@-top") TextureRegion topRegion; public @Load("@-edge") TextureRegion edgeRegion; public Interp interp = Interp.pow5; - public float payloadLimit = 2.5f; + public float payloadLimit = 2.9f; public PayloadConveyor(String name){ super(name); @@ -51,6 +50,13 @@ public class PayloadConveyor extends Block{ } } + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.payloadCapacity, (payloadLimit), StatUnit.blocksSquared); + } + public class PayloadConveyorBuild extends Building{ public @Nullable Payload item; public float progress, itemRotation, animation; @@ -59,6 +65,16 @@ public class PayloadConveyor extends Block{ public boolean blocked; public int step = -1, stepAccepted = -1; + @Override + public boolean canControlSelect(Player player){ + return this.item == null && !player.unit().spawnedByCore && player.unit().hitSize / tilesize <= payloadLimit && player.tileOn().build == this; + } + + @Override + public void onControlSelect(Player player){ + acceptPlayerPayload(player, p -> item = p); + } + @Override public Payload takePayload(){ Payload t = item; @@ -108,6 +124,9 @@ public class PayloadConveyor extends Block{ progress = time() % moveTime; updatePayload(); + if(item != null && next == null){ + PayloadBlock.pushOutput(item, progress / moveTime); + } //TODO nondeterministic input priority int curStep = curStep(); @@ -283,7 +302,7 @@ public class PayloadConveyor extends Block{ if(direction == rotation){ return !blocked || next != null; } - return PayloadAcceptor.blends(this, direction); + return PayloadBlock.blends(this, direction); } protected TextureRegion clipRegion(Rect bounds, Rect sprite, TextureRegion region){ diff --git a/core/src/mindustry/world/blocks/distribution/PayloadRouter.java b/core/src/mindustry/world/blocks/distribution/PayloadRouter.java index 3e1d2949dc..6a73e75bcc 100644 --- a/core/src/mindustry/world/blocks/distribution/PayloadRouter.java +++ b/core/src/mindustry/world/blocks/distribution/PayloadRouter.java @@ -7,6 +7,7 @@ import mindustry.annotations.Annotations.*; import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.world.blocks.payloads.*; public class PayloadRouter extends PayloadConveyor{ @@ -28,6 +29,7 @@ public class PayloadRouter extends PayloadConveyor{ public class PayloadRouterBuild extends PayloadConveyorBuild{ public float smoothRot; + public float controlTime = -1f; @Override public void add(){ @@ -36,7 +38,7 @@ public class PayloadRouter extends PayloadConveyor{ } public void pickNext(){ - if(item != null){ + if(item != null && controlTime <= 0f){ int rotations = 0; do{ rotation = (rotation + 1) % 4; @@ -47,6 +49,16 @@ public class PayloadRouter extends PayloadConveyor{ } } + @Override + public void control(LAccess type, double p1, double p2, double p3, double p4){ + super.control(type, p1, p2, p3, p4); + if(type == LAccess.config){ + rotation = (int)p1; + //when manually controlled, routers do not turn automatically for a while, same as turrets + controlTime = Building.timeToUncontrol; + } + } + @Override public void handlePayload(Building source, Payload payload){ super.handlePayload(source, payload); @@ -62,6 +74,7 @@ public class PayloadRouter extends PayloadConveyor{ public void updateTile(){ super.updateTile(); + controlTime -= Time.delta; smoothRot = Mathf.slerpDelta(smoothRot, rotdeg(), 0.2f); } diff --git a/core/src/mindustry/world/blocks/distribution/StackConveyor.java b/core/src/mindustry/world/blocks/distribution/StackConveyor.java index 6427aa26c1..7c7007cb54 100644 --- a/core/src/mindustry/world/blocks/distribution/StackConveyor.java +++ b/core/src/mindustry/world/blocks/distribution/StackConveyor.java @@ -145,7 +145,7 @@ public class StackConveyor extends Block implements Autotiler{ //item float size = itemSize * Mathf.lerp(Math.min((float)items.total() / itemCapacity, 1), 1f, 0.4f); Drawf.shadow(Tmp.v1.x, Tmp.v1.y, size * 1.2f); - Draw.rect(lastItem.icon(Cicon.medium), Tmp.v1.x, Tmp.v1.y, size, size, 0); + Draw.rect(lastItem.fullIcon, Tmp.v1.x, Tmp.v1.y, size, size, 0); } @Override diff --git a/core/src/mindustry/world/blocks/environment/Bush.java b/core/src/mindustry/world/blocks/environment/Bush.java new file mode 100644 index 0000000000..ec1f61d9c1 --- /dev/null +++ b/core/src/mindustry/world/blocks/environment/Bush.java @@ -0,0 +1,46 @@ +package mindustry.world.blocks.environment; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.world.*; + +public class Bush extends Prop{ + public @Load(value = "@-bot", fallback = "@") TextureRegion botRegion; + public @Load(value = "@-center") TextureRegion centerRegion; + + public int lobesMin = 7, lobesMax = 7; + public float botAngle = 60f, origin = 0.1f; + public float sclMin = 30f, sclMax = 50f, magMin = 5f, magMax = 15f, timeRange = 40f, spread = 0f; + + static Rand rand = new Rand(); + + public Bush(String name){ + super(name); + variants = 0; + } + + @Override + public void drawBase(Tile tile){ + rand.setSeed(tile.pos()); + float offset = rand.random(180f); + int lobes = rand.random(lobesMin, lobesMax); + for(int i = 0; i < lobes; i++){ + float ba = i / (float)lobes * 360f + offset + rand.range(spread), angle = ba + Mathf.sin(Time.time + rand.random(0, timeRange), rand.random(sclMin, sclMax), rand.random(magMin, magMax)); + float w = region.width * Draw.scl, h = region.height * Draw.scl; + var region = Angles.angleDist(ba, 225f) <= botAngle ? botRegion : this.region; + + Draw.rect(region, + tile.worldx() - Angles.trnsx(angle, origin) + w*0.5f, tile.worldy() - Angles.trnsy(angle, origin), + w, h, + origin*4f, h/2f, + angle + ); + } + + if(centerRegion.found()){ + Draw.rect(centerRegion, tile.worldx(), tile.worldy()); + } + } +} diff --git a/core/src/mindustry/world/blocks/environment/Floor.java b/core/src/mindustry/world/blocks/environment/Floor.java index 02aed4b003..7cee47c4d0 100644 --- a/core/src/mindustry/world/blocks/environment/Floor.java +++ b/core/src/mindustry/world/blocks/environment/Floor.java @@ -4,7 +4,6 @@ import arc.*; import arc.audio.*; import arc.graphics.*; import arc.graphics.g2d.*; -import arc.graphics.g2d.TextureAtlas.*; import arc.math.*; import arc.math.geom.*; import arc.struct.*; @@ -15,7 +14,6 @@ import mindustry.gen.*; import mindustry.graphics.*; import mindustry.graphics.MultiPacker.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.*; @@ -50,8 +48,6 @@ public class Floor extends Block{ public @Nullable Liquid liquidDrop = null; /** Multiplier for pumped liquids, used for deep water. */ public float liquidMultiplier = 1f; - /** item that drops from this block, used for drills */ - public @Nullable Item itemDrop = null; /** whether this block can be drowned in */ public boolean isLiquid; /** if true, this block cannot be mined by players. useful for annoying things like sand. */ @@ -119,7 +115,7 @@ public class Floor extends Block{ if(wall == null) wall = Blocks.air; if(decoration == Blocks.air){ - decoration = content.blocks().min(b -> b instanceof Boulder && b.minfo.mod == null && b.breakable ? mapColor.diff(b.mapColor) : Float.POSITIVE_INFINITY); + decoration = content.blocks().min(b -> b instanceof Prop && b.minfo.mod == null && b.breakable ? mapColor.diff(b.mapColor) : Float.POSITIVE_INFINITY); } if(isLiquid && walkEffect == Fx.none){ @@ -134,7 +130,7 @@ public class Floor extends Block{ @Override public void createIcons(MultiPacker packer){ super.createIcons(packer); - packer.add(PageType.editor, "editor-" + name, Core.atlas.getPixmap((AtlasRegion)icon(Cicon.full)).crop()); + packer.add(PageType.editor, "editor-" + name, Core.atlas.getPixmap(fullIcon).crop()); if(blendGroup != this){ return; @@ -147,16 +143,13 @@ public class Floor extends Block{ } } - Color color = new Color(); - Color color2 = new Color(); - PixmapRegion image = Core.atlas.getPixmap((AtlasRegion)icons()[0]); + PixmapRegion image = Core.atlas.getPixmap(icons()[0]); PixmapRegion edge = Core.atlas.getPixmap("edge-stencil"); Pixmap result = new Pixmap(edge.width, edge.height); for(int x = 0; x < edge.width; x++){ for(int y = 0; y < edge.height; y++){ - edge.getPixel(x, y, color); - result.draw(x, y, color.mul(color2.set(image.getPixel(x % image.width, y % image.height)))); + result.set(x, y, Color.muli(edge.get(x, y), image.get(x % image.width, y % image.height))); } } diff --git a/core/src/mindustry/world/blocks/environment/OreBlock.java b/core/src/mindustry/world/blocks/environment/OreBlock.java index c08a3eccce..1ff509465f 100644 --- a/core/src/mindustry/world/blocks/environment/OreBlock.java +++ b/core/src/mindustry/world/blocks/environment/OreBlock.java @@ -14,8 +14,8 @@ import static mindustry.Vars.*; /**An overlay ore for a specific item type.*/ public class OreBlock extends OverlayFloor{ - public OreBlock(Item ore){ - super("ore-" + ore.name); + public OreBlock(String name, Item ore){ + super(name); this.localizedName = ore.localizedName; this.itemDrop = ore; this.variants = 3; @@ -23,6 +23,10 @@ public class OreBlock extends OverlayFloor{ this.useColor = true; } + public OreBlock(Item ore){ + this("ore-" + ore.name, ore); + } + /** For mod use only!*/ public OreBlock(String name){ super(name); @@ -39,25 +43,20 @@ public class OreBlock extends OverlayFloor{ @OverrideCallSuper public void createIcons(MultiPacker packer){ for(int i = 0; i < variants; i++){ - Pixmap image = new Pixmap(32, 32); PixmapRegion shadow = Core.atlas.getPixmap(itemDrop.name + (i + 1)); + Pixmap image = shadow.crop(); - int offset = image.getWidth() / tilesize - 1; - Color color = new Color(); + int offset = image.width / tilesize - 1; + int shadowColor = Color.rgba8888(0, 0, 0, 0.3f); - for(int x = 0; x < image.getWidth(); x++){ - for(int y = offset; y < image.getHeight(); y++){ - shadow.getPixel(x, y - offset, color); - - if(color.a > 0.001f){ - color.set(0, 0, 0, 0.3f); - image.draw(x, y, color); + for(int x = 0; x < image.width; x++){ + for(int y = offset; y < image.height; y++){ + if(shadow.getA(x, y) == 0 && shadow.getA(x, y - offset) != 0){ + image.setRaw(x, y, shadowColor); } } } - image.draw(shadow); - packer.add(PageType.environment, name + (i + 1), image); packer.add(PageType.editor, "editor-" + name + (i + 1), image); diff --git a/core/src/mindustry/world/blocks/environment/Boulder.java b/core/src/mindustry/world/blocks/environment/Prop.java similarity index 71% rename from core/src/mindustry/world/blocks/environment/Boulder.java rename to core/src/mindustry/world/blocks/environment/Prop.java index 5e453a7c28..1056748bce 100644 --- a/core/src/mindustry/world/blocks/environment/Boulder.java +++ b/core/src/mindustry/world/blocks/environment/Prop.java @@ -3,26 +3,25 @@ package mindustry.world.blocks.environment; import arc.*; import arc.graphics.g2d.*; import arc.math.*; +import mindustry.content.*; import mindustry.world.*; -public class Boulder extends Block{ +public class Prop extends Block{ public int variants; - public Boulder(String name){ + public Prop(String name){ super(name); breakable = true; alwaysReplace = true; + instantDeconstruct = true; deconstructThreshold = 0.35f; + breakEffect = Fx.breakProp; } @Override public void drawBase(Tile tile){ - if(variants > 0){ - Draw.rect(variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantRegions.length - 1))], tile.worldx(), tile.worldy()); - }else{ - Draw.rect(region, tile.worldx(), tile.worldy()); - } + Draw.rect(variants > 0 ? variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantRegions.length - 1))] : region, tile.worldx(), tile.worldy()); } @Override diff --git a/core/src/mindustry/world/blocks/environment/StaticClusterWall.java b/core/src/mindustry/world/blocks/environment/StaticClusterWall.java new file mode 100644 index 0000000000..90f9417863 --- /dev/null +++ b/core/src/mindustry/world/blocks/environment/StaticClusterWall.java @@ -0,0 +1,25 @@ +package mindustry.world.blocks.environment; + +import arc.graphics.g2d.*; +import arc.math.*; +import mindustry.annotations.Annotations.*; +import mindustry.world.*; + +public class StaticClusterWall extends StaticWall{ + public @Load(value = "@-cluster#", length = 1) TextureRegion[] clusters; + + public StaticClusterWall(String name){ + super(name); + variants = 1; + } + + @Override + public void drawBase(Tile tile){ + super.drawBase(tile); + + if(Mathf.randomSeed(tile.pos(), 10) < 2){ + Draw.rect(clusters[0], tile.worldx(), tile.worldy(), Mathf.randomSeedRange(tile.pos() + 1, 180f)); + } + } + +} diff --git a/core/src/mindustry/world/blocks/environment/StaticWall.java b/core/src/mindustry/world/blocks/environment/StaticWall.java index a364db4ab2..ff648f6dae 100644 --- a/core/src/mindustry/world/blocks/environment/StaticWall.java +++ b/core/src/mindustry/world/blocks/environment/StaticWall.java @@ -10,7 +10,7 @@ import mindustry.world.*; import static mindustry.Vars.*; -public class StaticWall extends Boulder{ +public class StaticWall extends Prop{ public @Load("@-large") TextureRegion large; public TextureRegion[][] split; @@ -34,6 +34,11 @@ public class StaticWall extends Boulder{ }else{ Draw.rect(region, tile.worldx(), tile.worldy()); } + + //draw ore on top + if(tile.overlay() instanceof WallOreBlock ore){ + ore.drawBase(tile); + } } @Override diff --git a/core/src/mindustry/world/blocks/environment/TreeBlock.java b/core/src/mindustry/world/blocks/environment/TreeBlock.java index d0354c2811..d33c1afc30 100644 --- a/core/src/mindustry/world/blocks/environment/TreeBlock.java +++ b/core/src/mindustry/world/blocks/environment/TreeBlock.java @@ -2,7 +2,6 @@ package mindustry.world.blocks.environment; import arc.graphics.g2d.*; import arc.math.*; -import arc.math.geom.*; import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.graphics.*; @@ -15,16 +14,17 @@ public class TreeBlock extends Block{ public TreeBlock(String name){ super(name); solid = true; - expanded = true; + clipSize = 90; } @Override public void drawBase(Tile tile){ - float x = tile.worldx(), y = tile.worldy(); - float rot = Mathf.randomSeed(tile.pos(), 0, 4) * 90 + Mathf.sin(Time.time + x, 50f, 0.5f) + Mathf.sin(Time.time - y, 65f, 0.9f) + Mathf.sin(Time.time + y - x, 85f, 0.9f); - float w = region.width * Draw.scl, h = region.height * Draw.scl; - float scl = 30f, mag = 0.2f; + float + x = tile.worldx(), y = tile.worldy(), + rot = Mathf.randomSeed(tile.pos(), 0, 4) * 90 + Mathf.sin(Time.time + x, 50f, 0.5f) + Mathf.sin(Time.time - y, 65f, 0.9f) + Mathf.sin(Time.time + y - x, 85f, 0.9f), + w = region.width * Draw.scl, h = region.height * Draw.scl, + scl = 30f, mag = 0.2f; if(shadow.found()){ Draw.z(Layer.power - 1); @@ -32,15 +32,9 @@ public class TreeBlock extends Block{ } Draw.z(Layer.power + 1); - Draw.rectv(region, x, y, w, h, rot, vec -> { - vec.add( - Mathf.sin(vec.y*3 + Time.time, scl, mag) + Mathf.sin(vec.x*3 - Time.time, 70, 0.8f), - Mathf.cos(vec.x*3 + Time.time + 8, scl + 6f, mag * 1.1f) + Mathf.sin(vec.y*3 - Time.time, 50, 0.2f) - ); - }); - } - - void tweak(Vec2 vec){ - + Draw.rectv(region, x, y, w, h, rot, vec -> vec.add( + Mathf.sin(vec.y*3 + Time.time, scl, mag) + Mathf.sin(vec.x*3 - Time.time, 70, 0.8f), + Mathf.cos(vec.x*3 + Time.time + 8, scl + 6f, mag * 1.1f) + Mathf.sin(vec.y*3 - Time.time, 50, 0.2f) + )); } } diff --git a/core/src/mindustry/world/blocks/environment/WallOreBlock.java b/core/src/mindustry/world/blocks/environment/WallOreBlock.java new file mode 100644 index 0000000000..cdfffff89e --- /dev/null +++ b/core/src/mindustry/world/blocks/environment/WallOreBlock.java @@ -0,0 +1,16 @@ +package mindustry.world.blocks.environment; + +import mindustry.type.*; + +/**An overlay ore that draws on top of walls. */ +public class WallOreBlock extends OreBlock{ + + public WallOreBlock(Item ore){ + super("wall-ore-" + ore.name, ore); + } + + //mods only + public WallOreBlock(String name){ + super(name); + } +} diff --git a/core/src/mindustry/world/blocks/environment/WavingProp.java b/core/src/mindustry/world/blocks/environment/WavingProp.java new file mode 100644 index 0000000000..e999137af5 --- /dev/null +++ b/core/src/mindustry/world/blocks/environment/WavingProp.java @@ -0,0 +1,30 @@ +package mindustry.world.blocks.environment; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.world.*; + +public class WavingProp extends Prop{ + + public WavingProp(String name){ + super(name); + } + + @Override + public void drawBase(Tile tile){ + var region = variants > 0 ? variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantRegions.length - 1))] : this.region; + + float + x = tile.worldx(), y = tile.worldy(), + rotmag = 3f, rotscl = 0.5f, + rot = Mathf.randomSeedRange(tile.pos(), 20f) - 45 + Mathf.sin(Time.time + x, 50f * rotscl, 0.5f * rotmag) + Mathf.sin(Time.time - y, 65f * rotscl, 0.9f* rotmag) + Mathf.sin(Time.time + y - x, 85f * rotscl, 0.9f* rotmag), + w = region.width * Draw.scl, h = region.height * Draw.scl, + scl = 30f, mag = 0.3f; + + Draw.rectv(region, x, y, w, h, rot, vec -> vec.add( + Mathf.sin(vec.y*3 + Time.time, scl, mag) + Mathf.sin(vec.x*3 - Time.time, 70, 0.8f), + Mathf.cos(vec.x*3 + Time.time + 8, scl + 6f, mag * 1.1f) + Mathf.sin(vec.y*3 - Time.time, 50, 0.2f) + )); + } +} diff --git a/core/src/mindustry/world/blocks/environment/WobbleProp.java b/core/src/mindustry/world/blocks/environment/WobbleProp.java new file mode 100644 index 0000000000..8a8f005574 --- /dev/null +++ b/core/src/mindustry/world/blocks/environment/WobbleProp.java @@ -0,0 +1,24 @@ +package mindustry.world.blocks.environment; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.world.*; + +public class WobbleProp extends Prop{ + public float wscl = 25f, wmag = 0.4f, wtscl = 1f, wmag2 = 1f; + + public WobbleProp(String name){ + super(name); + } + + @Override + public void drawBase(Tile tile){ + var region = variants > 0 ? variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantRegions.length - 1))] : this.region; + + Draw.rectv(region, tile.worldx(), tile.worldy(), region.width * Draw.scl, region.height * Draw.scl, 0, vec -> vec.add( + Mathf.sin(vec.y*3 + Time.time, wscl, wmag) + Mathf.sin(vec.x*3 - Time.time, 70 * wtscl, 0.8f * wmag2), + Mathf.cos(vec.x*3 + Time.time + 8, wscl + 6f, wmag * 1.1f) + Mathf.sin(vec.y*3 - Time.time, 50 * wtscl, 0.2f * wmag2) + )); + } +} diff --git a/core/src/mindustry/world/blocks/experimental/BlockForge.java b/core/src/mindustry/world/blocks/experimental/BlockForge.java deleted file mode 100644 index fa871d347c..0000000000 --- a/core/src/mindustry/world/blocks/experimental/BlockForge.java +++ /dev/null @@ -1,169 +0,0 @@ -package mindustry.world.blocks.experimental; - -import arc.graphics.*; -import arc.graphics.g2d.*; -import arc.math.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import arc.util.*; -import arc.util.io.*; -import mindustry.*; -import mindustry.entities.units.*; -import mindustry.gen.*; -import mindustry.graphics.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.*; -import mindustry.world.blocks.*; -import mindustry.world.blocks.payloads.*; -import mindustry.world.blocks.production.*; -import mindustry.world.consumers.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class BlockForge extends PayloadAcceptor{ - public float buildSpeed = 0.4f; - public int minBlockSize = 1, maxBlockSize = 2; - - public BlockForge(String name){ - super(name); - - size = 3; - update = true; - outputsPayload = true; - hasItems = true; - configurable = true; - hasPower = true; - rotate = true; - - config(Block.class, (BlockForgeBuild tile, Block block) -> { - if(tile.recipe != block) tile.progress = 0f; - tile.recipe = block; - }); - - consumes.add(new ConsumeItemDynamic((BlockForgeBuild e) -> e.recipe != null ? e.recipe.requirements : ItemStack.empty)); - } - - @Override - public TextureRegion[] icons(){ - return new TextureRegion[]{region, outRegion}; - } - - @Override - public void setBars(){ - super.setBars(); - - bars.add("progress", (BlockForgeBuild entity) -> new Bar("bar.progress", Pal.ammo, () -> entity.recipe == null ? 0f : (entity.progress / entity.recipe.buildCost))); - } - - @Override - public void setStats(){ - super.setStats(); - - stats.add(Stat.output, "@x@ ~ @x@", minBlockSize, minBlockSize, maxBlockSize, maxBlockSize); - } - - - @Override - public void drawRequestRegion(BuildPlan req, Eachable list){ - Draw.rect(region, req.drawx(), req.drawy()); - Draw.rect(outRegion, req.drawx(), req.drawy(), req.rotation * 90); - } - - public class BlockForgeBuild extends PayloadAcceptorBuild{ - public @Nullable Block recipe; - public float progress, time, heat; - - @Override - public boolean acceptItem(Building source, Item item){ - return items.get(item) < getMaximumAccepted(item); - } - - @Override - public int getMaximumAccepted(Item item){ - if(recipe == null) return 0; - for(ItemStack stack : recipe.requirements){ - if(stack.item == item) return stack.amount * 2; - } - return 0; - } - - @Override - public boolean acceptPayload(Building source, Payload payload){ - return false; - } - - @Override - public void updateTile(){ - boolean produce = recipe != null && consValid() && payload == null; - - if(produce){ - progress += buildSpeed * edelta(); - - if(progress >= recipe.buildCost){ - consume(); - payload = new BuildPayload(recipe, team); - payVector.setZero(); - progress %= 1f; - } - } - - heat = Mathf.lerpDelta(heat, Mathf.num(produce), 0.3f); - time += heat * delta(); - - moveOutPayload(); - } - - @Override - public void buildConfiguration(Table table){ - Seq blocks = Vars.content.blocks().select(b -> b.isVisible() && b.size >= minBlockSize && b.size <= maxBlockSize); - - ItemSelection.buildTable(table, blocks, () -> recipe, this::configure); - } - - @Override - public Object config(){ - return recipe; - } - - @Override - public void draw(){ - Draw.rect(region, x, y); - Draw.rect(outRegion, x, y, rotdeg()); - - if(recipe != null){ - Draw.draw(Layer.blockOver, () -> Drawf.construct(this, recipe, 0, progress / recipe.buildCost, heat, time)); - } - - drawPayload(); - } - - @Override - public void drawSelect(){ - if(recipe != null){ - float dx = x - size * tilesize/2f, dy = y + size * tilesize/2f; - TextureRegion icon = recipe.icon(Cicon.medium); - Draw.mixcol(Color.darkGray, 1f); - //Fixes size because modded content icons are not scaled - Draw.rect(icon, dx - 0.7f, dy - 1f, Draw.scl * Draw.xscl * 24f, Draw.scl * Draw.yscl * 24f); - Draw.reset(); - Draw.rect(icon, dx, dy, Draw.scl * Draw.xscl * 24f, Draw.scl * Draw.yscl * 24f); - } - } - - @Override - public void write(Writes write){ - super.write(write); - write.s(recipe == null ? -1 : recipe.id); - write.f(progress); - } - - @Override - public void read(Reads read, byte revision){ - super.read(read, revision); - recipe = Vars.content.block(read.s()); - progress = read.f(); - } - } -} diff --git a/core/src/mindustry/world/blocks/experimental/BlockLoader.java b/core/src/mindustry/world/blocks/experimental/BlockLoader.java index 172ea8e5b7..41f2aa13f8 100644 --- a/core/src/mindustry/world/blocks/experimental/BlockLoader.java +++ b/core/src/mindustry/world/blocks/experimental/BlockLoader.java @@ -9,11 +9,10 @@ import mindustry.graphics.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.blocks.payloads.*; -import mindustry.world.blocks.production.*; import static mindustry.Vars.*; -public class BlockLoader extends PayloadAcceptor{ +public class BlockLoader extends PayloadBlock{ public final int timerLoad = timers++; public float loadTime = 2f; @@ -58,7 +57,7 @@ public class BlockLoader extends PayloadAcceptor{ Draw.rect(topRegion, req.drawx(), req.drawy()); } - public class BlockLoaderBuild extends PayloadAcceptorBuild{ + public class BlockLoaderBuild extends PayloadBlockBuild{ @Override public boolean acceptPayload(Building source, Payload payload){ @@ -90,7 +89,6 @@ public class BlockLoader extends PayloadAcceptor{ Draw.rect(outRegion, x, y, rotdeg()); Draw.z(Layer.blockOver); - payRotation = rotdeg(); drawPayload(); Draw.z(Layer.blockOver + 0.1f); diff --git a/core/src/mindustry/world/blocks/logic/LogicBlock.java b/core/src/mindustry/world/blocks/logic/LogicBlock.java index 62f23884d5..f38644e5f5 100644 --- a/core/src/mindustry/world/blocks/logic/LogicBlock.java +++ b/core/src/mindustry/world/blocks/logic/LogicBlock.java @@ -197,14 +197,15 @@ public class LogicBlock extends Block{ public Seq links = new Seq<>(); public boolean checkedDuplicates = false; - public void readCompressed(byte[] data, boolean relative){ - DataInputStream stream = new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data))); + /** Block of code to run after load. */ + public @Nullable Runnable loadBlock; - try{ + public void readCompressed(byte[] data, boolean relative){ + try(DataInputStream stream = new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data)))){ int version = stream.read(); int bytelen = stream.readInt(); - if(bytelen > maxByteLen) throw new RuntimeException("Malformed logic data! Length: " + bytelen); + if(bytelen > maxByteLen) throw new IOException("Malformed logic data! Length: " + bytelen); byte[] bytes = new byte[bytelen]; stream.readFully(bytes); @@ -243,6 +244,7 @@ public class LogicBlock extends Block{ updateCode(new String(bytes, charset)); }catch(Exception ignored){ + //invalid logic doesn't matter here } } @@ -350,11 +352,8 @@ public class LogicBlock extends Block{ executor.load(asm); }catch(Exception e){ - Log.err("Failed to compile logic program @", code); - Log.err(e); - //handle malformed code and replace it with nothing - executor.load(""); + executor.load(code = ""); } } } @@ -372,6 +371,12 @@ public class LogicBlock extends Block{ @Override public void updateTile(){ + //load up code from read() + if(loadBlock != null){ + loadBlock.run(); + loadBlock = null; + } + executor.team = team; if(!checkedDuplicates){ @@ -447,9 +452,9 @@ public class LogicBlock extends Block{ } public Seq relativeConnections(){ - Seq copy = new Seq<>(links.size); - for(LogicLink l : links){ - LogicLink c = l.copy(); + var copy = new Seq(links.size); + for(var l : links){ + var c = l.copy(); c.x -= tileX(); c.y -= tileY(); copy.add(c); @@ -582,8 +587,7 @@ public class LogicBlock extends Block{ //skip memory, it isn't used anymore read.skip(memory * 8); - updateCode(code, false, asm -> { - + loadBlock = () -> updateCode(code, false, asm -> { //load up the variables that were stored for(int i = 0; i < varcount; i++){ BVar dest = asm.getVar(names[i]); @@ -592,6 +596,7 @@ public class LogicBlock extends Block{ } } }); + } } } diff --git a/core/src/mindustry/world/blocks/payloads/BallisticSilo.java b/core/src/mindustry/world/blocks/payloads/BallisticSilo.java new file mode 100644 index 0000000000..a173a5ec94 --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/BallisticSilo.java @@ -0,0 +1,29 @@ +package mindustry.world.blocks.payloads; + +import mindustry.gen.*; +import mindustry.world.blocks.payloads.NuclearWarhead.*; + +public class BallisticSilo extends PayloadBlock{ + + public BallisticSilo(String name){ + super(name); + } + + public class BallisticSiloBuild extends PayloadBlockBuild{ + @Override + public boolean acceptPayload(Building source, Payload payload){ + return this.payload == null && payload instanceof BuildPayload b && b.build instanceof NuclearWarheadBuild; + } + + @Override + public void updateTile(){ + moveInPayload(); + } + + @Override + public void draw(){ + super.draw(); + drawPayload(); + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/BlockForge.java b/core/src/mindustry/world/blocks/payloads/BlockForge.java new file mode 100644 index 0000000000..52827780af --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/BlockForge.java @@ -0,0 +1,89 @@ +package mindustry.world.blocks.payloads; + +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.scene.ui.layout.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.*; +import mindustry.ui.*; +import mindustry.world.*; +import mindustry.world.blocks.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +/** Configurable BlockProducer variant. */ +public class BlockForge extends BlockProducer{ + public float buildSpeed = 0.4f; + public int minBlockSize = 1, maxBlockSize = 2; + + public BlockForge(String name){ + super(name); + + size = 3; + configurable = true; + + config(Block.class, (BlockForgeBuild tile, Block block) -> { + if(tile.recipe != block) tile.progress = 0f; + if(canProduce(block)){ + tile.recipe = block; + } + }); + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.output, "@x@ ~ @x@", minBlockSize, minBlockSize, maxBlockSize, maxBlockSize); + } + + public boolean canProduce(Block b){ + return b.isVisible() && b.size >= minBlockSize && b.size <= maxBlockSize; + } + + public class BlockForgeBuild extends BlockProducerBuild{ + public @Nullable Block recipe; + + @Override + public @Nullable Block recipe(){ + return recipe; + } + + @Override + public void buildConfiguration(Table table){ + ItemSelection.buildTable(table, content.blocks().select(BlockForge.this::canProduce), () -> recipe, this::configure); + } + + @Override + public Object config(){ + return recipe; + } + + @Override + public void drawSelect(){ + if(recipe != null){ + float dx = x - size * tilesize/2f, dy = y + size * tilesize/2f; + TextureRegion icon = recipe.uiIcon; + Draw.mixcol(Color.darkGray, 1f); + //Fixes size because modded content icons are not scaled + Draw.rect(icon, dx - 0.7f, dy - 1f, Draw.scl * Draw.xscl * 24f, Draw.scl * Draw.yscl * 24f); + Draw.reset(); + Draw.rect(icon, dx, dy, Draw.scl * Draw.xscl * 24f, Draw.scl * Draw.yscl * 24f); + } + } + + @Override + public void write(Writes write){ + super.write(write); + write.s(recipe == null ? -1 : recipe.id); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + recipe = Vars.content.block(read.s()); + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/BlockProducer.java b/core/src/mindustry/world/blocks/payloads/BlockProducer.java new file mode 100644 index 0000000000..ec8642cc90 --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/BlockProducer.java @@ -0,0 +1,143 @@ +package mindustry.world.blocks.payloads; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.type.*; +import mindustry.ui.*; +import mindustry.world.*; +import mindustry.world.consumers.*; + +import static mindustry.Vars.*; + +/** Generic building that produces other buildings. */ +public abstract class BlockProducer extends PayloadBlock{ + public float buildSpeed = 0.4f; + + public BlockProducer(String name){ + super(name); + + size = 3; + update = true; + outputsPayload = true; + hasItems = true; + hasPower = true; + rotate = true; + + consumes.add(new ConsumeItemDynamic((BlockProducerBuild e) -> e.recipe() != null ? e.recipe().requirements : ItemStack.empty)); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, outRegion}; + } + + @Override + public void setBars(){ + super.setBars(); + + bars.add("progress", (BlockProducerBuild entity) -> new Bar("bar.progress", Pal.ammo, () -> entity.recipe() == null ? 0f : (entity.progress / entity.recipe().buildCost))); + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(region, req.drawx(), req.drawy()); + Draw.rect(outRegion, req.drawx(), req.drawy(), req.rotation * 90); + } + + public abstract class BlockProducerBuild extends PayloadBlockBuild{ + public float progress, time, heat; + + public abstract @Nullable Block recipe(); + + @Override + public boolean acceptItem(Building source, Item item){ + return items.get(item) < getMaximumAccepted(item); + } + + @Override + public int getMaximumAccepted(Item item){ + if(recipe() == null) return 0; + for(ItemStack stack : recipe().requirements){ + if(stack.item == item) return stack.amount * 2; + } + return 0; + } + + @Override + public boolean acceptPayload(Building source, Payload payload){ + return false; + } + + @Override + public void updateTile(){ + var recipe = recipe(); + boolean produce = recipe != null && consValid() && payload == null; + + if(produce){ + progress += buildSpeed * edelta(); + + if(progress >= recipe.buildCost){ + consume(); + payload = new BuildPayload(recipe, team); + payVector.setZero(); + progress %= 1f; + } + } + + heat = Mathf.lerpDelta(heat, Mathf.num(produce), 0.15f); + time += heat * delta(); + + moveOutPayload(); + } + + @Override + public void draw(){ + Draw.rect(region, x, y); + Draw.rect(outRegion, x, y, rotdeg()); + + var recipe = recipe(); + if(recipe != null){ + Drawf.shadow(x, y, recipe.size * tilesize * 2f, progress / recipe.buildCost); + Draw.draw(Layer.blockBuilding, () -> { + Draw.color(Pal.accent); + + for(TextureRegion region : recipe.getGeneratedIcons()){ + Shaders.blockbuild.region = region; + Shaders.blockbuild.progress = progress / recipe.buildCost; + + Draw.rect(region, x, y, recipe.rotate ? rotdeg() : 0); + Draw.flush(); + } + + Draw.color(); + }); + Draw.z(Layer.blockBuilding + 1); + Draw.color(Pal.accent, heat); + + Lines.lineAngleCenter(x + Mathf.sin(time, 10f, Vars.tilesize / 2f * recipe.size + 1f), y, 90, recipe.size * Vars.tilesize + 1f); + + Draw.reset(); + } + + drawPayload(); + } + + @Override + public void write(Writes write){ + super.write(write); + write.f(progress); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + progress = read.f(); + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/BuildPayload.java b/core/src/mindustry/world/blocks/payloads/BuildPayload.java index 446954883b..d9e6c08fa0 100644 --- a/core/src/mindustry/world/blocks/payloads/BuildPayload.java +++ b/core/src/mindustry/world/blocks/payloads/BuildPayload.java @@ -5,7 +5,6 @@ import arc.util.io.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; -import mindustry.ui.*; import mindustry.world.*; import static mindustry.Vars.*; @@ -34,6 +33,16 @@ public class BuildPayload implements Payload{ build.dropped(); } + @Override + public float x(){ + return build.x; + } + + @Override + public float y(){ + return build.y; + } + @Override public float size(){ return build.block.size * tilesize; @@ -55,11 +64,11 @@ public class BuildPayload implements Payload{ @Override public void draw(){ Drawf.shadow(build.x, build.y, build.block.size * tilesize * 2f); - Draw.rect(build.block.icon(Cicon.full), build.x, build.y); + Draw.rect(build.block.fullIcon, build.x, build.y); } @Override - public TextureRegion icon(Cicon icon){ - return block().icon(icon); + public TextureRegion icon(){ + return block().fullIcon; } } diff --git a/core/src/mindustry/world/blocks/payloads/NuclearWarhead.java b/core/src/mindustry/world/blocks/payloads/NuclearWarhead.java new file mode 100644 index 0000000000..4f4805822c --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/NuclearWarhead.java @@ -0,0 +1,18 @@ +package mindustry.world.blocks.payloads; + +import mindustry.gen.*; +import mindustry.world.*; + +public class NuclearWarhead extends Block{ + public float radius = 100f; + + public NuclearWarhead(String name){ + super(name); + solid = true; + update = true; + } + + public class NuclearWarheadBuild extends Building{ + + } +} diff --git a/core/src/mindustry/world/blocks/payloads/Payload.java b/core/src/mindustry/world/blocks/payloads/Payload.java index afce4b0da4..d182f08a37 100644 --- a/core/src/mindustry/world/blocks/payloads/Payload.java +++ b/core/src/mindustry/world/blocks/payloads/Payload.java @@ -1,16 +1,16 @@ package mindustry.world.blocks.payloads; import arc.graphics.g2d.*; +import arc.math.geom.*; import arc.util.*; import arc.util.io.*; import mindustry.game.*; import mindustry.gen.*; -import mindustry.ui.*; import mindustry.world.*; import static mindustry.Vars.*; -public interface Payload{ +public interface Payload extends Position{ int payloadUnit = 0, payloadBlock = 1; /** sets this payload's position on the map. */ @@ -22,6 +22,9 @@ public interface Payload{ /** @return hitbox size of the payload. */ float size(); + float x(); + float y(); + /** @return whether this payload was dumped. */ default boolean dump(){ return false; @@ -41,7 +44,17 @@ public interface Payload{ void write(Writes write); /** @return icon describing the contents. */ - TextureRegion icon(Cicon icon); + TextureRegion icon(); + + @Override + default float getX(){ + return x(); + } + + @Override + default float getY(){ + return y(); + } static void write(@Nullable Payload payload, Writes write){ if(payload == null){ diff --git a/core/src/mindustry/world/blocks/payloads/PayloadBlock.java b/core/src/mindustry/world/blocks/payloads/PayloadBlock.java new file mode 100644 index 0000000000..20db57c296 --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/PayloadBlock.java @@ -0,0 +1,233 @@ +package mindustry.world.blocks.payloads; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.world.*; + +import static mindustry.Vars.*; + +public class PayloadBlock extends Block{ + public float payloadSpeed = 0.5f, payloadRotateSpeed = 5f; + + public @Load(value = "@-top", fallback = "factory-top-@size") TextureRegion topRegion; + public @Load(value = "@-out", fallback = "factory-out-@size") TextureRegion outRegion; + public @Load(value = "@-in", fallback = "factory-in-@size") TextureRegion inRegion; + + public PayloadBlock(String name){ + super(name); + + update = true; + sync = true; + } + + public static boolean blends(Building build, int direction){ + int size = build.block.size; + int trns = build.block.size/2 + 1; + Building accept = build.nearby(Geometry.d4(direction).x * trns, Geometry.d4(direction).y * trns); + return accept != null && + accept.block.outputsPayload && + + //if size is the same, block must either be facing this one, or not be rotating + ((accept.block.size == size + && Math.abs(accept.tileX() - build.tileX()) % size == 0 //check alignment + && Math.abs(accept.tileY() - build.tileY()) % size == 0 + && ((accept.block.rotate && accept.tileX() + Geometry.d4(accept.rotation).x * size == build.tileX() && accept.tileY() + Geometry.d4(accept.rotation).y * size == build.tileY()) + || !accept.block.rotate + || !accept.block.outputFacing)) || + + //if the other block is smaller, check alignment + (accept.block.size != size && + (accept.rotation % 2 == 0 ? //check orientation; make sure it's aligned properly with this block. + Math.abs(accept.y - build.y) <= Math.abs(size * tilesize - accept.block.size * tilesize)/2f : //check Y alignment + Math.abs(accept.x - build.x) <= Math.abs(size * tilesize - accept.block.size * tilesize)/2f //check X alignment + )) && (!accept.block.rotate || accept.front() == build || !accept.block.outputFacing) //make sure it's facing this block + ); + } + + public static void pushOutput(Payload payload, float progress){ + float thresh = 0.55f; + if(progress >= thresh){ + boolean legStep = payload instanceof UnitPayload u && u.unit.type.allowLegStep; + float size = payload.size(), radius = size/2f, x = payload.x(), y = payload.y(), scl = Mathf.clamp(((progress - thresh) / (1f - thresh)) * 1.1f); + + Groups.unit.intersect(x - size/2f, y - size/2f, size, size, u -> { + float dst = u.dst(payload); + float rs = radius + u.hitSize/2f; + if(u.isGrounded() && u.type.allowLegStep == legStep && dst < rs){ + u.vel.add(Tmp.v1.set(u.x - x, u.y - y).setLength(Math.min(rs - dst, 1f)).scl(scl)); + } + }); + } + } + + public class PayloadBlockBuild extends Building{ + public @Nullable T payload; + //TODO redundant; already stored in payload? + public Vec2 payVector = new Vec2(); + public float payRotation; + public boolean carried; + + public boolean acceptUnitPayload(Unit unit){ + return false; + } + + @Override + public boolean canControlSelect(Player player){ + return !player.unit().spawnedByCore && this.payload == null && acceptUnitPayload(player.unit()) && player.tileOn().build == this; + } + + @Override + public void onControlSelect(Player player){ + float x = player.x, y = player.y; + acceptPlayerPayload(player, p -> payload = (T)p); + this.payVector.set(x, y).sub(this).clamp(-size * tilesize / 2f, -size * tilesize / 2f, size * tilesize / 2f, size * tilesize / 2f); + this.payRotation = player.unit().rotation; + } + + @Override + public boolean acceptPayload(Building source, Payload payload){ + return this.payload == null; + } + + @Override + public void handlePayload(Building source, Payload payload){ + this.payload = (T)payload; + this.payVector.set(source).sub(this).clamp(-size * tilesize / 2f, -size * tilesize / 2f, size * tilesize / 2f, size * tilesize / 2f); + this.payRotation = payload.rotation(); + + updatePayload(); + } + + @Override + public Payload getPayload(){ + return payload; + } + + @Override + public void pickedUp(){ + carried = true; + } + + @Override + public void drawTeamTop(){ + carried = false; + } + + @Override + public Payload takePayload(){ + T t = payload; + payload = null; + return t; + } + + @Override + public void onRemoved(){ + super.onRemoved(); + if(payload != null && !carried) payload.dump(); + } + + public boolean blends(int direction){ + return PayloadBlock.blends(this, direction); + } + + public void updatePayload(){ + if(payload != null){ + payload.set(x + payVector.x, y + payVector.y, payRotation); + } + } + + /** @return true if the payload is in position. */ + public boolean moveInPayload(){ + return moveInPayload(true); + } + + /** @return true if the payload is in position. */ + public boolean moveInPayload(boolean rotate){ + if(payload == null) return false; + + updatePayload(); + + if(rotate){ + payRotation = Angles.moveToward(payRotation, rotate ? rotdeg() : 90f, payloadRotateSpeed * edelta()); + } + payVector.approach(Vec2.ZERO, payloadSpeed * delta()); + + return hasArrived(); + } + + public void moveOutPayload(){ + if(payload == null) return; + + updatePayload(); + + Vec2 dest = Tmp.v1.trns(rotdeg(), size * tilesize/2f); + + payRotation = Angles.moveToward(payRotation, rotdeg(), payloadRotateSpeed * edelta()); + payVector.approach(dest, payloadSpeed * delta()); + + Building front = front(); + boolean canDump = front == null || !front.tile().solid(); + boolean canMove = front != null && (front.block.outputsPayload || front.block.acceptsPayload); + + if(canDump && !canMove){ + pushOutput(payload, 1f - (payVector.dst(dest) / (size * tilesize / 2f))); + } + + if(payVector.within(dest, 0.001f)){ + payVector.clamp(-size * tilesize / 2f, -size * tilesize / 2f, size * tilesize / 2f, size * tilesize / 2f); + + if(canMove){ + if(movePayload(payload)){ + payload = null; + } + }else if(canDump){ + dumpPayload(); + } + } + } + + public void dumpPayload(){ + if(payload.dump()){ + payload = null; + } + } + + public boolean hasArrived(){ + return payVector.isZero(0.01f); + } + + public void drawPayload(){ + if(payload != null){ + updatePayload(); + + Draw.z(Layer.blockOver); + payload.draw(); + } + } + + @Override + public void write(Writes write){ + super.write(write); + + write.f(payVector.x); + write.f(payVector.y); + write.f(payRotation); + Payload.write(payload, write); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + + payVector.set(read.f(), read.f()); + payRotation = read.f(); + payload = Payload.read(read); + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/PayloadMassDriver.java b/core/src/mindustry/world/blocks/payloads/PayloadMassDriver.java new file mode 100644 index 0000000000..50aacfe31f --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/PayloadMassDriver.java @@ -0,0 +1,481 @@ +package mindustry.world.blocks.payloads; + +import arc.audio.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.struct.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.logic.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; +import static mindustry.world.blocks.payloads.PayloadMassDriver.PayloadDriverState.*; + +public class PayloadMassDriver extends PayloadBlock{ + public float range = 100f; + public float rotateSpeed = 2f; + public float length = 89 / 8f; + public float knockback = 5f; + public float reloadTime = 30f; + public float chargeTime = 100f; + public float maxPayloadSize = 3; + public float grabWidth = 8f, grabHeight = 11/4f; + public Effect shootEffect = Fx.shootBig2; + public Effect smokeEffect = Fx.shootPayloadDriver; + public Effect receiveEffect = Fx.payloadReceive; + public Sound shootSound = Sounds.shootBig; + public float shake = 3f; + + public Effect transferEffect = new Effect(11f, 300f, e -> { + if(!(e.data instanceof PayloadMassDriverData data)) return; + Tmp.v1.set(data.x, data.y).lerp(data.ox, data.oy, Interp.sineIn.apply(e.fin())); + data.payload.set(Tmp.v1.x, Tmp.v1.y, e.rotation); + data.payload.draw(); + }).layer(Layer.flyingUnitLow - 1); + + public @Load("@-base") TextureRegion baseRegion; + public @Load("@-cap") TextureRegion capRegion; + public @Load("@-left") TextureRegion leftRegion; + public @Load("@-right") TextureRegion rightRegion; + public @Load("@-cap-outline") TextureRegion capOutlineRegion; + public @Load("@-left-outline") TextureRegion leftOutlineRegion; + public @Load("@-right-outline") TextureRegion rightOutlineRegion; + public @Load("bridge-arrow") TextureRegion arrow; + + public PayloadMassDriver(String name){ + super(name); + update = true; + solid = true; + configurable = true; + hasPower = true; + outlineIcon = true; + sync = true; + rotate = true; + outputsPayload = true; + + //point2 is relative + config(Point2.class, (PayloadDriverBuild tile, Point2 point) -> tile.link = Point2.pack(point.x + tile.tileX(), point.y + tile.tileY())); + config(Integer.class, (PayloadDriverBuild tile, Integer point) -> tile.link = point); + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.payloadCapacity, maxPayloadSize, StatUnit.blocksSquared); + stats.add(Stat.reload, 60f / (chargeTime + reloadTime), StatUnit.seconds); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{baseRegion, outRegion, region}; + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(baseRegion, req.drawx(), req.drawy()); + Draw.rect(topRegion, req.drawx(), req.drawy()); + Draw.rect(outRegion, req.drawx(), req.drawy(), req.rotation * 90); + Draw.rect(region, req.drawx(), req.drawy()); + } + + @Override + public void drawPlace(int x, int y, int rotation, boolean valid){ + super.drawPlace(x, y, rotation, valid); + + Drawf.dashCircle(x * tilesize, y * tilesize, range, Pal.accent); + + //check if a mass driver is selected while placing this driver + if(!control.input.frag.config.isShown()) return; + Building selected = control.input.frag.config.getSelectedTile(); + if(selected == null || selected.block != this || !selected.within(x * tilesize, y * tilesize, range)) return; + + //if so, draw a dotted line towards it while it is in range + float sin = Mathf.absin(Time.time, 6f, 1f); + Tmp.v1.set(x * tilesize + offset, y * tilesize + offset).sub(selected.x, selected.y).limit((size / 2f + 1) * tilesize + sin + 0.5f); + float x2 = x * tilesize - Tmp.v1.x, y2 = y * tilesize - Tmp.v1.y, + x1 = selected.x + Tmp.v1.x, y1 = selected.y + Tmp.v1.y; + int segs = (int)(selected.dst(x * tilesize, y * tilesize) / tilesize); + + Lines.stroke(4f, Pal.gray); + Lines.dashLine(x1, y1, x2, y2, segs); + Lines.stroke(2f, Pal.placing); + Lines.dashLine(x1, y1, x2, y2, segs); + Draw.reset(); + } + + @Override + public TextureRegion[] makeIconRegions(){ + return new TextureRegion[]{leftRegion, rightRegion, capRegion}; + } + + public class PayloadDriverBuild extends PayloadBlockBuild{ + public int link = -1; + public float turretRotation = 90; + public float reload = 0f, charge = 0f; + public float targetSize = grabWidth*2f, curSize = targetSize; + public float payLength = 0f; + public boolean loaded; + public boolean charging; + public PayloadDriverState state = idle; + public Queue waitingShooters = new Queue<>(); + public Payload recPayload; + + public Building currentShooter(){ + return waitingShooters.isEmpty() ? null : waitingShooters.first(); + } + + @Override + public void updateTile(){ + Building link = world.build(this.link); + boolean hasLink = linkValid(); + + //discharge when charging isn't happening + if(!charging){ + charge -= Time.delta * 10f; + if(charge < 0) charge = 0f; + } + + curSize = Mathf.lerpDelta(curSize, targetSize, 0.05f); + targetSize = grabWidth*2f; + + if(payload != null){ + targetSize = payload.size(); + } + + charging = false; + + if(hasLink){ + this.link = link.pos(); + } + + //reload regardless of state + reload -= edelta() / reloadTime; + if(reload < 0) reload = 0f; + + var current = currentShooter(); + + //cleanup waiting shooters that are not valid + if(current != null && + !( + current instanceof PayloadDriverBuild entity && + entity.consValid() && entity.block == block && + entity.link == pos() && within(current, range) + )){ + waitingShooters.removeFirst(); + } + + //switch states + if(state == idle){ + //start accepting when idle and there's space + if(!waitingShooters.isEmpty() && payload == null){ + state = accepting; + }else if(hasLink){ //switch to shooting if there's a valid link. + state = shooting; + } + } + + //dump when idle or accepting + if((state == idle || state == accepting) && payload != null){ + if(loaded){ + payLength -= payloadSpeed * delta(); + if(payLength <= 0f){ + loaded = false; + payVector.setZero(); + payRotation = Angles.moveToward(payRotation, turretRotation + 180f, payloadRotateSpeed * delta()); + } + }else{ + moveOutPayload(); + } + } + + //skip when there's no power + if(!consValid()){ + return; + } + + if(state == accepting){ + //if there's nothing shooting at this or items are full, bail out + if(currentShooter() == null || payload != null){ + state = idle; + return; + } + + if(currentShooter().getPayload() != null){ + targetSize = recPayload == null ? currentShooter().getPayload().size() : recPayload.size(); + } + + //align to shooter rotation + turretRotation = Angles.moveToward(turretRotation, tile.angleTo(currentShooter()), rotateSpeed * efficiency()); + }else if(state == shooting){ + //if there's nothing to shoot at OR someone wants to shoot at this thing, bail + if(!hasLink || (!waitingShooters.isEmpty() && payload == null)){ + state = idle; + return; + } + + float targetRotation = tile.angleTo(link); + boolean movedOut = false; + + payRotation = Angles.moveToward(payRotation, turretRotation, payloadRotateSpeed * delta()); + if(loaded){ + float loadLength = length - reload*knockback; + payLength += payloadSpeed * delta(); + if(payLength >= loadLength){ + payLength = loadLength; + movedOut = true; + } + }else if(moveInPayload()){ + payLength = 0f; + loaded = true; + } + + //make sure payload firing can happen + if(movedOut && payload != null && link.getPayload() == null){ + var other = (PayloadDriverBuild)link; + + if(!other.waitingShooters.contains(this)){ + other.waitingShooters.addLast(this); + } + + if(reload <= 0){ + //align to target location + turretRotation = Angles.moveToward(turretRotation, targetRotation, rotateSpeed * efficiency()); + + //fire when it's the first in the queue and angles are ready. + if(other.currentShooter() == this && + other.state == accepting && + other.reload <= 0f && + Angles.within(turretRotation, targetRotation, 1f) && Angles.within(other.turretRotation, targetRotation + 180f, 1f)){ + charge += edelta(); + charging = true; + + if(charge >= chargeTime){ + float cx = Angles.trnsx(turretRotation, length), cy = Angles.trnsy(turretRotation, length); + + //effects + shootEffect.at(x + cx, y + cy, turretRotation); + smokeEffect.at(x, y, turretRotation); + + Effect.shake(shake, shake, this); + shootSound.at(this, Mathf.random(0.9f, 1.1f)); + transferEffect.at(x + cx, y + cy, turretRotation, new PayloadMassDriverData(x + cx, y + cy, other.x - cx, other.y - cy, payload)); + Payload pay = payload; + other.recPayload = payload; + + Time.run(transferEffect.lifetime, () -> { + receiveEffect.at(other.x - cx/2f, other.y - cy/2f, other.turretRotation); + Effect.shake(shake, shake, this); + + //transfer payload + other.reload = 1f; + other.handlePayload(this, pay); + other.payVector.set(-cx, -cy); + other.payRotation = turretRotation; + other.payLength = length; + other.loaded = true; + other.updatePayload(); + other.recPayload = null; + + if(other.waitingShooters.size != 0 && other.waitingShooters.first() == this){ + other.waitingShooters.removeFirst(); + } + other.state = idle; + }); + + //reset state after shooting immediately + payload = null; + payLength = 0f; + loaded = false; + state = idle; + reload = 1f; + } + } + } + } + } + } + + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(1f - reload / reloadTime); + return super.sense(sensor); + } + + @Override + public void updatePayload(){ + if(payload != null){ + if(loaded){ + payload.set(x + Angles.trnsx(turretRotation, payLength), y + Angles.trnsy(turretRotation, payLength), payRotation); + }else{ + payload.set(x + payVector.x, y + payVector.y, payRotation); + } + } + } + + @Override + public void draw(){ + float + tx = x + Angles.trnsx(turretRotation + 180f, reload * knockback), + ty = y + Angles.trnsy(turretRotation + 180f, reload * knockback), r = turretRotation - 90; + + Draw.rect(baseRegion, x, y); + + //draw input + for(int i = 0; i < 4; i++){ + if(blends(i) && i != rotation){ + Draw.rect(inRegion, x, y, (i * 90) - 180); + } + } + + Draw.rect(outRegion, x, y, rotdeg()); + + if(payload != null){ + updatePayload(); + + Draw.z(loaded ? Layer.blockOver + 0.2f : Layer.blockOver); + payload.draw(); + } + + Draw.z(Layer.blockOver + 0.1f); + Draw.rect(topRegion, x, y); + + Draw.z(Layer.turret); + //TODO + Drawf.shadow(region, tx - (size / 2f), ty - (size / 2f), r); + + Tmp.v1.trns(turretRotation, 0, -(curSize/2f - grabWidth)); + Tmp.v2.trns(rotation, -Math.max(curSize/2f - grabHeight - length, 0f), 0f); + float rx = tx + Tmp.v1.x + Tmp.v2.x, ry = ty + Tmp.v1.y + Tmp.v2.y; + float lx = tx - Tmp.v1.x + Tmp.v2.x, ly = ty - Tmp.v1.y + Tmp.v2.y; + + Draw.rect(capOutlineRegion, tx, ty, r); + Draw.rect(leftOutlineRegion, lx, ly, r); + Draw.rect(rightOutlineRegion, rx, ry, r); + + Draw.rect(leftRegion, lx, ly, r); + Draw.rect(rightRegion, rx, ry, r); + Draw.rect(capRegion, tx, ty, r); + + Draw.z(Layer.effect); + + if(charge > 0 && linkValid()){ + Building link = world.build(this.link); + + float fin = Interp.pow2Out.apply(charge / chargeTime), fout = 1f-fin, len = length*1.8f, w = curSize/2f + 7f*fout; + Vec2 right = Tmp.v1.trns(turretRotation, len, w); + Vec2 left = Tmp.v2.trns(turretRotation, len, -w); + + Lines.stroke(fin * 1.2f, Pal.accent); + Lines.line(x + left.x, y + left.y, link.x - right.x, link.y - right.y); + Lines.line(x + right.x, y + right.y, link.x - left.x, link.y - left.y); + + for(int i = 0; i < 4; i++){ + Tmp.v3.set(x, y).lerp(link.x, link.y, 0.5f + (i - 2) * 0.1f); + Draw.scl(fin * 1.1f); + Draw.rect(arrow, Tmp.v3.x, Tmp.v3.y, turretRotation); + Draw.scl(); + } + + Draw.reset(); + } + } + + @Override + public void drawConfigure(){ + float sin = Mathf.absin(Time.time, 6f, 1f); + + Draw.color(Pal.accent); + Lines.stroke(1f); + Drawf.circles(x, y, (tile.block().size / 2f + 1) * tilesize + sin - 2f, Pal.accent); + + for(var shooter : waitingShooters){ + Drawf.circles(shooter.x, shooter.y, (tile.block().size / 2f + 1) * tilesize + sin - 2f, Pal.place); + Drawf.arrow(shooter.x, shooter.y, x, y, size * tilesize + sin, 4f + sin, Pal.place); + } + + if(linkValid()){ + Building target = world.build(link); + Drawf.circles(target.x, target.y, (target.block().size / 2f + 1) * tilesize + sin - 2f, Pal.place); + Drawf.arrow(x, y, target.x, target.y, size * tilesize + sin, 4f + sin); + } + + Drawf.dashCircle(x, y, range, Pal.accent); + } + + @Override + public boolean onConfigureTileTapped(Building other){ + if(this == other){ + configure(-1); + return false; + } + + if(link == other.pos()){ + configure(-1); + return false; + }else if(other.block instanceof PayloadMassDriver && other.dst(tile) <= range && other.team == team){ + configure(other.pos()); + return false; + } + + return true; + } + + @Override + public boolean acceptPayload(Building source, Payload payload){ + return super.acceptPayload(source, payload) && payload.size() <= maxPayloadSize * tilesize; + } + + protected boolean linkValid(){ + return link != -1 && world.build(this.link) instanceof PayloadDriverBuild other && other.block == block && other.team == team && within(other, range); + } + + @Override + public Point2 config(){ + return Point2.unpack(link).sub(tile.x, tile.y); + } + + @Override + public void write(Writes write){ + super.write(write); + write.i(link); + write.f(turretRotation); + write.b((byte)state.ordinal()); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + link = read.i(); + turretRotation = read.f(); + state = PayloadDriverState.all[read.b()]; + } + } + + public static class PayloadMassDriverData{ + public float x, y, ox, oy; + public Payload payload; + + public PayloadMassDriverData(float x, float y, float ox, float oy, Payload payload){ + this.x = x; + this.y = y; + this.ox = ox; + this.oy = oy; + this.payload = payload; + } + } + + public enum PayloadDriverState{ + idle, accepting, shooting; + + public static final PayloadDriverState[] all = values(); + } +} diff --git a/core/src/mindustry/world/blocks/payloads/PayloadSource.java b/core/src/mindustry/world/blocks/payloads/PayloadSource.java new file mode 100644 index 0000000000..aa6a5c9a6c --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/PayloadSource.java @@ -0,0 +1,135 @@ +package mindustry.world.blocks.payloads; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.scene.ui.layout.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.*; +import mindustry.ctype.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.blocks.*; + +import static mindustry.Vars.*; + +/** Generic building that produces other buildings. */ +public class PayloadSource extends PayloadBlock{ + + public PayloadSource(String name){ + super(name); + + size = 3; + update = true; + outputsPayload = true; + hasPower = true; + rotate = true; + configurable = true; + + config(Block.class, (PayloadSourceBuild build, Block block) -> { + if(canProduce(block) && build.block != block){ + build.block = block; + build.unit = null; + build.payload = null; + build.scl = 0f; + } + }); + + config(UnitType.class, (PayloadSourceBuild build, UnitType unit) -> { + if(canProduce(unit) && build.unit != unit){ + build.unit = unit; + build.block = null; + build.payload = null; + build.scl = 0f; + } + }); + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, outRegion, topRegion}; + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(region, req.drawx(), req.drawy()); + Draw.rect(outRegion, req.drawx(), req.drawy(), req.rotation * 90); + Draw.rect(topRegion, req.drawx(), req.drawy()); + } + + public boolean canProduce(Block b){ + return b.isVisible() && b.size < size; + } + + public boolean canProduce(UnitType t){ + return !t.isHidden(); + } + + public class PayloadSourceBuild extends PayloadBlockBuild{ + public UnitType unit; + public Block block; + public float scl; + + @Override + public void buildConfiguration(Table table){ + ItemSelection.buildTable(table, + content.blocks().select(PayloadSource.this::canProduce).as() + .and(content.units().select(PayloadSource.this::canProduce).as()), + () -> (UnlockableContent)config(), this::configure); + } + + @Override + public Object config(){ + return unit == null ? block : unit; + } + + @Override + public boolean acceptPayload(Building source, Payload payload){ + return false; + } + + @Override + public void updateTile(){ + if(payload == null){ + scl = 0f; + if(unit != null){ + payload = new UnitPayload(unit.create(team)); + }else if(block != null){ + payload = new BuildPayload(block, team); + } + payVector.setZero(); + payRotation = rotdeg(); + } + scl = Mathf.lerpDelta(scl, 1f, 0.1f); + + moveOutPayload(); + } + + @Override + public void draw(){ + Draw.rect(region, x, y); + Draw.rect(outRegion, x, y, rotdeg()); + Draw.rect(topRegion, x, y); + + Draw.scl(scl); + drawPayload(); + Draw.reset(); + } + + @Override + public void write(Writes write){ + super.write(write); + write.s(unit == null ? -1 : unit.id); + write.s(block == null ? -1 : block.id); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + unit = Vars.content.unit(read.s()); + block = Vars.content.block(read.s()); + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/PayloadVoid.java b/core/src/mindustry/world/blocks/payloads/PayloadVoid.java new file mode 100644 index 0000000000..434753f0ee --- /dev/null +++ b/core/src/mindustry/world/blocks/payloads/PayloadVoid.java @@ -0,0 +1,57 @@ +package mindustry.world.blocks.payloads; + +import arc.audio.*; +import arc.graphics.g2d.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.gen.*; +import mindustry.graphics.*; + +public class PayloadVoid extends PayloadBlock{ + public Effect incinerateEffect = Fx.blastExplosion; + public Sound incinerateSound = Sounds.bang; + + public PayloadVoid(String name){ + super(name); + + outputsPayload = false; + acceptsPayload = true; + update = true; + rotate = false; + size = 3; + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, topRegion}; + } + + public class BlockLoaderBuild extends PayloadBlockBuild{ + + @Override + public void draw(){ + Draw.rect(region, x, y); + + //draw input + for(int i = 0; i < 4; i++){ + if(blends(i)){ + Draw.rect(inRegion, x, y, (i * 90) - 180); + } + } + + Draw.rect(topRegion, x, y); + + Draw.z(Layer.blockOver); + drawPayload(); + } + + @Override + public void updateTile(){ + if(moveInPayload(false) && cons.valid()){ + payload = null; + incinerateEffect.at(this); + incinerateSound.at(this); + } + } + } +} diff --git a/core/src/mindustry/world/blocks/payloads/UnitPayload.java b/core/src/mindustry/world/blocks/payloads/UnitPayload.java index ee090b0a68..2300dba10b 100644 --- a/core/src/mindustry/world/blocks/payloads/UnitPayload.java +++ b/core/src/mindustry/world/blocks/payloads/UnitPayload.java @@ -12,8 +12,6 @@ import mindustry.entities.EntityCollisions.*; import mindustry.entities.*; import mindustry.game.EventType.*; import mindustry.gen.*; -import mindustry.graphics.*; -import mindustry.ui.*; public class UnitPayload implements Payload{ public static final float deactiveDuration = 40f; @@ -38,6 +36,16 @@ public class UnitPayload implements Payload{ unit.rotation = rotation; } + @Override + public float x(){ + return unit.x; + } + + @Override + public float y(){ + return unit.y; + } + @Override public float rotation(){ return unit.rotation; @@ -71,6 +79,11 @@ public class UnitPayload implements Payload{ if(!nearEmpty) return false; } + //cannnot dump when there's a lot of overlap going on + if(!unit.type.flying && Units.count(unit.x, unit.y, unit.physicSize(), o -> o.isGrounded() && (o.type.allowLegStep == unit.type.allowLegStep)) > 0){ + return false; + } + //no client dumping if(Vars.net.client()) return true; @@ -87,8 +100,8 @@ public class UnitPayload implements Payload{ //TODO should not happen if(unit.type == null) return; - Drawf.shadow(unit.x, unit.y, 20); - Draw.rect(unit.type.icon(Cicon.full), unit.x, unit.y, unit.rotation - 90); + unit.type.drawSoftShadow(unit); + Draw.rect(unit.type.fullIcon, unit.x, unit.y, unit.rotation - 90); unit.type.drawCell(unit); //draw warning @@ -106,7 +119,7 @@ public class UnitPayload implements Payload{ } @Override - public TextureRegion icon(Cicon icon){ - return unit.type.icon(icon); + public TextureRegion icon(){ + return unit.type.fullIcon; } } diff --git a/core/src/mindustry/world/blocks/power/LightBlock.java b/core/src/mindustry/world/blocks/power/LightBlock.java index 8f035689b9..8a88a8e017 100644 --- a/core/src/mindustry/world/blocks/power/LightBlock.java +++ b/core/src/mindustry/world/blocks/power/LightBlock.java @@ -29,6 +29,13 @@ public class LightBlock extends Block{ config(Integer.class, (LightBuild tile, Integer value) -> tile.color = value); } + @Override + public void init(){ + lightRadius = radius; + emitLight = true; + super.init(); + } + public class LightBuild extends Building{ public int color = Pal.accent.rgba(); public float smoothTime = 1f; @@ -67,7 +74,7 @@ public class LightBlock extends Block{ @Override public void drawLight(){ - Drawf.light(team, x, y, radius * Math.min(smoothTime, 2f), Tmp.c1.set(color), brightness * efficiency()); + Drawf.light(team, x, y, lightRadius * Math.min(smoothTime, 2f), Tmp.c1.set(color), brightness * efficiency()); } @Override diff --git a/core/src/mindustry/world/blocks/power/NuclearReactor.java b/core/src/mindustry/world/blocks/power/NuclearReactor.java index 2a53e1b4c0..6ed9f11189 100644 --- a/core/src/mindustry/world/blocks/power/NuclearReactor.java +++ b/core/src/mindustry/world/blocks/power/NuclearReactor.java @@ -30,6 +30,7 @@ public class NuclearReactor extends PowerGenerator{ public Color lightColor = Color.valueOf("7f19ea"); public Color coolColor = new Color(1, 1, 1, 0f); public Color hotColor = Color.valueOf("ff9575a3"); + public Effect explodeEffect = Fx.reactorExplosion; /** ticks to consume 1 fuel */ public float itemDuration = 120; /** heating per frame * fullness */ @@ -135,26 +136,9 @@ public class NuclearReactor extends PowerGenerator{ if((fuel < 5 && heat < 0.5f) || !state.rules.reactorExplosions) return; Effect.shake(6f, 16f, x, y); - Fx.nuclearShockwave.at(x, y); - for(int i = 0; i < 6; i++){ - Time.run(Mathf.random(40), () -> Fx.nuclearcloud.at(x, y)); - } - Damage.damage(x, y, explosionRadius * tilesize, explosionDamage * 4); - for(int i = 0; i < 20; i++){ - Time.run(Mathf.random(50), () -> { - tr.rnd(Mathf.random(40f)); - Fx.explosion.at(tr.x + x, tr.y + y); - }); - } - - for(int i = 0; i < 70; i++){ - Time.run(Mathf.random(80), () -> { - tr.rnd(Mathf.random(120f)); - Fx.nuclearsmoke.at(tr.x + x, tr.y + y); - }); - } + explodeEffect.at(x, y); } @Override diff --git a/core/src/mindustry/world/blocks/power/PowerDiode.java b/core/src/mindustry/world/blocks/power/PowerDiode.java index 6e2052250e..422c978aef 100644 --- a/core/src/mindustry/world/blocks/power/PowerDiode.java +++ b/core/src/mindustry/world/blocks/power/PowerDiode.java @@ -35,7 +35,7 @@ public class PowerDiode extends Block{ @Override public void drawRequestRegion(BuildPlan req, Eachable list){ - Draw.rect(icon(Cicon.full), req.drawx(), req.drawy()); + Draw.rect(fullIcon, req.drawx(), req.drawy()); Draw.rect(arrow, req.drawx(), req.drawy(), !rotate ? 0 : req.rotation * 90); } diff --git a/core/src/mindustry/world/blocks/production/AttributeCrafter.java b/core/src/mindustry/world/blocks/production/AttributeCrafter.java new file mode 100644 index 0000000000..5312c3d4b3 --- /dev/null +++ b/core/src/mindustry/world/blocks/production/AttributeCrafter.java @@ -0,0 +1,58 @@ +package mindustry.world.blocks.production; + +import arc.*; +import mindustry.graphics.*; +import mindustry.ui.*; +import mindustry.world.meta.*; + +/** A crafter that gains efficiency from attribute tiles. */ +public class AttributeCrafter extends GenericCrafter{ + public Attribute attribute = Attribute.heat; + public float baseEfficiency = 1f; + public float boostScale = 1f; + public float maxBoost = 1f; + + public AttributeCrafter(String name){ + super(name); + } + + @Override + public void drawPlace(int x, int y, int rotation, boolean valid){ + drawPlaceText(Core.bundle.format("bar.efficiency", + (int)((baseEfficiency + Math.min(maxBoost, boostScale * sumAttribute(attribute, x, y))) * 100f)), x, y, valid); + } + + @Override + public void setBars(){ + super.setBars(); + + bars.add("efficiency", entity -> + new Bar(() -> + Core.bundle.format("bar.efficiency", (int)(entity.efficiency() * 100)), + () -> Pal.lightOrange, + entity::efficiency)); + } + + @Override + public void setStats(){ + super.setStats(); + + stats.add(Stat.affinities, attribute, boostScale); + } + + public class AttributeCrafterBuild extends GenericCrafterBuild{ + public float attrsum; + + @Override + public float efficiency(){ + return (baseEfficiency + Math.min(maxBoost, boostScale * attrsum)) * super.efficiency(); + } + + @Override + public void onProximityUpdate(){ + super.onProximityUpdate(); + + attrsum = sumAttribute(attribute, tile.x, tile.y); + } + } +} diff --git a/core/src/mindustry/world/blocks/production/AttributeSmelter.java b/core/src/mindustry/world/blocks/production/AttributeSmelter.java index c2650ab030..edece33611 100644 --- a/core/src/mindustry/world/blocks/production/AttributeSmelter.java +++ b/core/src/mindustry/world/blocks/production/AttributeSmelter.java @@ -1,58 +1,25 @@ package mindustry.world.blocks.production; -import arc.*; -import mindustry.graphics.*; -import mindustry.ui.*; -import mindustry.world.meta.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import mindustry.annotations.Annotations.*; -/** A smelter that gains efficiency from attribute tiles. */ -public class AttributeSmelter extends GenericSmelter{ - public Attribute attribute = Attribute.heat; - public float baseEfficiency = 1f; - public float boostScale = 1f; +/** @deprecated use AttributeCrafter instead, this is only a transition class. No flame effects are drawn, to encourage transition! */ +@Deprecated +public class AttributeSmelter extends AttributeCrafter{ + //parameters are kept for compatibility but deliberately unused + public Color flameColor = Color.valueOf("ffc999"); + public @Load("@-top") TextureRegion topRegion; + //compat public float maxHeatBoost = 1f; public AttributeSmelter(String name){ super(name); } - @Override - public void drawPlace(int x, int y, int rotation, boolean valid){ - drawPlaceText(Core.bundle.format("bar.efficiency", - (int)((baseEfficiency + Math.min(maxHeatBoost, boostScale * sumAttribute(attribute, x, y))) * 100f)), x, y, valid); - } + //unused, kept for compatibility + @Deprecated + public class AttributeSmelterBuild extends AttributeCrafterBuild{ - @Override - public void setBars(){ - super.setBars(); - - bars.add("efficiency", entity -> - new Bar(() -> - Core.bundle.format("bar.efficiency", (int)(entity.efficiency() * 100)), - () -> Pal.lightOrange, - entity::efficiency)); - } - - @Override - public void setStats(){ - super.setStats(); - - stats.add(Stat.affinities, attribute, boostScale); - } - - public class AttributeSmelterBuild extends SmelterBuild{ - public float attrsum; - - @Override - public float efficiency(){ - return (baseEfficiency + Math.min(maxHeatBoost, boostScale * attrsum)) * super.efficiency(); - } - - @Override - public void onProximityUpdate(){ - super.onProximityUpdate(); - - attrsum = sumAttribute(attribute, tile.x, tile.y); - } } } diff --git a/core/src/mindustry/world/blocks/production/BeamDrill.java b/core/src/mindustry/world/blocks/production/BeamDrill.java new file mode 100644 index 0000000000..32ffdc90d2 --- /dev/null +++ b/core/src/mindustry/world/blocks/production/BeamDrill.java @@ -0,0 +1,254 @@ +package mindustry.world.blocks.production; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.type.*; +import mindustry.world.*; + +import static mindustry.Vars.*; + +public class BeamDrill extends Block{ + public @Load("minelaser") TextureRegion laser; + public @Load("minelaser-end") TextureRegion laserEnd; + public @Load("@-top") TextureRegion topRegion; + + public float drillTime = 200f; + public int range = 5; + public int tier = 1; + public float laserWidth = 0.7f; + /** Effect randomly played while drilling. */ + public Effect updateEffect = Fx.mineSmall; + + public BeamDrill(String name){ + super(name); + + hasItems = true; + rotate = true; + update = true; + solid = true; + drawArrow = false; + } + + @Override + public void init(){ + clipSize = Math.max(clipSize, size * tilesize + (range + 1) * tilesize); + super.init(); + } + + @Override + public boolean outputsItems(){ + return true; + } + + @Override + public boolean rotatedOutput(int x, int y){ + return false; + } + + @Override + public TextureRegion[] icons(){ + return new TextureRegion[]{region, topRegion}; + } + + @Override + public void drawRequestRegion(BuildPlan req, Eachable list){ + Draw.rect(region, req.drawx(), req.drawy()); + Draw.rect(topRegion, req.drawx(), req.drawy(), req.rotation * 90); + } + + @Override + public void drawPlace(int x, int y, int rotation, boolean valid){ + Item item = null; + boolean multiple = false; + int count = 0; + + for(int i = 0; i < size; i++){ + getLaserPos(x, y, rotation, i, Tmp.p1); + + int j = 0; + Item found = null; + for(; j < range; j++){ + int rx = Tmp.p1.x + Geometry.d4x(rotation)*j, ry = Tmp.p1.y + Geometry.d4y(rotation)*j; + Tile other = world.tile(rx, ry); + if(other != null){ + if(other.solid()){ + Item drop = other.wallDrop(); + if(drop != null && drop.hardness <= tier){ + found = drop; + count ++; + } + break; + } + } + } + + if(found != null){ + //check if multiple items will be drilled + if(item != found && item != null){ + multiple = true; + } + item = found; + } + + int len = Math.min(j, range - 1); + Drawf.dashLine(found == null ? Pal.remove : Pal.placing, + Tmp.p1.x * tilesize, + Tmp.p1.y *tilesize, + (Tmp.p1.x + Geometry.d4x(rotation)*len) * tilesize, + (Tmp.p1.y + Geometry.d4y(rotation)*len) * tilesize + ); + } + + if(item != null){ + float width = drawPlaceText(Core.bundle.formatFloat("bar.drillspeed", 60f / drillTime * count, 2), x, y, valid); + if(!multiple){ + float dx = x * tilesize + offset - width/2f - 4f, dy = y * tilesize + offset + size * tilesize / 2f + 5, s = iconSmall / 4f; + Draw.mixcol(Color.darkGray, 1f); + Draw.rect(item.fullIcon, dx, dy - 1, s, s); + Draw.reset(); + Draw.rect(item.fullIcon, dx, dy, s, s); + } + } + + } + + void getLaserPos(int tx, int ty, int rotation, int i, Point2 out){ + int cornerX = tx - (size-1)/2, cornerY = ty - (size-1)/2, s = size; + switch(rotation){ + case 0 -> out.set(cornerX + s, cornerY + i); + case 1 -> out.set(cornerX + i, cornerY + s); + case 2 -> out.set(cornerX - 1, cornerY + i); + case 3 -> out.set(cornerX + i, cornerY - 1); + } + } + + public class BeamDrillBuild extends Building{ + public Tile[] facing = new Tile[size]; + public Point2[] lasers = new Point2[size]; + public @Nullable Item lastItem; + + public float time; + public float warmup; + + @Override + public void drawSelect(){ + + if(lastItem != null){ + float dx = x - size * tilesize/2f, dy = y + size * tilesize/2f, s = iconSmall / 4f; + Draw.mixcol(Color.darkGray, 1f); + Draw.rect(lastItem.fullIcon, dx, dy - 1, s, s); + Draw.reset(); + Draw.rect(lastItem.fullIcon, dx, dy, s, s); + } + } + + @Override + public void updateTile(){ + super.updateTile(); + + if(lasers[0] == null) updateLasers(); + boolean cons = shouldConsume(); + + warmup = Mathf.lerpDelta(warmup, Mathf.num(consValid()), 0.1f); + lastItem = null; + boolean multiple = false; + + //update facing tiles + for(int p = 0; p < size; p++){ + Point2 l = lasers[p]; + Tile dest = null; + for(int i = 0; i < range; i++){ + int rx = l.x + Geometry.d4x(rotation)*i, ry = l.y + Geometry.d4y(rotation)*i; + Tile other = world.tile(rx, ry); + if(other != null){ + if(other.solid()){ + Item drop = other.wallDrop(); + if(drop != null && drop.hardness <= tier){ + if(lastItem != drop && lastItem != null){ + multiple = true; + } + lastItem = drop; + dest = other; + } + break; + } + } + } + + facing[p] = dest; + if(cons && dest != null && Mathf.chanceDelta(0.05 * warmup)){ + updateEffect.at(dest.worldx() + Mathf.range(4f), dest.worldy() + Mathf.range(4f), dest.wallDrop().color); + } + } + + //when multiple items are present, count that as no item + if(multiple){ + lastItem = null; + } + + time += edelta(); + + if(time >= drillTime){ + for(Tile tile : facing){ + Item drop = tile == null ? null : tile.wallDrop(); + if(items.total() < itemCapacity && drop != null){ + items.add(drop, 1); + } + } + time %= drillTime; + } + + if(timer(timerDump, dumpTime)){ + dump(); + } + } + + @Override + public boolean shouldConsume(){ + return items.total() < itemCapacity; + } + + @Override + public void draw(){ + Draw.rect(block.region, x, y); + Draw.rect(topRegion, x, y, rotdeg()); + + Draw.z(Layer.power - 1); + var dir = Geometry.d4(rotation); + + for(int i = 0; i < size; i++){ + Tile face = facing[i]; + if(face != null){ + Point2 p = lasers[i]; + Drawf.laser(team, laser, laserEnd, (p.x - dir.x/2f) * tilesize, (p.y - dir.y/2f) * tilesize, + face.worldx() - (dir.x/2f)*(tilesize), face.worldy() - (dir.y/2f)*(tilesize), + (laserWidth + Mathf.absin(Time.time + i*4 + (id%20)*6, 3f, 0.07f)) * warmup); + } + } + Draw.reset(); + } + + @Override + public void onProximityUpdate(){ + //when rotated. + updateLasers(); + } + + void updateLasers(){ + for(int i = 0; i < size; i++){ + if(lasers[i] == null) lasers[i] = new Point2(); + getLaserPos(tileX(), tileY(), rotation, i, lasers[i]); + } + } + } +} diff --git a/core/src/mindustry/world/blocks/production/Cultivator.java b/core/src/mindustry/world/blocks/production/Cultivator.java index 57f79a98a0..4e51ae072b 100644 --- a/core/src/mindustry/world/blocks/production/Cultivator.java +++ b/core/src/mindustry/world/blocks/production/Cultivator.java @@ -1,18 +1,20 @@ package mindustry.world.blocks.production; -import arc.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; -import arc.util.*; import arc.util.io.*; import mindustry.annotations.Annotations.*; -import mindustry.content.*; -import mindustry.graphics.*; -import mindustry.ui.*; import mindustry.world.meta.*; +/** + * @deprecated use GenericCrafter or AttributeCrafter with a DrawCultivator instead. + * WARNING: If you switch from a class that used Cultivator to a GenericCrafter, make sure you set legacyReadWarmup to true! Failing to do so will break saves. + * This class has been gutted of its behavior. + * */ +@Deprecated public class Cultivator extends GenericCrafter{ + //fields are kept for compatibility public Color plantColor = Color.valueOf("5541b1"); public Color plantColorLight = Color.valueOf("7457ce"); public Color bottomColor = Color.valueOf("474747"); @@ -25,84 +27,11 @@ public class Cultivator extends GenericCrafter{ public Cultivator(String name){ super(name); - craftEffect = Fx.none; - } - - @Override - public void setBars(){ - super.setBars(); - bars.add("multiplier", (CultivatorBuild entity) -> new Bar(() -> - Core.bundle.formatFloat("bar.efficiency", - ((entity.boost + 1f + attribute.env()) * entity.warmup) * 100f, 1), - () -> Pal.ammo, - () -> entity.warmup)); - } - - @Override - public void setStats(){ - super.setStats(); - - stats.add(Stat.affinities, attribute); - } - - @Override - public void drawPlace(int x, int y, int rotation, boolean valid){ - super.drawPlace(x, y, rotation, valid); - - drawPlaceText(Core.bundle.formatFloat("bar.efficiency", (1 + sumAttribute(attribute, x, y)) * 100, 1), x, y, valid); - } - - @Override - public TextureRegion[] icons(){ - return new TextureRegion[]{region, topRegion}; } public class CultivatorBuild extends GenericCrafterBuild{ - public float warmup; - public float boost; - - @Override - public void updateTile(){ - super.updateTile(); - - warmup = Mathf.lerpDelta(warmup, consValid() ? 1f : 0f, 0.015f); - } - - @Override - public void draw(){ - Draw.rect(region, x, y); - - Drawf.liquid(middleRegion, x, y, warmup, plantColor); - - Draw.color(bottomColor, plantColorLight, warmup); - - random.setSeed(tile.pos()); - for(int i = 0; i < 12; i++){ - float offset = random.nextFloat() * 999999f; - float x = random.range(4f), y = random.range(4f); - float life = 1f - (((Time.time + offset) / 50f) % recurrence); - - if(life > 0){ - Lines.stroke(warmup * (life + 0.2f)); - Lines.poly(x + x, y + y, 8, (1f - life) * 3f); - } - } - - Draw.color(); - Draw.rect(topRegion, x, y); - } - - @Override - public void onProximityUpdate(){ - super.onProximityAdded(); - - boost = sumAttribute(attribute, tile.x, tile.y); - } - - @Override - public float getProgressIncrease(float baseTime){ - return super.getProgressIncrease(baseTime) * (1f + boost + attribute.env()); - } + //compat + public float warmup, boost; @Override public void write(Writes write){ diff --git a/core/src/mindustry/world/blocks/production/Drill.java b/core/src/mindustry/world/blocks/production/Drill.java index cab281a85f..8c627ea493 100644 --- a/core/src/mindustry/world/blocks/production/Drill.java +++ b/core/src/mindustry/world/blocks/production/Drill.java @@ -6,6 +6,7 @@ import arc.graphics.g2d.*; import arc.math.*; import arc.struct.*; import arc.util.*; +import arc.util.io.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.entities.*; @@ -13,12 +14,12 @@ import mindustry.entities.units.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.environment.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -122,11 +123,11 @@ public class Drill extends Block{ if(returnItem != null){ float width = drawPlaceText(Core.bundle.formatFloat("bar.drillspeed", 60f / (drillTime + hardnessDrillMultiplier * returnItem.hardness) * returnCount, 2), x, y, valid); - float dx = x * tilesize + offset - width/2f - 4f, dy = y * tilesize + offset + size * tilesize / 2f + 5; + float dx = x * tilesize + offset - width/2f - 4f, dy = y * tilesize + offset + size * tilesize / 2f + 5, s = iconSmall / 4f; Draw.mixcol(Color.darkGray, 1f); - Draw.rect(returnItem.icon(Cicon.small), dx, dy - 1); + Draw.rect(returnItem.fullIcon, dx, dy - 1, s, s); Draw.reset(); - Draw.rect(returnItem.icon(Cicon.small), dx, dy); + Draw.rect(returnItem.fullIcon, dx, dy, s, s); if(drawMineItem){ Draw.color(returnItem.color); @@ -146,7 +147,7 @@ public class Drill extends Block{ public void setStats(){ super.setStats(); - stats.add(Stat.drillTier, new BlockFilterValue(b -> b instanceof Floor f && f.itemDrop != null && f.itemDrop.hardness <= tier)); + stats.add(Stat.drillTier, StatValues.blocks(b -> b instanceof Floor f && f.itemDrop != null && f.itemDrop.hardness <= tier)); stats.add(Stat.drillSpeed, 60f / drillTime * size * size, StatUnit.itemsSecond); if(liquidBoostIntensity != 1){ @@ -225,16 +226,18 @@ public class Drill extends Block{ @Override public void drawSelect(){ if(dominantItem != null){ - float dx = x - size * tilesize/2f, dy = y + size * tilesize/2f; + float dx = x - size * tilesize/2f, dy = y + size * tilesize/2f, s = iconSmall / 4f; Draw.mixcol(Color.darkGray, 1f); - Draw.rect(dominantItem.icon(Cicon.small), dx, dy - 1); + Draw.rect(dominantItem.fullIcon, dx, dy - 1, s, s); Draw.reset(); - Draw.rect(dominantItem.icon(Cicon.small), dx, dy); + Draw.rect(dominantItem.fullIcon, dx, dy, s, s); } } @Override public void onProximityUpdate(){ + super.onProximityUpdate(); + countOre(tile); dominantItem = returnItem; dominantItems = returnCount; @@ -247,7 +250,7 @@ public class Drill extends Block{ } if(timer(timerDump, dumpTime)){ - dump(dominantItem); + dump(items.has(dominantItem) ? dominantItem : null); } timeDrilled += warmup * delta(); @@ -285,6 +288,12 @@ public class Drill extends Block{ } } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress && dominantItem != null) return Mathf.clamp(progress / (drillTime + hardnessDrillMultiplier * dominantItem.hardness)); + return super.sense(sensor); + } + @Override public void drawCracks(){} @@ -315,6 +324,27 @@ public class Drill extends Block{ Draw.color(); } } + + @Override + public byte version(){ + return 1; + } + + @Override + public void write(Writes write){ + super.write(write); + write.f(progress); + write.f(warmup); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + if(revision >= 1){ + progress = read.f(); + warmup = read.f(); + } + } } } diff --git a/core/src/mindustry/world/blocks/production/Fracker.java b/core/src/mindustry/world/blocks/production/Fracker.java index 0ce66cd54c..a3dc07b312 100644 --- a/core/src/mindustry/world/blocks/production/Fracker.java +++ b/core/src/mindustry/world/blocks/production/Fracker.java @@ -18,10 +18,12 @@ public class Fracker extends SolidPump{ hasItems = true; ambientSound = Sounds.drill; ambientSoundVolume = 0.03f; + envRequired |= Env.groundOil; } @Override public void setStats(){ + stats.timePeriod = itemUseTime; super.setStats(); stats.add(Stat.productionTime, itemUseTime / 60f, StatUnit.seconds); diff --git a/core/src/mindustry/world/blocks/production/GenericCrafter.java b/core/src/mindustry/world/blocks/production/GenericCrafter.java index 11b6c37bc0..bd684fcf8a 100644 --- a/core/src/mindustry/world/blocks/production/GenericCrafter.java +++ b/core/src/mindustry/world/blocks/production/GenericCrafter.java @@ -8,6 +8,7 @@ import arc.util.io.*; import mindustry.content.*; import mindustry.entities.*; import mindustry.gen.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.draw.*; @@ -21,6 +22,9 @@ public class GenericCrafter extends Block{ public Effect craftEffect = Fx.none; public Effect updateEffect = Fx.none; public float updateEffectChance = 0.04f; + public float warmupSpeed = 0.019f; + /** Only used for legacy cultivator blocks. */ + public boolean legacyReadWarmup = false; public DrawBlock drawer = new DrawBlock(); @@ -37,11 +41,12 @@ public class GenericCrafter extends Block{ @Override public void setStats(){ + stats.timePeriod = craftTime; super.setStats(); stats.add(Stat.productionTime, craftTime / 60f, StatUnit.seconds); if(outputItem != null){ - stats.add(Stat.output, outputItem); + stats.add(Stat.output, StatValues.items(craftTime, outputItem)); } if(outputLiquid != null){ @@ -82,9 +87,15 @@ public class GenericCrafter extends Block{ drawer.draw(this); } + @Override + public void drawLight(){ + super.drawLight(); + drawer.drawLight(this); + } + @Override public boolean shouldConsume(){ - if(outputItem != null && items.get(outputItem.item) >= itemCapacity){ + if(outputItem != null && items.get(outputItem.item) + outputItem.amount > itemCapacity){ return false; } return (outputLiquid == null || !(liquids.get(outputLiquid.liquid) >= liquidCapacity - 0.001f)) && enabled; @@ -96,13 +107,13 @@ public class GenericCrafter extends Block{ progress += getProgressIncrease(craftTime); totalProgress += delta(); - warmup = Mathf.lerpDelta(warmup, 1f, 0.02f); + warmup = Mathf.approachDelta(warmup, 1f, warmupSpeed); if(Mathf.chanceDelta(updateEffectChance)){ updateEffect.at(getX() + Mathf.range(size * 4f), getY() + Mathf.range(size * 4)); } }else{ - warmup = Mathf.lerp(warmup, 0f, 0.02f); + warmup = Mathf.approachDelta(warmup, 0f, warmupSpeed); } if(progress >= 1f){ @@ -122,7 +133,7 @@ public class GenericCrafter extends Block{ progress %= 1f; } - if(outputItem != null && timer(timerDump, dumpTime)){ + if(outputItem != null && timer(timerDump, dumpTime / timeScale)){ dump(outputItem.item); } @@ -131,6 +142,12 @@ public class GenericCrafter extends Block{ } } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(progress); + return super.sense(sensor); + } + @Override public int getMaximumAccepted(Item item){ return itemCapacity; @@ -146,6 +163,7 @@ public class GenericCrafter extends Block{ super.write(write); write.f(progress); write.f(warmup); + if(legacyReadWarmup) write.f(0f); } @Override @@ -153,6 +171,7 @@ public class GenericCrafter extends Block{ super.read(read, revision); progress = read.f(); warmup = read.f(); + if(legacyReadWarmup) read.f(); } } } diff --git a/core/src/mindustry/world/blocks/production/GenericSmelter.java b/core/src/mindustry/world/blocks/production/GenericSmelter.java index 1c7e0383ff..9d68130cb0 100644 --- a/core/src/mindustry/world/blocks/production/GenericSmelter.java +++ b/core/src/mindustry/world/blocks/production/GenericSmelter.java @@ -8,10 +8,12 @@ import mindustry.annotations.Annotations.*; import mindustry.gen.*; import mindustry.graphics.*; -/** A GenericCrafter with a new glowing region drawn on top. */ +/** @deprecated this class has no new functionality over GenericCrafter, use GenericCrafter with a DrawSmelter drawer instead. See vanilla smelter blocks. */ +@Deprecated public class GenericSmelter extends GenericCrafter{ public Color flameColor = Color.valueOf("ffc999"); public @Load("@-top") TextureRegion topRegion; + public float flameRadius = 3f, flameRadiusIn = 1.9f, flameRadiusScl = 5f, flameRadiusMag = 2f, flameRadiusInMag = 1f; public GenericSmelter(String name){ super(name); @@ -35,10 +37,10 @@ public class GenericSmelter extends GenericCrafter{ Draw.alpha(((1f - g) + Mathf.absin(Time.time, 8f, g) + Mathf.random(r) - r) * warmup); Draw.tint(flameColor); - Fill.circle(x, y, 3f + Mathf.absin(Time.time, 5f, 2f) + cr); + Fill.circle(x, y, flameRadius + Mathf.absin(Time.time, flameRadiusScl, flameRadiusMag) + cr); Draw.color(1f, 1f, 1f, warmup); Draw.rect(topRegion, x, y); - Fill.circle(x, y, 1.9f + Mathf.absin(Time.time, 5f, 1f) + cr); + Fill.circle(x, y, flameRadiusIn + Mathf.absin(Time.time, flameRadiusScl, flameRadiusInMag) + cr); Draw.color(); } diff --git a/core/src/mindustry/world/blocks/production/LiquidConverter.java b/core/src/mindustry/world/blocks/production/LiquidConverter.java index 3ea25117e0..0b0ad4a0f9 100644 --- a/core/src/mindustry/world/blocks/production/LiquidConverter.java +++ b/core/src/mindustry/world/blocks/production/LiquidConverter.java @@ -1,5 +1,6 @@ package mindustry.world.blocks.production; +import arc.math.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; @@ -47,16 +48,25 @@ public class LiquidConverter extends GenericCrafter{ ConsumeLiquid cl = consumes.get(ConsumeType.liquid); if(cons.valid()){ + if(Mathf.chanceDelta(updateEffectChance)){ + updateEffect.at(getX() + Mathf.range(size * 4f), getY() + Mathf.range(size * 4)); + } + + warmup = Mathf.lerpDelta(warmup, 1f, 0.02f); float use = Math.min(cl.amount * edelta(), liquidCapacity - liquids.get(outputLiquid.liquid)); + float ratio = outputLiquid.amount / cl.amount; liquids.remove(cl.liquid, Math.min(use, liquids.get(cl.liquid))); progress += use / cl.amount; - liquids.add(outputLiquid.liquid, use); + liquids.add(outputLiquid.liquid, use * ratio); if(progress >= craftTime){ consume(); progress %= craftTime; } + }else{ + //warmup is still 1 even if not consuming + warmup = Mathf.lerp(warmup, cons.canConsume() ? 1f : 0f, 0.02f); } dumpLiquid(outputLiquid.liquid); diff --git a/core/src/mindustry/world/blocks/production/PayloadAcceptor.java b/core/src/mindustry/world/blocks/production/PayloadAcceptor.java index df21f8ad6f..be5d296008 100644 --- a/core/src/mindustry/world/blocks/production/PayloadAcceptor.java +++ b/core/src/mindustry/world/blocks/production/PayloadAcceptor.java @@ -1,186 +1,18 @@ package mindustry.world.blocks.production; -import arc.graphics.g2d.*; -import arc.math.*; -import arc.math.geom.*; -import arc.util.*; -import arc.util.io.*; -import mindustry.annotations.Annotations.*; -import mindustry.gen.*; -import mindustry.graphics.*; -import mindustry.world.*; import mindustry.world.blocks.payloads.*; -import static mindustry.Vars.*; - -public class PayloadAcceptor extends Block{ - public float payloadSpeed = 0.5f, payloadRotateSpeed = 5f; - - public @Load(value = "@-top", fallback = "factory-top-@size") TextureRegion topRegion; - public @Load(value = "@-out", fallback = "factory-out-@size") TextureRegion outRegion; - public @Load(value = "@-in", fallback = "factory-in-@size") TextureRegion inRegion; +/** @deprecated used PayloadBlock instead. */ +@Deprecated +public abstract class PayloadAcceptor extends PayloadBlock{ public PayloadAcceptor(String name){ super(name); - - update = true; - sync = true; } - public static boolean blends(Building build, int direction){ - int size = build.block.size; - int trns = build.block.size/2 + 1; - Building accept = build.nearby(Geometry.d4(direction).x * trns, Geometry.d4(direction).y * trns); - return accept != null && - accept.block.outputsPayload && + /** @deprecated used PayloadBlockBuild instead. */ + @Deprecated + public class PayloadAcceptorBuild extends PayloadBlockBuild{ - //if size is the same, block must either be facing this one, or not be rotating - ((accept.block.size == size - && Math.abs(accept.tileX() - build.tileX()) % size == 0 //check alignment - && Math.abs(accept.tileY() - build.tileY()) % size == 0 - && ((accept.block.rotate && accept.tileX() + Geometry.d4(accept.rotation).x * size == build.tileX() && accept.tileY() + Geometry.d4(accept.rotation).y * size == build.tileY()) - || !accept.block.rotate - || !accept.block.outputFacing)) || - - //if the other block is smaller, check alignment - (accept.block.size < size && - (accept.rotation % 2 == 0 ? //check orientation; make sure it's aligned properly with this block. - Math.abs(accept.y - build.y) <= (size * tilesize - accept.block.size * tilesize)/2f : //check Y alignment - Math.abs(accept.x - build.x) <= (size * tilesize - accept.block.size * tilesize)/2f //check X alignment - )) && (!accept.block.rotate || accept.front() == build || !accept.block.outputFacing) //make sure it's facing this block - ); - } - - public class PayloadAcceptorBuild extends Building{ - public @Nullable T payload; - public Vec2 payVector = new Vec2(); - public float payRotation; - public boolean carried; - - @Override - public boolean acceptPayload(Building source, Payload payload){ - return this.payload == null; - } - - @Override - public void handlePayload(Building source, Payload payload){ - this.payload = (T)payload; - this.payVector.set(source).sub(this).clamp(-size * tilesize / 2f, -size * tilesize / 2f, size * tilesize / 2f, size * tilesize / 2f); - this.payRotation = payload.rotation(); - - updatePayload(); - } - - @Override - public Payload getPayload(){ - return payload; - } - - @Override - public void pickedUp(){ - carried = true; - } - - @Override - public void drawTeamTop(){ - carried = false; - } - - @Override - public Payload takePayload(){ - T t = payload; - payload = null; - return t; - } - - @Override - public void onRemoved(){ - super.onRemoved(); - if(payload != null && !carried) payload.dump(); - } - - public boolean blends(int direction){ - return PayloadAcceptor.blends(this, direction); - } - - public void updatePayload(){ - if(payload != null){ - payload.set(x + payVector.x, y + payVector.y, payRotation); - } - } - - /** @return true if the payload is in position. */ - public boolean moveInPayload(){ - if(payload == null) return false; - - updatePayload(); - - payRotation = Angles.moveToward(payRotation, rotate ? rotdeg() : 90f, payloadRotateSpeed * edelta()); - payVector.approach(Vec2.ZERO, payloadSpeed * delta()); - - return hasArrived(); - } - - public void moveOutPayload(){ - if(payload == null) return; - - updatePayload(); - - Vec2 dest = Tmp.v1.trns(rotdeg(), size* tilesize/2f); - - payRotation = Angles.moveToward(payRotation, rotdeg(), payloadRotateSpeed * edelta()); - payVector.approach(dest, payloadSpeed * delta()); - - if(payVector.within(dest, 0.001f)){ - payVector.clamp(-size * tilesize / 2f, -size * tilesize / 2f, size * tilesize / 2f, size * tilesize / 2f); - - Building front = front(); - if(front != null && front.block.outputsPayload){ - if(movePayload(payload)){ - payload = null; - } - }else if(front == null || !front.tile().solid()){ - dumpPayload(); - } - } - } - - public void dumpPayload(){ - if(payload.dump()){ - payload = null; - } - } - - public boolean hasArrived(){ - return payVector.isZero(0.01f); - } - - public void drawPayload(){ - if(payload != null){ - updatePayload(); - - Draw.z(Layer.blockOver); - payload.draw(); - } - } - - @Override - public void write(Writes write){ - super.write(write); - - write.f(payVector.x); - write.f(payVector.y); - write.f(payRotation); - Payload.write(payload, write); - } - - @Override - public void read(Reads read, byte revision){ - super.read(read, revision); - - payVector.set(read.f(), read.f()); - payRotation = read.f(); - payload = Payload.read(read); - } } } diff --git a/core/src/mindustry/world/blocks/production/Pump.java b/core/src/mindustry/world/blocks/production/Pump.java index 692b637428..fd150ba38a 100644 --- a/core/src/mindustry/world/blocks/production/Pump.java +++ b/core/src/mindustry/world/blocks/production/Pump.java @@ -6,7 +6,6 @@ import arc.graphics.g2d.*; import mindustry.game.*; import mindustry.graphics.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.liquid.*; import mindustry.world.meta.*; @@ -48,11 +47,11 @@ public class Pump extends LiquidBlock{ if(liquidDrop != null){ float width = drawPlaceText(Core.bundle.formatFloat("bar.pumpspeed", amount * pumpAmount * 60f, 0), x, y, valid); - float dx = x * tilesize + offset - width/2f - 4f, dy = y * tilesize + offset + size * tilesize / 2f + 5; + float dx = x * tilesize + offset - width/2f - 4f, dy = y * tilesize + offset + size * tilesize / 2f + 5, s = iconSmall / 4f; Draw.mixcol(Color.darkGray, 1f); - Draw.rect(liquidDrop.icon(Cicon.small), dx, dy - 1); + Draw.rect(liquidDrop.fullIcon, dx, dy - 1, s, s); Draw.reset(); - Draw.rect(liquidDrop.icon(Cicon.small), dx, dy); + Draw.rect(liquidDrop.fullIcon, dx, dy, s, s); } } diff --git a/core/src/mindustry/world/blocks/production/Separator.java b/core/src/mindustry/world/blocks/production/Separator.java index 494a2977d8..eb6acdd878 100644 --- a/core/src/mindustry/world/blocks/production/Separator.java +++ b/core/src/mindustry/world/blocks/production/Separator.java @@ -3,6 +3,7 @@ package mindustry.world.blocks.production; import arc.*; import arc.graphics.g2d.*; import arc.math.*; +import arc.util.*; import arc.util.io.*; import mindustry.annotations.Annotations.*; import mindustry.gen.*; @@ -11,7 +12,6 @@ import mindustry.type.*; import mindustry.world.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; /** * Extracts a random list of items from an input item and an input liquid. @@ -34,15 +34,10 @@ public class Separator extends Block{ @Override public void setStats(){ + stats.timePeriod = craftTime; super.setStats(); - stats.add(Stat.output, new ItemFilterValue(item -> { - for(ItemStack i : results){ - if(item == i.item) return true; - } - return false; - })); - + stats.add(Stat.output, StatValues.items(item -> Structs.contains(results, i -> i.item == item))); stats.add(Stat.productionTime, craftTime / 60f, StatUnit.seconds); } diff --git a/core/src/mindustry/world/blocks/production/SingleBlockProducer.java b/core/src/mindustry/world/blocks/production/SingleBlockProducer.java new file mode 100644 index 0000000000..5dab7d775d --- /dev/null +++ b/core/src/mindustry/world/blocks/production/SingleBlockProducer.java @@ -0,0 +1,23 @@ +package mindustry.world.blocks.production; + +import arc.util.*; +import mindustry.content.*; +import mindustry.world.*; +import mindustry.world.blocks.payloads.*; + +public class SingleBlockProducer extends BlockProducer{ + public Block result = Blocks.router; + + public SingleBlockProducer(String name){ + super(name); + } + + public class SingleBlockProducerBuild extends BlockProducerBuild{ + + @Nullable + @Override + public Block recipe(){ + return result; + } + } +} diff --git a/core/src/mindustry/world/blocks/sandbox/ItemSource.java b/core/src/mindustry/world/blocks/sandbox/ItemSource.java index aee34a9446..c534c0a601 100644 --- a/core/src/mindustry/world/blocks/sandbox/ItemSource.java +++ b/core/src/mindustry/world/blocks/sandbox/ItemSource.java @@ -24,6 +24,7 @@ public class ItemSource extends Block{ configurable = true; saveConfig = true; noUpdateDisabled = true; + envEnabled = Env.any; config(Item.class, (ItemSourceBuild tile, Item item) -> tile.outputItem = item); configClear((ItemSourceBuild tile) -> tile.outputItem = null); diff --git a/core/src/mindustry/world/blocks/sandbox/ItemVoid.java b/core/src/mindustry/world/blocks/sandbox/ItemVoid.java index 030433d43d..fcda607542 100644 --- a/core/src/mindustry/world/blocks/sandbox/ItemVoid.java +++ b/core/src/mindustry/world/blocks/sandbox/ItemVoid.java @@ -11,6 +11,7 @@ public class ItemVoid extends Block{ super(name); group = BlockGroup.transportation; update = solid = acceptsItems = true; + envEnabled = Env.any; } public class ItemVoidBuild extends Building{ diff --git a/core/src/mindustry/world/blocks/sandbox/PowerSource.java b/core/src/mindustry/world/blocks/sandbox/PowerSource.java index b392ce8813..8953a595b6 100644 --- a/core/src/mindustry/world/blocks/sandbox/PowerSource.java +++ b/core/src/mindustry/world/blocks/sandbox/PowerSource.java @@ -1,6 +1,7 @@ package mindustry.world.blocks.sandbox; import mindustry.world.blocks.power.*; +import mindustry.world.meta.*; public class PowerSource extends PowerNode{ @@ -11,6 +12,8 @@ public class PowerSource extends PowerNode{ maxNodes = 100; outputsPower = true; consumesPower = false; + //TODO maybe don't? + envEnabled = Env.any; } public class PowerSourceBuild extends PowerNodeBuild{ diff --git a/core/src/mindustry/world/blocks/sandbox/PowerVoid.java b/core/src/mindustry/world/blocks/sandbox/PowerVoid.java index e97622c609..93427b7e02 100644 --- a/core/src/mindustry/world/blocks/sandbox/PowerVoid.java +++ b/core/src/mindustry/world/blocks/sandbox/PowerVoid.java @@ -8,6 +8,7 @@ public class PowerVoid extends PowerBlock{ public PowerVoid(String name){ super(name); consumes.power(Float.MAX_VALUE); + envEnabled = Env.any; } @Override diff --git a/core/src/mindustry/world/blocks/storage/CoreBlock.java b/core/src/mindustry/world/blocks/storage/CoreBlock.java index 45b2ce32d1..a5d61237e3 100644 --- a/core/src/mindustry/world/blocks/storage/CoreBlock.java +++ b/core/src/mindustry/world/blocks/storage/CoreBlock.java @@ -6,6 +6,7 @@ import arc.graphics.g2d.*; import arc.math.*; import arc.math.geom.*; import arc.struct.*; +import arc.util.*; import mindustry.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; @@ -19,7 +20,6 @@ import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.*; -import mindustry.world.blocks.*; import mindustry.world.blocks.units.*; import mindustry.world.meta.*; import mindustry.world.modules.*; @@ -37,6 +37,7 @@ public class CoreBlock extends StorageBlock{ public int ammoAmount = 5; public float resupplyRate = 10f; public float resupplyRange = 60f; + public float captureInvicibility = 60f * 15f; public Item resupplyItem = Items.copper; public CoreBlock(String name){ @@ -46,12 +47,16 @@ public class CoreBlock extends StorageBlock{ update = true; hasItems = true; priority = TargetPriority.core; - flags = EnumSet.of(BlockFlag.core, BlockFlag.unitModifier); + flags = EnumSet.of(BlockFlag.core); unitCapModifier = 10; loopSound = Sounds.respawning; loopSoundVolume = 1f; drawDisabled = false; canOverdrive = false; + + //support everything + envEnabled = Env.any; + drawDisabled = false; replaceable = false; } @@ -97,6 +102,15 @@ public class CoreBlock extends StorageBlock{ )); } + @Override + public void init(){ + //assign to update clipSize internally + lightRadius = 30f + 20f * size; + emitLight = true; + + super.init(); + } + @Override public boolean canBreak(Tile tile){ return false; @@ -156,19 +170,29 @@ public class CoreBlock extends StorageBlock{ if(!canPlaceOn(world.tile(x, y), player.team())){ - drawPlaceText(Core.bundle.get((player.team().core() != null && player.team().core().items.has(requirements, state.rules.buildCostMultiplier)) || state.rules.infiniteResources ? + drawPlaceText(Core.bundle.get( + (player.team().core() != null && player.team().core().items.has(requirements, state.rules.buildCostMultiplier)) || state.rules.infiniteResources ? "bar.corereq" : "bar.noresources" ), x, y, valid); - } } - public class CoreBuild extends Building implements ControlBlock{ + public class CoreBuild extends Building{ public int storageCapacity; - //note that this unit is never actually used for control; the possession handler makes the player respawn when this unit is controlled - public BlockUnitc unit = Nulls.blockUnit; public boolean noEffect = false; + public Team lastDamage = Team.derelict; + public float iframes = -1f; + + @Override + public void damage(@Nullable Team source, float damage){ + if(iframes > 0) return; + + if(source != null && source != team){ + lastDamage = source; + } + super.damage(source, damage); + } @Override public double sense(LAccess sensor){ @@ -177,23 +201,34 @@ public class CoreBlock extends StorageBlock{ } @Override - public void created(){ - unit = (BlockUnitc)UnitTypes.block.create(team); - unit.tile(this); + public boolean canControlSelect(Player player){ + return true; } @Override - public Unit unit(){ - return (Unit)unit; + public void onControlSelect(Player player){ + Fx.spawn.at(player); + if(net.client()){ + control.input.controlledType = null; + } + + player.clearUnit(); + player.deathTimer = Player.deathDelay + 1f; + requestSpawn(player); } public void requestSpawn(Player player){ + //do not try to respawn in unsupported environments at all + if(!unitType.supportsEnv(state.rules.environment)) return; + Call.playerSpawn(tile, player); } @Override public void updateTile(){ + iframes -= Time.delta; + //resupply nearby units if(items.has(resupplyItem) && timer(timerResupply, resupplyRate) && ResupplyPoint.resupply(this, resupplyRange, ammoAmount, resupplyItem.color)){ items.remove(resupplyItem, 1); @@ -208,7 +243,13 @@ public class CoreBlock extends StorageBlock{ @Override public void onDestroyed(){ - super.onDestroyed(); + if(state.rules.coreCapture){ + //just create an explosion, no fire. this prevents immediate recapture + Damage.dynamicExplosion(x, y, 0, 0, 0, tilesize * block.size / 2f, state.rules.damageExplosions); + Fx.commandSend.at(x, y, 140f); + }else{ + super.onDestroyed(); + } //add a spawn to the map for future reference - waves should be disabled, so it shouldn't matter if(state.isCampaign() && team == state.rules.waveTeam && team.cores().size <= 1){ @@ -221,9 +262,18 @@ public class CoreBlock extends StorageBlock{ } } + @Override + public void afterDestroyed(){ + if(state.rules.coreCapture){ + tile.setBlock(block, lastDamage); + //core is invincible for several seconds to prevent recapture + ((CoreBuild)tile.build).iframes = captureInvicibility; + } + } + @Override public void drawLight(){ - Drawf.light(team, x, y, 30f + 20f * size, Pal.accent, 0.65f + Mathf.absin(20f, 0.1f)); + Drawf.light(team, x, y, lightRadius, Pal.accent, 0.65f + Mathf.absin(20f, 0.1f)); } @Override @@ -238,6 +288,8 @@ public class CoreBlock extends StorageBlock{ @Override public void onProximityUpdate(){ + super.onProximityUpdate(); + for(Building other : state.teams.cores(team)){ if(other.tile() != tile){ this.items = other.items; diff --git a/core/src/mindustry/world/blocks/storage/Unloader.java b/core/src/mindustry/world/blocks/storage/Unloader.java index a2fa940da9..d43015b7cf 100644 --- a/core/src/mindustry/world/blocks/storage/Unloader.java +++ b/core/src/mindustry/world/blocks/storage/Unloader.java @@ -10,12 +10,12 @@ import mindustry.gen.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.*; +import mindustry.world.meta.*; import static mindustry.Vars.*; public class Unloader extends Block{ public float speed = 1f; - public final int timerUnload = timers++; public Unloader(String name){ super(name); @@ -33,6 +33,12 @@ public class Unloader extends Block{ configClear((UnloaderBuild tile) -> tile.sortItem = null); } + @Override + public void setStats(){ + super.setStats(); + stats.add(Stat.speed, 60f / speed, StatUnit.itemsSecond); + } + @Override public void drawRequestConfig(BuildPlan req, Eachable list){ drawRequestConfigCenter(req, req.config, "unloader-center"); @@ -45,6 +51,7 @@ public class Unloader extends Block{ } public class UnloaderBuild extends Building{ + public float unloadTimer = 0f; public Item sortItem = null; public Building dumpingTo; public int offset = 0; @@ -52,7 +59,8 @@ public class Unloader extends Block{ @Override public void updateTile(){ - if(timer(timerUnload, speed / timeScale)){ + if((unloadTimer += delta()) >= speed){ + boolean any = false; if(rotations == null || rotations.length != proximity.size){ rotations = new int[proximity.size]; } @@ -72,6 +80,7 @@ public class Unloader extends Block{ //remove item if it's dumped correctly if(put(item)){ other.items.remove(item, 1); + any = true; if(sortItem == null){ rotations[pos] = item.id + 1; @@ -84,6 +93,12 @@ public class Unloader extends Block{ } } + if(any){ + unloadTimer %= speed; + }else{ + unloadTimer = Math.min(unloadTimer, speed); + } + if(proximity.size > 0){ offset ++; offset %= proximity.size; diff --git a/core/src/mindustry/world/blocks/units/Reconstructor.java b/core/src/mindustry/world/blocks/units/Reconstructor.java index 77647f2909..c2e5dd6c4a 100644 --- a/core/src/mindustry/world/blocks/units/Reconstructor.java +++ b/core/src/mindustry/world/blocks/units/Reconstructor.java @@ -13,6 +13,7 @@ import mindustry.entities.units.*; import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.blocks.payloads.*; @@ -71,12 +72,12 @@ public class Reconstructor extends UnitBlock{ for(var upgrade : upgrades){ float size = 8 * 3; if(upgrade[0].unlockedNow() && upgrade[1].unlockedNow()){ - table.image(upgrade[0].icon(Cicon.small)).size(size).padRight(4).padLeft(10).scaling(Scaling.fit).right(); + table.image(upgrade[0].uiIcon).size(size).padRight(4).padLeft(10).scaling(Scaling.fit).right(); table.add(upgrade[0].localizedName).left(); table.add("[lightgray] -> "); - table.image(upgrade[1].icon(Cicon.small)).size(size).padRight(4).scaling(Scaling.fit); + table.image(upgrade[1].uiIcon).size(size).padRight(4).scaling(Scaling.fit); table.add(upgrade[1].localizedName).left(); table.row(); } @@ -107,6 +108,11 @@ public class Reconstructor extends UnitBlock{ return progress / constructTime; } + @Override + public boolean acceptUnitPayload(Unit unit){ + return hasUpgrade(unit.type); + } + @Override public boolean acceptPayload(Building source, Payload payload){ return this.payload == null @@ -147,7 +153,7 @@ public class Reconstructor extends UnitBlock{ if(constructing() && hasArrived()){ Draw.draw(Layer.blockOver, () -> { Draw.alpha(1f - progress/ constructTime); - Draw.rect(payload.unit.type.icon(Cicon.full), x, y, payload.rotation() - 90); + Draw.rect(payload.unit.type.fullIcon, x, y, payload.rotation() - 90); Draw.reset(); Drawf.construct(this, upgrade(payload.unit.type), payload.rotation() - 90f, progress / constructTime, speedScl, time); }); @@ -193,6 +199,12 @@ public class Reconstructor extends UnitBlock{ time += edelta() * speedScl * state.rules.unitBuildSpeedMultiplier; } + @Override + public double sense(LAccess sensor){ + if(sensor == LAccess.progress) return Mathf.clamp(fraction()); + return super.sense(sensor); + } + @Override public boolean shouldConsume(){ return constructing(); diff --git a/core/src/mindustry/world/blocks/units/RepairPoint.java b/core/src/mindustry/world/blocks/units/RepairPoint.java index b84ed1d119..b79413e3c4 100644 --- a/core/src/mindustry/world/blocks/units/RepairPoint.java +++ b/core/src/mindustry/world/blocks/units/RepairPoint.java @@ -8,29 +8,47 @@ import arc.struct.*; import arc.util.*; import arc.util.io.*; import mindustry.annotations.Annotations.*; +import mindustry.content.*; import mindustry.entities.*; +import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.logic.*; import mindustry.world.*; +import mindustry.world.consumers.*; import mindustry.world.meta.*; import static mindustry.Vars.*; public class RepairPoint extends Block{ static final Rect rect = new Rect(); + static final Rand rand = new Rand(); public int timerTarget = timers++; + public int timerEffect = timers++; public float repairRadius = 50f; public float repairSpeed = 0.3f; public float powerUse; + public float length = 5f; + public float beamWidth = 1f; + public float pulseRadius = 6f; + public float pulseStroke = 2f; + public boolean acceptCoolant = false; - public @Load("@-base") TextureRegion baseRegion; - public @Load("laser") TextureRegion laser; - public @Load("laser-end") TextureRegion laserEnd; + public float coolantUse = 0.5f; + /** Effect displayed when coolant is used. */ + public Effect coolEffect = Fx.fuelburn; + /** How much healing is increased by with heat capacity. */ + public float coolantMultiplier = 1f; - public Color laserColor = Color.valueOf("e8ffd7"); + public @Load(value = "@-base", fallback = "block-@size") TextureRegion baseRegion; + public @Load("laser-white") TextureRegion laser; + public @Load("laser-white-end") TextureRegion laserEnd; + public @Load("laser-top") TextureRegion laserTop; + public @Load("laser-top-end") TextureRegion laserTopEnd; + + public Color laserColor = Color.valueOf("98ffa9"), laserTopColor = Color.white.cpy(); public RepairPoint(String name){ super(name); @@ -39,18 +57,30 @@ public class RepairPoint extends Block{ flags = EnumSet.of(BlockFlag.repair); hasPower = true; outlineIcon = true; - expanded = true; + //yeah, this isn't the same thing, but it's close enough + group = BlockGroup.projectors; } @Override public void setStats(){ super.setStats(); stats.add(Stat.range, repairRadius / tilesize, StatUnit.blocks); + stats.add(Stat.repairSpeed, repairSpeed * 60f, StatUnit.perSecond); + + if(acceptCoolant){ + stats.add(Stat.booster, StatValues.strengthBoosters(coolantMultiplier, l -> consumes.liquidfilters.get(l.id))); + } } @Override public void init(){ - consumes.powerCond(powerUse, entity -> ((RepairPointBuild)entity).target != null); + if(acceptCoolant){ + hasLiquids = true; + consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, coolantUse)).optional(true, true); + } + + consumes.powerCond(powerUse, (RepairPointBuild entity) -> entity.target != null); + clipSize = Math.max(clipSize, (repairRadius + tilesize) * 2); super.init(); } @@ -66,8 +96,56 @@ public class RepairPoint extends Block{ return new TextureRegion[]{baseRegion, region}; } + public static void drawBeam(float x, float y, float rotation, float length, int id, Sized target, Team team, + float strength, float pulseStroke, float pulseRadius, float beamWidth, + Vec2 lastEnd, Vec2 offset, + Color laserColor, Color laserTopColor, + TextureRegion laser, TextureRegion laserEnd, TextureRegion laserTop, TextureRegion laserTopEnd){ + if(target != null){ + float + originX = x + Angles.trnsx(rotation, length), + originY = y + Angles.trnsy(rotation, length); + + rand.setSeed(id + (target instanceof Entityc e ? e.id() : 0)); + + lastEnd.set(target).sub(originX, originY); + lastEnd.setLength(Math.max(2f, lastEnd.len())); + + lastEnd.add(offset.trns( + rand.random(360f) + Time.time/2f, + Mathf.sin(Time.time + rand.random(200f), 55f, rand.random(target.hitSize() * 0.2f, target.hitSize() * 0.45f)) + ).rotate(target instanceof Rotc rot ? rot.rotation() : 0f)); + + lastEnd.add(originX, originY); + } + + if(strength > 0.01f){ + float + originX = x + Angles.trnsx(rotation, length), + originY = y + Angles.trnsy(rotation, length); + + Draw.z(Layer.flyingUnit + 1); //above all units + + Draw.color(laserColor); + + float f = (Time.time / 85f + rand.random(1f)) % 1f; + + Draw.alpha(1f - Interp.pow5In.apply(f)); + Lines.stroke(strength * pulseStroke); + Lines.circle(lastEnd.x, lastEnd.y, 1f + f * pulseRadius); + + Draw.color(laserColor); + Drawf.laser(team, laser, laserEnd, originX, originY, lastEnd.x, lastEnd.y, strength * beamWidth); + Draw.z(Layer.flyingUnit + 1.1f); + Draw.color(laserTopColor); + Drawf.laser(team, laserTop, laserTopEnd, originX, originY, lastEnd.x, lastEnd.y, strength * beamWidth); + Draw.color(); + } + } + public class RepairPointBuild extends Building implements Ranged{ public Unit target; + public Vec2 offset = new Vec2(), lastEnd = new Vec2(); public float strength, rotation = 90; @Override @@ -78,17 +156,9 @@ public class RepairPoint extends Block{ Drawf.shadow(region, x - (size / 2f), y - (size / 2f), rotation - 90); Draw.rect(region, x, y, rotation - 90); - if(target != null && Angles.angleDist(angleTo(target), rotation) < 30f){ - Draw.z(Layer.flyingUnit + 1); //above all units - float ang = angleTo(target); - float len = 5f; - - Draw.color(laserColor); - Drawf.laser(team, laser, laserEnd, - x + Angles.trnsx(ang, len), y + Angles.trnsy(ang, len), - target.x(), target.y(), strength); - Draw.color(); - } + drawBeam(x, y, rotation, length, id, target, team, strength, + pulseStroke, pulseRadius, beamWidth, lastEnd, offset, laserColor, laserTopColor, + laser, laserEnd, laserTop, laserTopEnd); } @Override @@ -98,21 +168,33 @@ public class RepairPoint extends Block{ @Override public void updateTile(){ - boolean targetIsBeingRepaired = false; - if(target != null && (target.dead() || target.dst(tile) - target.hitSize/2f > repairRadius || target.health() >= target.maxHealth())){ - target = null; - }else if(target != null && consValid()){ - target.heal(repairSpeed * strength * edelta()); - rotation = Mathf.slerpDelta(rotation, angleTo(target), 0.5f * efficiency() * timeScale); - targetIsBeingRepaired = true; + float multiplier = 1f; + if(acceptCoolant){ + var liq = consumes.get(ConsumeType.liquid); + multiplier = liq.valid(this) ? 1f + liquids.current().heatCapacity * coolantMultiplier : 1f; } - if(target != null && targetIsBeingRepaired){ - strength = Mathf.lerpDelta(strength, 1f, 0.08f * Time.delta); - }else{ - strength = Mathf.lerpDelta(strength, 0f, 0.07f * Time.delta); + if(target != null && (target.dead() || target.dst(tile) - target.hitSize/2f > repairRadius || target.health() >= target.maxHealth())){ + target = null; } + if(target == null){ + offset.setZero(); + } + + boolean healed = false; + + if(target != null && consValid()){ + float angle = Angles.angle(x, y, target.x + offset.x, target.y + offset.y); + if(Angles.angleDist(angle, rotation) < 30f){ + healed = true; + target.heal(repairSpeed * strength * edelta() * multiplier); + } + rotation = Mathf.slerpDelta(rotation, angle, 0.5f * efficiency() * timeScale); + } + + strength = Mathf.lerpDelta(strength, healed ? 1f : 0f, 0.08f * Time.delta); + if(timer(timerTarget, 20)){ rect.setSize(repairRadius * 2).setCenter(x, y); target = Units.closest(team, x, y, repairRadius, Unit::damaged); diff --git a/core/src/mindustry/world/blocks/units/UnitBlock.java b/core/src/mindustry/world/blocks/units/UnitBlock.java index 872745c334..4c78b650ad 100644 --- a/core/src/mindustry/world/blocks/units/UnitBlock.java +++ b/core/src/mindustry/world/blocks/units/UnitBlock.java @@ -1,20 +1,12 @@ package mindustry.world.blocks.units; -import arc.*; -import arc.math.*; -import arc.util.*; import mindustry.annotations.Annotations.*; -import mindustry.content.*; -import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.world.*; import mindustry.world.blocks.payloads.*; -import mindustry.world.blocks.production.*; import mindustry.world.meta.*; -import static mindustry.Vars.*; - -public class UnitBlock extends PayloadAcceptor{ +public class UnitBlock extends PayloadBlock{ public UnitBlock(String name){ super(name); @@ -31,7 +23,7 @@ public class UnitBlock extends PayloadAcceptor{ build.spawned(); } - public class UnitBuild extends PayloadAcceptorBuild{ + public class UnitBuild extends PayloadBlockBuild{ public float progress, time, speedScl; public void spawned(){ diff --git a/core/src/mindustry/world/blocks/units/UnitFactory.java b/core/src/mindustry/world/blocks/units/UnitFactory.java index 2cd64fd33e..8c337e9919 100644 --- a/core/src/mindustry/world/blocks/units/UnitFactory.java +++ b/core/src/mindustry/world/blocks/units/UnitFactory.java @@ -98,7 +98,7 @@ public class UnitFactory extends UnitBlock{ table.row(); for(var plan : p){ if(plan.unit.unlockedNow()){ - table.image(plan.unit.icon(Cicon.small)).size(8 * 3).padRight(2).right(); + table.image(plan.unit.uiIcon).size(8 * 3).padRight(2).right(); table.add(plan.unit.localizedName).left(); table.add(Strings.autoFixed(plan.time / 60f, 1) + " " + Core.bundle.get("unit.seconds")).color(Color.lightGray).padLeft(12).left(); table.row(); @@ -143,6 +143,7 @@ public class UnitFactory extends UnitBlock{ @Override public Object senseObject(LAccess sensor){ if(sensor == LAccess.config) return currentPlan == -1 ? null : plans.get(currentPlan).unit; + if(sensor == LAccess.progress) return Mathf.clamp(fraction()); return super.senseObject(sensor); } @@ -172,7 +173,7 @@ public class UnitFactory extends UnitBlock{ table.table(t -> { t.left(); t.image().update(i -> { - i.setDrawable(currentPlan == -1 ? Icon.cancel : reg.set(plans.get(currentPlan).unit.icon(Cicon.medium))); + i.setDrawable(currentPlan == -1 ? Icon.cancel : reg.set(plans.get(currentPlan).unit.uiIcon)); i.setScaling(Scaling.fit); i.setColor(currentPlan == -1 ? Color.lightGray : Color.white); }).size(32).padBottom(-4).padRight(2); diff --git a/core/src/mindustry/world/consumers/Consume.java b/core/src/mindustry/world/consumers/Consume.java index 3066dbb9bb..e1e9cac916 100644 --- a/core/src/mindustry/world/consumers/Consume.java +++ b/core/src/mindustry/world/consumers/Consume.java @@ -8,10 +8,10 @@ import mindustry.world.meta.*; /** An abstract class that defines a type of resource that a block can consume. */ public abstract class Consume{ /** If true, this consumer will not influence consumer validity. */ - protected boolean optional; + public boolean optional; /** If true, this consumer will be displayed as a boost input. */ - protected boolean booster; - protected boolean update = true; + public boolean booster; + public boolean update = true; /** * Apply a filter to items accepted. diff --git a/core/src/mindustry/world/consumers/ConsumeItemDynamic.java b/core/src/mindustry/world/consumers/ConsumeItemDynamic.java index 580ba7ed1d..009824d1d8 100644 --- a/core/src/mindustry/world/consumers/ConsumeItemDynamic.java +++ b/core/src/mindustry/world/consumers/ConsumeItemDynamic.java @@ -46,7 +46,7 @@ public class ConsumeItemDynamic extends Consume{ int i = 0; for(ItemStack stack : items.get(tile)){ - table.add(new ReqImage(new ItemImage(stack.item.icon(Cicon.medium), stack.amount), + table.add(new ReqImage(new ItemImage(stack.item.uiIcon, stack.amount), () -> tile.items != null && tile.items.has(stack.item, stack.amount))).padRight(8).left(); if(++i % 4 == 0) table.row(); } diff --git a/core/src/mindustry/world/consumers/ConsumeItemFilter.java b/core/src/mindustry/world/consumers/ConsumeItemFilter.java index 93d70a46f3..b4346d0c6e 100644 --- a/core/src/mindustry/world/consumers/ConsumeItemFilter.java +++ b/core/src/mindustry/world/consumers/ConsumeItemFilter.java @@ -7,7 +7,6 @@ import mindustry.gen.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -31,7 +30,7 @@ public class ConsumeItemFilter extends Consume{ @Override public void build(Building tile, Table table){ MultiReqImage image = new MultiReqImage(); - content.items().each(i -> filter.get(i) && i.unlockedNow(), item -> image.add(new ReqImage(new ItemImage(item.icon(Cicon.medium), 1), + content.items().each(i -> filter.get(i) && i.unlockedNow(), item -> image.add(new ReqImage(new ItemImage(item.uiIcon, 1), () -> tile.items != null && tile.items.has(item)))); table.add(image).size(8 * 4); @@ -71,6 +70,6 @@ public class ConsumeItemFilter extends Consume{ @Override public void display(Stats stats){ - stats.add(booster ? Stat.booster : Stat.input, new ItemFilterValue(filter)); + stats.add(booster ? Stat.booster : Stat.input, stats.timePeriod < 0 ? StatValues.items(filter) : StatValues.items(stats.timePeriod, filter)); } } diff --git a/core/src/mindustry/world/consumers/ConsumeItems.java b/core/src/mindustry/world/consumers/ConsumeItems.java index c4f5b03cfd..be41ed773c 100644 --- a/core/src/mindustry/world/consumers/ConsumeItems.java +++ b/core/src/mindustry/world/consumers/ConsumeItems.java @@ -6,7 +6,6 @@ import mindustry.gen.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; public class ConsumeItems extends Consume{ public final ItemStack[] items; @@ -22,7 +21,7 @@ public class ConsumeItems extends Consume{ @Override public void applyItemFilter(Bits filter){ - for(ItemStack stack : items){ + for(var stack : items){ filter.set(stack.item.id); } } @@ -36,10 +35,10 @@ public class ConsumeItems extends Consume{ public void build(Building tile, Table table){ table.table(c -> { int i = 0; - for(ItemStack stack : items){ - c.add(new ReqImage(new ItemImage(stack.item.icon(Cicon.medium), stack.amount), + for(var stack : items){ + c.add(new ReqImage(new ItemImage(stack.item.uiIcon, stack.amount), () -> tile.items != null && tile.items.has(stack.item, stack.amount))).padRight(8); - if(++i % 4 == 0) table.row(); + if(++i % 4 == 0) c.row(); } }).left(); } @@ -56,7 +55,7 @@ public class ConsumeItems extends Consume{ @Override public void trigger(Building entity){ - for(ItemStack stack : items){ + for(var stack : items){ entity.items.remove(stack); } } @@ -68,6 +67,6 @@ public class ConsumeItems extends Consume{ @Override public void display(Stats stats){ - stats.add(booster ? Stat.booster : Stat.input, new ItemListValue(items)); + stats.add(booster ? Stat.booster : Stat.input, stats.timePeriod < 0 ? StatValues.items(items) : StatValues.items(stats.timePeriod, items)); } } diff --git a/core/src/mindustry/world/consumers/ConsumeLiquid.java b/core/src/mindustry/world/consumers/ConsumeLiquid.java index 6a4f765edb..fc345f88cc 100644 --- a/core/src/mindustry/world/consumers/ConsumeLiquid.java +++ b/core/src/mindustry/world/consumers/ConsumeLiquid.java @@ -7,6 +7,8 @@ import mindustry.type.*; import mindustry.ui.*; import mindustry.world.meta.*; +import static mindustry.Vars.*; + public class ConsumeLiquid extends ConsumeLiquidBase{ public final Liquid liquid; @@ -26,7 +28,7 @@ public class ConsumeLiquid extends ConsumeLiquidBase{ @Override public void build(Building tile, Table table){ - table.add(new ReqImage(liquid.icon(Cicon.medium), () -> valid(tile))).size(8 * 4); + table.add(new ReqImage(liquid.uiIcon, () -> valid(tile))).size(iconMed).top().left(); } @Override diff --git a/core/src/mindustry/world/consumers/ConsumeLiquidFilter.java b/core/src/mindustry/world/consumers/ConsumeLiquidFilter.java index 5b1881458b..8a7fc026d1 100644 --- a/core/src/mindustry/world/consumers/ConsumeLiquidFilter.java +++ b/core/src/mindustry/world/consumers/ConsumeLiquidFilter.java @@ -7,7 +7,6 @@ import mindustry.gen.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.meta.*; -import mindustry.world.meta.values.*; import static mindustry.Vars.*; @@ -28,7 +27,7 @@ public class ConsumeLiquidFilter extends ConsumeLiquidBase{ public void build(Building build, Table table){ Seq list = content.liquids().select(l -> !l.isHidden() && filter.get(l)); MultiReqImage image = new MultiReqImage(); - list.each(liquid -> image.add(new ReqImage(liquid.icon(Cicon.medium), () -> + list.each(liquid -> image.add(new ReqImage(liquid.uiIcon, () -> build.liquids != null && build.liquids.current() == liquid && build.liquids.get(liquid) >= Math.max(use(build), amount * build.delta())))); table.add(image).size(8 * 4); @@ -51,6 +50,6 @@ public class ConsumeLiquidFilter extends ConsumeLiquidBase{ @Override public void display(Stats stats){ - stats.add(booster ? Stat.booster : Stat.input, new LiquidFilterValue(filter, amount * 60f, true)); + stats.add(booster ? Stat.booster : Stat.input, StatValues.liquids(filter, amount * 60f, true)); } } diff --git a/core/src/mindustry/world/draw/DrawAnimation.java b/core/src/mindustry/world/draw/DrawAnimation.java index 6c4de1ef36..f0699634a4 100644 --- a/core/src/mindustry/world/draw/DrawAnimation.java +++ b/core/src/mindustry/world/draw/DrawAnimation.java @@ -15,17 +15,21 @@ public class DrawAnimation extends DrawBlock{ public TextureRegion liquid, top; @Override - public void draw(GenericCrafterBuild entity){ - Draw.rect(entity.block.region, entity.x, entity.y); + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y); Draw.rect( sine ? - frames[(int)Mathf.absin(entity.totalProgress, frameSpeed, frameCount - 0.001f)] : - frames[(int)((entity.totalProgress / frameSpeed) % frameCount)], - entity.x, entity.y); - Draw.color(Color.clear, entity.liquids.current().color, entity.liquids.total() / entity.block.liquidCapacity); - Draw.rect(liquid, entity.x, entity.y); - Draw.color(); - Draw.rect(top, entity.x, entity.y); + frames[(int)Mathf.absin(build.totalProgress, frameSpeed, frameCount - 0.001f)] : + frames[(int)((build.totalProgress / frameSpeed) % frameCount)], + build.x, build.y); + if(build.liquids != null){ + Draw.color(Color.clear, build.liquids.current().color, build.liquids.total() / build.block.liquidCapacity); + Draw.rect(liquid, build.x, build.y); + Draw.color(); + } + if(top.found()){ + Draw.rect(top, build.x, build.y); + } } @Override diff --git a/core/src/mindustry/world/draw/DrawArcSmelter.java b/core/src/mindustry/world/draw/DrawArcSmelter.java new file mode 100644 index 0000000000..efab1368ed --- /dev/null +++ b/core/src/mindustry/world/draw/DrawArcSmelter.java @@ -0,0 +1,69 @@ +package mindustry.world.draw; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.world.*; +import mindustry.world.blocks.production.GenericCrafter.*; + +//TODO +public class DrawArcSmelter extends DrawBlock{ + public TextureRegion top, bottom; + public Color flameColor = Color.valueOf("f58349"), midColor = Color.valueOf("f2d585"); + public float flameRad = 1f, circleSpace = 2f, flameRadiusScl = 3f, flameRadiusMag = 0.3f, circleStroke = 1.5f; + + public float alpha = 0.68f; + public int particles = 25; + public float particleLife = 40f, particleRad = 7f, particleStroke = 1.1f, particleLen = 3f; + + @Override + public void draw(GenericCrafterBuild build){ + Draw.rect(bottom, build.x, build.y); + + if(build.warmup > 0f && flameColor.a > 0.001f){ + + + Lines.stroke(circleStroke * build.warmup); + + float si = Mathf.absin(flameRadiusScl, flameRadiusMag); + float a = alpha * build.warmup; + Draw.blend(Blending.additive); + + Draw.color(midColor, a); + Fill.circle(build.x, build.y, flameRad + si); + + Draw.color(flameColor, a); + Lines.circle(build.x, build.y, (flameRad + circleSpace + si) * build.warmup); + + Lines.stroke(particleStroke * build.warmup); + + float base = (Time.time / particleLife); + rand.setSeed(build.id); + for(int i = 0; i < particles; i++){ + float fin = (rand.random(1f) + base) % 1f, fout = 1f - fin; + float angle = rand.random(360f); + float len = particleRad * Interp.pow2Out.apply(fin); + Lines.lineAngle(build.x + Angles.trnsx(angle, len), build.y + Angles.trnsy(angle, len), angle, particleLen * fout * build.warmup); + } + + Draw.blend(); + Draw.reset(); + } + + Draw.rect(top, build.x, build.y); + Draw.rect(build.block.region, build.x, build.y); + } + + @Override + public void load(Block block){ + top = Core.atlas.find(block.name + "-top"); + bottom = Core.atlas.find(block.name + "-bottom"); + } + + @Override + public TextureRegion[] icons(Block block){ + return new TextureRegion[]{bottom, block.region, top}; + } +} diff --git a/core/src/mindustry/world/draw/DrawBlock.java b/core/src/mindustry/world/draw/DrawBlock.java index d40d8fe583..4c186f137b 100644 --- a/core/src/mindustry/world/draw/DrawBlock.java +++ b/core/src/mindustry/world/draw/DrawBlock.java @@ -1,16 +1,23 @@ package mindustry.world.draw; import arc.graphics.g2d.*; +import arc.math.*; import mindustry.world.*; import mindustry.world.blocks.production.GenericCrafter.*; /** An implementation of custom rendering behavior for a block. * This is used mostly for mods. */ public class DrawBlock{ + protected static final Rand rand = new Rand(); /** Draws the block. */ - public void draw(GenericCrafterBuild entity){ - Draw.rect(entity.block.region, entity.x, entity.y, entity.block.rotate ? entity.rotdeg() : 0); + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y, build.block.rotate ? build.rotdeg() : 0); + } + + /** Draws any extra light for the block. */ + public void drawLight(GenericCrafterBuild build){ + } /** Load any relevant texture regions. */ diff --git a/core/src/mindustry/world/draw/DrawCells.java b/core/src/mindustry/world/draw/DrawCells.java new file mode 100644 index 0000000000..9730e29f2d --- /dev/null +++ b/core/src/mindustry/world/draw/DrawCells.java @@ -0,0 +1,57 @@ +package mindustry.world.draw; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.graphics.*; +import mindustry.world.*; +import mindustry.world.blocks.production.GenericCrafter.*; + +public class DrawCells extends DrawBlock{ + public TextureRegion bottom, middle; + public Color color = Color.white.cpy(), particleColorFrom = Color.black.cpy(), particleColorTo = Color.black.cpy(); + public int particles = 12; + public float range = 4f, recurrence = 6f, radius = 3f, lifetime = 60f; + + @Override + public void draw(GenericCrafterBuild build){ + + Draw.rect(bottom, build.x, build.y); + + Drawf.liquid(middle, build.x, build.y, build.warmup, color); + + if(build.warmup > 0.001f){ + rand.setSeed(build.id); + for(int i = 0; i < particles; i++){ + float offset = rand.nextFloat() * 999999f; + float x = rand.range(range), y = rand.range(range); + float fin = 1f - (((Time.time + offset) / lifetime) % recurrence); + float ca = rand.random(0.1f, 1f); + float fslope = Mathf.slope(fin); + + if(fin > 0){ + Draw.color(particleColorFrom, particleColorTo, ca); + Draw.alpha(build.warmup); + + Fill.circle(build.x + x, build.y + y, fslope * radius); + } + } + } + + Draw.color(); + Draw.rect(build.block.region, build.x, build.y); + } + + @Override + public void load(Block block){ + bottom = Core.atlas.find(block.name + "-bottom"); + middle = Core.atlas.find(block.name + "-middle"); + } + + @Override + public TextureRegion[] icons(Block block){ + return new TextureRegion[]{bottom, block.region}; + } +} diff --git a/core/src/mindustry/world/draw/DrawCultivator.java b/core/src/mindustry/world/draw/DrawCultivator.java new file mode 100644 index 0000000000..dcfe8c6c95 --- /dev/null +++ b/core/src/mindustry/world/draw/DrawCultivator.java @@ -0,0 +1,56 @@ +package mindustry.world.draw; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.util.*; +import mindustry.graphics.*; +import mindustry.world.*; +import mindustry.world.blocks.production.GenericCrafter.*; + +public class DrawCultivator extends DrawBlock{ + public Color plantColor = Color.valueOf("5541b1"); + public Color plantColorLight = Color.valueOf("7457ce"); + public Color bottomColor = Color.valueOf("474747"); + + public int bubbles = 12, sides = 8; + public float strokeMin = 0.2f, spread = 3f, timeScl = 70f; + public float recurrence = 6f, radius = 3f; + + public TextureRegion middle; + public TextureRegion top; + + @Override + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y); + + Drawf.liquid(middle, build.x, build.y, build.warmup, plantColor); + + Draw.color(bottomColor, plantColorLight, build.warmup); + + rand.setSeed(build.pos()); + for(int i = 0; i < bubbles; i++){ + float x = rand.range(spread), y = rand.range(spread); + float life = 1f - ((Time.time / timeScl + rand.random(recurrence)) % recurrence); + + if(life > 0){ + Lines.stroke(build.warmup * (life + strokeMin)); + Lines.poly(build.x + x, build.y + y, sides, (1f - life) * radius); + } + } + + Draw.color(); + Draw.rect(top, build.x, build.y); + } + + @Override + public void load(Block block){ + middle = Core.atlas.find(block.name + "-middle"); + top = Core.atlas.find(block.name + "-top"); + } + + @Override + public TextureRegion[] icons(Block block){ + return new TextureRegion[]{block.region, top}; + } +} diff --git a/core/src/mindustry/world/draw/DrawGlow.java b/core/src/mindustry/world/draw/DrawGlow.java index d15d477b61..47748cad06 100644 --- a/core/src/mindustry/world/draw/DrawGlow.java +++ b/core/src/mindustry/world/draw/DrawGlow.java @@ -11,10 +11,10 @@ public class DrawGlow extends DrawBlock{ public TextureRegion top; @Override - public void draw(GenericCrafterBuild entity){ - Draw.rect(entity.block.region, entity.x, entity.y); - Draw.alpha(Mathf.absin(entity.totalProgress, glowScale, glowAmount) * entity.warmup); - Draw.rect(top, entity.x, entity.y); + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y); + Draw.alpha(Mathf.absin(build.totalProgress, glowScale, glowAmount) * build.warmup); + Draw.rect(top, build.x, build.y); Draw.reset(); } diff --git a/core/src/mindustry/world/draw/DrawMixer.java b/core/src/mindustry/world/draw/DrawMixer.java index bb785e01f0..850ac5c6dd 100644 --- a/core/src/mindustry/world/draw/DrawMixer.java +++ b/core/src/mindustry/world/draw/DrawMixer.java @@ -10,19 +10,19 @@ public class DrawMixer extends DrawBlock{ public TextureRegion liquid, top, bottom; @Override - public void draw(GenericCrafterBuild entity){ - float rotation = entity.block.rotate ? entity.rotdeg() : 0; + public void draw(GenericCrafterBuild build){ + float rotation = build.block.rotate ? build.rotdeg() : 0; - Draw.rect(bottom, entity.x, entity.y, rotation); + Draw.rect(bottom, build.x, build.y, rotation); - if(entity.liquids.total() > 0.001f){ - Draw.color(((GenericCrafter)entity.block).outputLiquid.liquid.color); - Draw.alpha(entity.liquids.get(((GenericCrafter)entity.block).outputLiquid.liquid) / entity.block.liquidCapacity); - Draw.rect(liquid, entity.x, entity.y, rotation); + if(build.liquids.total() > 0.001f){ + Draw.color(((GenericCrafter)build.block).outputLiquid.liquid.color); + Draw.alpha(build.liquids.get(((GenericCrafter)build.block).outputLiquid.liquid) / build.block.liquidCapacity); + Draw.rect(liquid, build.x, build.y, rotation); Draw.color(); } - Draw.rect(top, entity.x, entity.y, rotation); + Draw.rect(top, build.x, build.y, rotation); } @Override diff --git a/core/src/mindustry/world/draw/DrawRotator.java b/core/src/mindustry/world/draw/DrawRotator.java index ca92aaf351..166245fa6c 100644 --- a/core/src/mindustry/world/draw/DrawRotator.java +++ b/core/src/mindustry/world/draw/DrawRotator.java @@ -9,9 +9,9 @@ public class DrawRotator extends DrawBlock{ public TextureRegion rotator; @Override - public void draw(GenericCrafterBuild entity){ - Draw.rect(entity.block.region, entity.x, entity.y); - Draw.rect(rotator, entity.x, entity.y, entity.totalProgress * 2f); + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y); + Draw.rect(rotator, build.x, build.y, build.totalProgress * 2f); } @Override diff --git a/core/src/mindustry/world/draw/DrawSmelter.java b/core/src/mindustry/world/draw/DrawSmelter.java new file mode 100644 index 0000000000..5b4880eba9 --- /dev/null +++ b/core/src/mindustry/world/draw/DrawSmelter.java @@ -0,0 +1,57 @@ +package mindustry.world.draw; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.graphics.*; +import mindustry.world.*; +import mindustry.world.blocks.production.GenericCrafter.*; + +public class DrawSmelter extends DrawBlock{ + public Color flameColor = Color.valueOf("ffc999"); + public TextureRegion top; + public float lightRadius = 60f, lightAlpha = 0.65f, lightSinScl = 10f, lightSinMag = 5; + public float flameRadius = 3f, flameRadiusIn = 1.9f, flameRadiusScl = 5f, flameRadiusMag = 2f, flameRadiusInMag = 1f; + + public DrawSmelter(){ + } + + public DrawSmelter(Color flameColor){ + this.flameColor = flameColor; + } + + @Override + public void load(Block block){ + top = Core.atlas.find(block.name + "-top"); + } + + @Override + public void draw(GenericCrafterBuild build){ + Draw.rect(build.block.region, build.x, build.y, build.block.rotate ? build.rotdeg() : 0); + + if(build.warmup > 0f && flameColor.a > 0.001f){ + float g = 0.3f; + float r = 0.06f; + float cr = Mathf.random(0.1f); + + Draw.z(Layer.block + 0.01f); + + Draw.alpha(((1f - g) + Mathf.absin(Time.time, 8f, g) + Mathf.random(r) - r) * build.warmup); + + Draw.tint(flameColor); + Fill.circle(build.x, build.y, flameRadius + Mathf.absin(Time.time, flameRadiusScl, flameRadiusMag) + cr); + Draw.color(1f, 1f, 1f, build.warmup); + Draw.rect(top, build.x, build.y); + Fill.circle(build.x, build.y, flameRadiusIn + Mathf.absin(Time.time, flameRadiusScl, flameRadiusInMag) + cr); + + Draw.color(); + } + } + + @Override + public void drawLight(GenericCrafterBuild build){ + Drawf.light(build.team, build.x, build.y, (lightRadius + Mathf.absin(lightSinScl, lightSinMag)) * build.warmup * build.block.size, flameColor, lightAlpha); + } +} diff --git a/core/src/mindustry/world/draw/DrawWeave.java b/core/src/mindustry/world/draw/DrawWeave.java index 5ccd476b5f..0d6c94dcb9 100644 --- a/core/src/mindustry/world/draw/DrawWeave.java +++ b/core/src/mindustry/world/draw/DrawWeave.java @@ -12,22 +12,22 @@ public class DrawWeave extends DrawBlock{ public TextureRegion weave, bottom; @Override - public void draw(GenericCrafterBuild entity){ - Draw.rect(bottom, entity.x, entity.y); - Draw.rect(weave, entity.x, entity.y, entity.totalProgress); + public void draw(GenericCrafterBuild build){ + Draw.rect(bottom, build.x, build.y); + Draw.rect(weave, build.x, build.y, build.totalProgress); Draw.color(Pal.accent); - Draw.alpha(entity.warmup); + Draw.alpha(build.warmup); Lines.lineAngleCenter( - entity.x + Mathf.sin(entity.totalProgress, 6f, Vars.tilesize / 3f * entity.block.size), - entity.y, + build.x + Mathf.sin(build.totalProgress, 6f, Vars.tilesize / 3f * build.block.size), + build.y, 90, - entity.block.size * Vars.tilesize / 2f); + build.block.size * Vars.tilesize / 2f); Draw.reset(); - Draw.rect(entity.block.region, entity.x, entity.y); + Draw.rect(build.block.region, build.x, build.y); } @Override diff --git a/core/src/mindustry/world/meta/Attribute.java b/core/src/mindustry/world/meta/Attribute.java index 07699ba6b3..5114d3c10a 100644 --- a/core/src/mindustry/world/meta/Attribute.java +++ b/core/src/mindustry/world/meta/Attribute.java @@ -2,23 +2,47 @@ package mindustry.world.meta; import mindustry.*; -public enum Attribute{ - /** Heat of this block. Used for calculating output of thermal generators. */ - heat, - /** Spore content of this block. Used for increasing cultivator yield. */ - spores, - /** Water content of this block. Used for increasing water extractor yield. */ - water, - /** Oil content of this block. Used for increasing oil extractor yield. */ - oil, +public class Attribute{ + public static Attribute[] all = {}; + + public static final Attribute + /** Heat content. Used for thermal generator yield. */ + heat = add("heat"), + /** Spore content. Used for cultivator yield. */ + spores = add("spores"), + /** Water content. Used for water extractor yield. */ + water = add("water"), + /** Oil content. Used for oil extractor yield. */ + oil = add("oil"), /** Light coverage. Negative values decrease solar panel efficiency. */ - light; + light = add("light"); - public static final Attribute[] all = values(); + public final int id; + public final String name; - /** @return the envrionmental value for this attribute. */ + /** @return the environmental value for this attribute. */ public float env(){ if(Vars.state == null) return 0; return Vars.state.envAttrs.get(this); } + + Attribute(int id, String name){ + this.id = id; + this.name = name; + } + + @Override + public String toString(){ + return name; + } + + /** Automatically registers this attribute for use. Do not call after mod init. */ + public static Attribute add(String name){ + Attribute a = new Attribute(all.length, name); + Attribute[] prev = all; + all = new Attribute[all.length + 1]; + System.arraycopy(prev, 0, all, 0, a.id); + all[a.id] = a; + return a; + } } diff --git a/core/src/mindustry/world/meta/BlockFlag.java b/core/src/mindustry/world/meta/BlockFlag.java index 2fad22868e..c68e396278 100644 --- a/core/src/mindustry/world/meta/BlockFlag.java +++ b/core/src/mindustry/world/meta/BlockFlag.java @@ -22,10 +22,13 @@ public enum BlockFlag{ resupply, /** Any reactor block. */ reactor, - /** Any block that boosts unit capacity. */ + /** This flag is unused, and will be removed. */ + @Deprecated unitModifier, /** Blocks that extinguishes fires. */ - extinguisher; + extinguisher, + /** Just a launch pad. */ + launchPad; public final static BlockFlag[] all = values(); diff --git a/core/src/mindustry/world/meta/Env.java b/core/src/mindustry/world/meta/Env.java new file mode 100644 index 0000000000..6ebe3a6227 --- /dev/null +++ b/core/src/mindustry/world/meta/Env.java @@ -0,0 +1,14 @@ +package mindustry.world.meta; + +/** Environmental flags for different types of locations. */ +public class Env{ + public static final int + terrestrial = 1, + space = 1 << 1, + underwater = 1 << 2, + spores = 1 << 3, + scorching = 1 << 4, + groundOil = 1 << 5, + groundWater = 1 << 6, + any = 0xffffffff; +} diff --git a/core/src/mindustry/world/meta/Stat.java b/core/src/mindustry/world/meta/Stat.java index f5f92afdfc..c5810c6018 100644 --- a/core/src/mindustry/world/meta/Stat.java +++ b/core/src/mindustry/world/meta/Stat.java @@ -70,6 +70,7 @@ public enum Stat{ speedIncrease(StatCat.function), repairTime(StatCat.function), + repairSpeed(StatCat.function), range(StatCat.function), shootRange(StatCat.function), inaccuracy(StatCat.function), diff --git a/core/src/mindustry/world/meta/StatValues.java b/core/src/mindustry/world/meta/StatValues.java new file mode 100644 index 0000000000..5499cc1ca1 --- /dev/null +++ b/core/src/mindustry/world/meta/StatValues.java @@ -0,0 +1,310 @@ +package mindustry.world.meta; + +import arc.*; +import arc.func.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.scene.ui.*; +import arc.scene.ui.layout.*; +import arc.struct.*; +import arc.util.*; +import mindustry.content.*; +import mindustry.ctype.*; +import mindustry.entities.bullet.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.ui.*; +import mindustry.world.*; +import mindustry.world.blocks.defense.turrets.*; +import mindustry.world.blocks.environment.*; + +import static mindustry.Vars.*; + +/** Utilities for displaying certain stats in a table. */ +public class StatValues{ + + public static StatValue string(String value, Object... args){ + String result = Strings.format(value, args); + return table -> table.add(result); + } + + public static StatValue bool(boolean value){ + return table -> table.add(!value ? "@no" : "@yes"); + } + + public static StatValue number(float value, StatUnit unit){ + return table -> { + int precision = Math.abs((int)value - value) <= 0.001f ? 0 : Math.abs((int)(value * 10) - value * 10) <= 0.001f ? 1 : 2; + + table.add(Strings.fixed(value, precision)); + table.add((unit.space ? " " : "") + unit.localized()); + }; + } + + public static StatValue liquid(Liquid liquid, float amount, boolean perSecond){ + return table -> table.add(new LiquidDisplay(liquid, amount, perSecond)); + } + + public static StatValue liquids(Boolf filter, float amount, boolean perSecond){ + return table -> { + Seq list = new Seq<>(); + + for(Liquid item : content.liquids()){ + if(!item.isHidden() && filter.get(item)) list.add(item); + } + + for(int i = 0; i < list.size; i++){ + table.add(new LiquidDisplay(list.get(i), amount, perSecond)).padRight(5); + + if(i != list.size - 1){ + table.add("/"); + } + } + }; + } + + public static StatValue items(ItemStack... stacks){ + return items(true, stacks); + } + + public static StatValue items(boolean displayName, ItemStack... stacks){ + return table -> { + for(ItemStack stack : stacks){ + table.add(new ItemDisplay(stack.item, stack.amount, displayName)).padRight(5); + } + }; + } + + public static StatValue items(float timePeriod, ItemStack... stacks){ + return table -> { + for(ItemStack stack : stacks){ + table.add(new ItemDisplay(stack.item, stack.amount, timePeriod, true)).padRight(5); + } + }; + } + + public static StatValue items(Boolf filter){ + return items(-1, filter); + } + + public static StatValue items(float timePeriod, Boolf filter){ + return table -> { + Seq list = content.items().select(filter); + + for(int i = 0; i < list.size; i++){ + Item item = list.get(i); + + table.add(timePeriod <= 0 ? new ItemDisplay(item) : new ItemDisplay(item, 0, timePeriod, true)).padRight(5); + + if(i != list.size - 1){ + table.add("/"); + } + } + }; + } + + public static StatValue content(UnlockableContent content){ + return table -> { + table.add(new Image(content.uiIcon)).size(iconSmall).padRight(3); + table.add(content.localizedName).padRight(3); + }; + } + + public static StatValue floorEfficiency(Floor floor, float multiplier, boolean startZero){ + return table -> table.stack( + new Image(floor.uiIcon).setScaling(Scaling.fit), + new Table(t -> t.top().right().add((multiplier < 0 ? "[scarlet]" : startZero ? "[accent]" : "[accent]+") + (int)((multiplier) * 100) + "%").style(Styles.outlineLabel)) + ); + } + + public static StatValue blocks(Boolf pred){ + return blocks(content.blocks().select(pred)); + } + + public static StatValue blocks(Seq list){ + return table -> table.table(l -> { + l.left(); + + for(int i = 0; i < list.size; i++){ + Block item = list.get(i); + + l.image(item.uiIcon).size(iconSmall).padRight(2).padLeft(2).padTop(3).padBottom(3); + l.add(item.localizedName).left().padLeft(1).padRight(4); + if(i % 5 == 4){ + l.row(); + } + } + }); + } + + public static StatValue boosters(float reload, float maxUsed, float multiplier, boolean baseReload, Boolf filter){ + return table -> { + table.row(); + table.table(c -> { + for(Liquid liquid : content.liquids()){ + if(!filter.get(liquid)) continue; + + c.image(liquid.uiIcon).size(3 * 8).padRight(4).right().top(); + c.add(liquid.localizedName).padRight(10).left().top(); + c.table(Tex.underline, bt -> { + bt.left().defaults().padRight(3).left(); + + float reloadRate = (baseReload ? 1f : 0f) + maxUsed * multiplier * liquid.heatCapacity; + float standardReload = baseReload ? reload : reload / (maxUsed * multiplier * 0.4f); + float result = standardReload / (reload / reloadRate); + bt.add(Core.bundle.format("bullet.reload", Strings.autoFixed(result, 2))); + }).left().padTop(-9); + c.row(); + } + }).colspan(table.getColumns()); + table.row(); + }; + } + + public static StatValue strengthBoosters(float multiplier, Boolf filter){ + return table -> { + table.row(); + table.table(c -> { + for(Liquid liquid : content.liquids()){ + if(!filter.get(liquid)) continue; + + c.image(liquid.uiIcon).size(3 * 8).padRight(4).right().top(); + c.add(liquid.localizedName).padRight(10).left().top(); + c.table(Tex.underline, bt -> { + bt.left().defaults().padRight(3).left(); + + float newRate = (1f + multiplier * liquid.heatCapacity); + bt.add(Core.bundle.format("bar.strength", Strings.autoFixed(newRate, 2))); + }).left().padTop(-9); + c.row(); + } + }).colspan(table.getColumns()); + table.row(); + }; + } + + public static StatValue weapons(UnitType unit, Seq weapons){ + return table -> { + table.row(); + for(int i = 0; i < weapons.size;i ++){ + Weapon weapon = weapons.get(i); + + if(weapon.flipSprite){ + //flipped weapons are not given stats + continue; + } + + TextureRegion region = !weapon.name.equals("") && weapon.outlineRegion.found() ? weapon.outlineRegion : unit.fullIcon; + + table.image(region).size(60).scaling(Scaling.bounded).right().top(); + + table.table(Tex.underline, w -> { + w.left().defaults().padRight(3).left(); + + if(weapon.inaccuracy > 0){ + sep(w, "[lightgray]" + Stat.inaccuracy.localized() + ": [white]" + (int)weapon.inaccuracy + " " + StatUnit.degrees.localized()); + } + sep(w, "[lightgray]" + Stat.reload.localized() + ": " + (weapon.mirror ? "2x " : "") + "[white]" + Strings.autoFixed(60f / weapon.reload * weapon.shots, 2)); + + ammo(ObjectMap.of(unit, weapon.bullet)).display(w); + }).padTop(-9).left(); + table.row(); + } + }; + } + + public static StatValue ammo(ObjectMap map){ + return table -> { + + table.row(); + + var orderedKeys = map.keys().toSeq(); + orderedKeys.sort(); + + for(T t : orderedKeys){ + boolean unit = t instanceof UnitType; + + BulletType type = map.get(t); + + //no point in displaying unit icon twice + if(!unit & !(t instanceof PowerTurret)){ + table.image(icon(t)).size(3 * 8).padRight(4).right().top(); + table.add(t.localizedName).padRight(10).left().top(); + } + + table.table(bt -> { + bt.left().defaults().padRight(3).left(); + + if(type.damage > 0 && (type.collides || type.splashDamage <= 0)){ + if(type.continuousDamage() > 0){ + bt.add(Core.bundle.format("bullet.damage", type.continuousDamage()) + StatUnit.perSecond.localized()); + }else{ + bt.add(Core.bundle.format("bullet.damage", type.damage)); + } + } + + if(type.buildingDamageMultiplier != 1){ + sep(bt, Core.bundle.format("bullet.buildingdamage", (int)(type.buildingDamageMultiplier * 100))); + } + + if(type.splashDamage > 0){ + sep(bt, Core.bundle.format("bullet.splashdamage", (int)type.splashDamage, Strings.fixed(type.splashDamageRadius / tilesize, 1))); + } + + if(!unit && !Mathf.equal(type.ammoMultiplier, 1f)){ + sep(bt, Core.bundle.format("bullet.multiplier", (int)type.ammoMultiplier)); + } + + if(!Mathf.equal(type.reloadMultiplier, 1f)){ + sep(bt, Core.bundle.format("bullet.reload", Strings.autoFixed(type.reloadMultiplier, 2))); + } + + if(type.knockback > 0){ + sep(bt, Core.bundle.format("bullet.knockback", Strings.autoFixed(type.knockback, 2))); + } + + if(type.healPercent > 0f){ + sep(bt, Core.bundle.format("bullet.healpercent", (int)type.healPercent)); + } + + if(type.pierce || type.pierceCap != -1){ + sep(bt, type.pierceCap == -1 ? "@bullet.infinitepierce" : Core.bundle.format("bullet.pierce", type.pierceCap)); + } + + if(type.incendAmount > 0){ + sep(bt, "@bullet.incendiary"); + } + + if(type.status != StatusEffects.none){ + sep(bt, (type.minfo.mod == null ? type.status.emoji() : "") + "[stat]" + type.status.localizedName); + } + + if(type.homingPower > 0.01f){ + sep(bt, "@bullet.homing"); + } + + if(type.lightning > 0){ + sep(bt, Core.bundle.format("bullet.lightning", type.lightning, type.lightningDamage < 0 ? type.damage : type.lightningDamage)); + } + + if(type.fragBullet != null){ + sep(bt, "@bullet.frag"); + } + }).padTop(unit ? 0 : -9).left().get().background(unit ? null : Tex.underline); + + table.row(); + } + }; + } + + //for AmmoListValue + + private static void sep(Table table, String text){ + table.row(); + table.add(text); + } + + private static TextureRegion icon(UnlockableContent t){ + return t.uiIcon; + } +} diff --git a/core/src/mindustry/world/meta/Stats.java b/core/src/mindustry/world/meta/Stats.java index 39d887f188..d5d91f2c20 100644 --- a/core/src/mindustry/world/meta/Stats.java +++ b/core/src/mindustry/world/meta/Stats.java @@ -6,7 +6,6 @@ import arc.util.*; import mindustry.*; import mindustry.type.*; import mindustry.world.blocks.environment.*; -import mindustry.world.meta.values.*; /** Hold and organizes a list of block stats. */ public class Stats{ @@ -14,6 +13,8 @@ public class Stats{ public boolean useCategories = false; /** Whether these stats are initialized yet. */ public boolean intialized = false; + /** Production time period in ticks. Used for crafters. **/ + public float timePeriod = -1; @Nullable private OrderedMap>> map; @@ -21,7 +22,7 @@ public class Stats{ /** Adds a single float value with this stat, formatted to 2 decimal places. */ public void add(Stat stat, float value, StatUnit unit){ - add(stat, new NumberValue(value, unit)); + add(stat, StatValues.number(value, unit)); } /** Adds a single float value with this stat and no unit. */ @@ -31,27 +32,27 @@ public class Stats{ /** Adds an integer percent stat value. Value is assumed to be in the 0-1 range. */ public void addPercent(Stat stat, float value){ - add(stat, new NumberValue((int)(value * 100), StatUnit.percent)); + add(stat, StatValues.number((int)(value * 100), StatUnit.percent)); } /** Adds a single y/n boolean value. */ public void add(Stat stat, boolean value){ - add(stat, new BooleanValue(value)); + add(stat, StatValues.bool(value)); } /** Adds an item value. */ public void add(Stat stat, Item item){ - add(stat, new ItemListValue(new ItemStack(item, 1))); + add(stat, StatValues.items(new ItemStack(item, 1))); } /** Adds an item value. */ public void add(Stat stat, ItemStack item){ - add(stat, new ItemListValue(item)); + add(stat, StatValues.items(item)); } /** Adds an item value. */ public void add(Stat stat, Liquid liquid, float amount, boolean perSecond){ - add(stat, new LiquidValue(liquid, amount, perSecond)); + add(stat, StatValues.liquid(liquid, amount, perSecond)); } public void add(Stat stat, Attribute attr){ @@ -70,13 +71,13 @@ public class Stats{ for(var block : Vars.content.blocks() .select(block -> block instanceof Floor f && f.attributes.get(attr) != 0 && !(f.isLiquid && !floating)) .as().with(s -> s.sort(f -> f.attributes.get(attr)))){ - add(stat, new FloorEfficiencyValue(block, block.attributes.get(attr) * scale, startZero)); + add(stat, StatValues.floorEfficiency(block, block.attributes.get(attr) * scale, startZero)); } } /** Adds a single string value with this stat. */ public void add(Stat stat, String format, Object... args){ - add(stat, new StringValue(format, args)); + add(stat, StatValues.string(format, args)); } /** Adds a stat value. */ diff --git a/core/src/mindustry/world/meta/values/AmmoListValue.java b/core/src/mindustry/world/meta/values/AmmoListValue.java deleted file mode 100644 index 9a72a85a24..0000000000 --- a/core/src/mindustry/world/meta/values/AmmoListValue.java +++ /dev/null @@ -1,115 +0,0 @@ -package mindustry.world.meta.values; - -import arc.*; -import arc.graphics.g2d.*; -import arc.math.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import arc.util.*; -import mindustry.content.*; -import mindustry.ctype.*; -import mindustry.entities.bullet.*; -import mindustry.gen.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.blocks.defense.turrets.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class AmmoListValue implements StatValue{ - private final ObjectMap map; - - public AmmoListValue(ObjectMap map){ - this.map = map; - } - - @Override - public void display(Table table){ - - table.row(); - - for(T t : map.keys()){ - boolean unit = t instanceof UnitType; - - BulletType type = map.get(t); - - //no point in displaying unit icon twice - if(!unit & !(t instanceof PowerTurret)){ - table.image(icon(t)).size(3 * 8).padRight(4).right().top(); - table.add(t.localizedName).padRight(10).left().top(); - } - - table.table(bt -> { - bt.left().defaults().padRight(3).left(); - - if(type.damage > 0 && (type.collides || type.splashDamage <= 0)){ - if(type.continuousDamage() > 0){ - bt.add(Core.bundle.format("bullet.damage", type.continuousDamage()) + StatUnit.perSecond.localized()); - }else{ - bt.add(Core.bundle.format("bullet.damage", type.damage)); - } - } - - if(type.buildingDamageMultiplier != 1){ - sep(bt, Core.bundle.format("bullet.buildingdamage", (int)(type.buildingDamageMultiplier * 100))); - } - - if(type.splashDamage > 0){ - sep(bt, Core.bundle.format("bullet.splashdamage", (int)type.splashDamage, Strings.fixed(type.splashDamageRadius / tilesize, 1))); - } - - if(!unit && !Mathf.equal(type.ammoMultiplier, 1f) && !(type instanceof LiquidBulletType)){ - sep(bt, Core.bundle.format("bullet.multiplier", (int)type.ammoMultiplier)); - } - - if(!Mathf.equal(type.reloadMultiplier, 1f)){ - sep(bt, Core.bundle.format("bullet.reload", Strings.autoFixed(type.reloadMultiplier, 2))); - } - - if(type.knockback > 0){ - sep(bt, Core.bundle.format("bullet.knockback", Strings.autoFixed(type.knockback, 2))); - } - - if(type.healPercent > 0f){ - sep(bt, Core.bundle.format("bullet.healpercent", (int)type.healPercent)); - } - - if(type.pierce || type.pierceCap != -1){ - sep(bt, type.pierceCap == -1 ? "@bullet.infinitepierce" : Core.bundle.format("bullet.pierce", type.pierceCap)); - } - - if(type.incendAmount > 0){ - sep(bt, "@bullet.incendiary"); - } - - if(type.status != StatusEffects.none){ - sep(bt, (type.minfo.mod == null ? type.status.emoji() : "") + "[stat]" + type.status.localizedName); - } - - if(type.homingPower > 0.01f){ - sep(bt, "@bullet.homing"); - } - - if(type.lightning > 0){ - sep(bt, Core.bundle.format("bullet.lightning", type.lightning, type.lightningDamage < 0 ? type.damage : type.lightningDamage)); - } - - if(type.fragBullet != null){ - sep(bt, "@bullet.frag"); - } - }).padTop(unit ? 0 : -9).left().get().background(unit ? null : Tex.underline); - - table.row(); - } - } - - void sep(Table table, String text){ - table.row(); - table.add(text); - } - - TextureRegion icon(T t){ - return t.icon(Cicon.medium); - } -} diff --git a/core/src/mindustry/world/meta/values/BlockFilterValue.java b/core/src/mindustry/world/meta/values/BlockFilterValue.java deleted file mode 100644 index 3c4d0fe56e..0000000000 --- a/core/src/mindustry/world/meta/values/BlockFilterValue.java +++ /dev/null @@ -1,37 +0,0 @@ -package mindustry.world.meta.values; - -import arc.func.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import mindustry.ui.*; -import mindustry.world.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class BlockFilterValue implements StatValue{ - public final Boolf pred; - - public BlockFilterValue(Boolf pred){ - this.pred = pred; - } - - @Override - public void display(Table table){ - Seq list = content.blocks().select(pred); - - table.table(l -> { - l.left(); - - for(int i = 0; i < list.size; i++){ - Block item = list.get(i); - - l.image(item.icon(Cicon.small)).size(8 * 3).padRight(2).padLeft(2).padTop(3).padBottom(3); - l.add(item.localizedName).left().padLeft(1).padRight(4); - if(i % 5 == 4){ - l.row(); - } - } - }); - } -} diff --git a/core/src/mindustry/world/meta/values/BlockListValue.java b/core/src/mindustry/world/meta/values/BlockListValue.java deleted file mode 100644 index a1cd5adffe..0000000000 --- a/core/src/mindustry/world/meta/values/BlockListValue.java +++ /dev/null @@ -1,33 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import arc.struct.*; -import mindustry.ui.*; -import mindustry.world.*; -import mindustry.world.meta.*; - -public class BlockListValue implements StatValue{ - public final Seq list; - - public BlockListValue(Seq list){ - this.list = list; - } - - @Override - public void display(Table table){ - - table.table(l -> { - l.left(); - - for(int i = 0; i < list.size; i++){ - Block item = list.get(i); - - l.image(item.icon(Cicon.small)).size(8 * 3).padRight(2).padLeft(2).padTop(3).padBottom(3); - l.add(item.localizedName).left().padLeft(1).padRight(4); - if(i % 5 == 4){ - l.row(); - } - } - }); - } -} diff --git a/core/src/mindustry/world/meta/values/BooleanValue.java b/core/src/mindustry/world/meta/values/BooleanValue.java deleted file mode 100644 index c1da598008..0000000000 --- a/core/src/mindustry/world/meta/values/BooleanValue.java +++ /dev/null @@ -1,17 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import mindustry.world.meta.*; - -public class BooleanValue implements StatValue{ - private final boolean value; - - public BooleanValue(boolean value){ - this.value = value; - } - - @Override - public void display(Table table){ - table.add(!value ? "@no" : "@yes"); - } -} diff --git a/core/src/mindustry/world/meta/values/BoosterListValue.java b/core/src/mindustry/world/meta/values/BoosterListValue.java deleted file mode 100644 index 6f08fc9eac..0000000000 --- a/core/src/mindustry/world/meta/values/BoosterListValue.java +++ /dev/null @@ -1,56 +0,0 @@ -package mindustry.world.meta.values; - -import arc.*; -import arc.func.*; -import arc.scene.ui.layout.*; -import arc.util.*; -import mindustry.gen.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class BoosterListValue implements StatValue{ - protected float reload, maxUsed, multiplier; - protected boolean baseReload; - protected Boolf filter; - - public BoosterListValue(float reload, float maxUsed, float multiplier, boolean baseReload, Boolf filter){ - this.reload = reload; - this.maxUsed = maxUsed; - this.baseReload = baseReload; - this.multiplier = multiplier; - this.filter = filter; - } - - @Override - public void display(Table table){ - - table.row(); - table.table(c -> { - for(Liquid liquid : content.liquids()){ - if(!filter.get(liquid)) continue; - - c.image(liquid.icon(Cicon.medium)).size(3 * 8).padRight(4).right().top(); - c.add(liquid.localizedName).padRight(10).left().top(); - c.table(Tex.underline, bt -> { - bt.left().defaults().padRight(3).left(); - - float reloadRate = (baseReload ? 1f : 0f) + maxUsed * multiplier * liquid.heatCapacity; - float standardReload = baseReload ? reload : reload / (maxUsed * multiplier * 0.4f); - float result = standardReload / (reload / reloadRate); - bt.add(Core.bundle.format("bullet.reload", Strings.autoFixed(result, 2))); - }).left().padTop(-9); - c.row(); - } - }).colspan(table.getColumns()); - table.row(); - - } - - void sep(Table table, String text){ - table.row(); - table.add(text); - } -} diff --git a/core/src/mindustry/world/meta/values/FloorEfficiencyValue.java b/core/src/mindustry/world/meta/values/FloorEfficiencyValue.java deleted file mode 100644 index ad32ecf453..0000000000 --- a/core/src/mindustry/world/meta/values/FloorEfficiencyValue.java +++ /dev/null @@ -1,27 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.*; -import arc.scene.ui.layout.*; -import arc.util.*; -import mindustry.ui.*; -import mindustry.world.blocks.environment.*; -import mindustry.world.meta.*; - -public class FloorEfficiencyValue implements StatValue{ - private final Floor floor; - private final float multiplier; - private final boolean startZero; - - public FloorEfficiencyValue(Floor floor, float multiplier, boolean startZero){ - this.floor = floor; - this.multiplier = multiplier; - this.startZero = startZero; - } - - @Override - public void display(Table table){ - table.stack(new Image(floor.icon(Cicon.medium)).setScaling(Scaling.fit), new Table(t -> { - t.top().right().add((multiplier < 0 ? "[scarlet]" : startZero ? "[accent]" : "[accent]+") + (int)((multiplier) * 100) + "%").style(Styles.outlineLabel); - })); - } -} diff --git a/core/src/mindustry/world/meta/values/FloorValue.java b/core/src/mindustry/world/meta/values/FloorValue.java deleted file mode 100644 index 45ff92bc7d..0000000000 --- a/core/src/mindustry/world/meta/values/FloorValue.java +++ /dev/null @@ -1,21 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.*; -import arc.scene.ui.layout.*; -import mindustry.ui.*; -import mindustry.world.blocks.environment.*; -import mindustry.world.meta.*; - -public class FloorValue implements StatValue{ - private final Floor floor; - - public FloorValue(Floor floor){ - this.floor = floor; - } - - @Override - public void display(Table table){ - table.add(new Image(floor.icon(Cicon.small))).padRight(3); - table.add(floor.localizedName).padRight(3); - } -} diff --git a/core/src/mindustry/world/meta/values/ItemFilterValue.java b/core/src/mindustry/world/meta/values/ItemFilterValue.java deleted file mode 100644 index 78db854658..0000000000 --- a/core/src/mindustry/world/meta/values/ItemFilterValue.java +++ /dev/null @@ -1,33 +0,0 @@ -package mindustry.world.meta.values; - -import arc.func.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class ItemFilterValue implements StatValue{ - private final Boolf filter; - - public ItemFilterValue(Boolf filter){ - this.filter = filter; - } - - @Override - public void display(Table table){ - Seq list = content.items().select(filter); - - for(int i = 0; i < list.size; i++){ - Item item = list.get(i); - - table.add(new ItemDisplay(item)).padRight(5); - - if(i != list.size - 1){ - table.add("/"); - } - } - } -} diff --git a/core/src/mindustry/world/meta/values/ItemListValue.java b/core/src/mindustry/world/meta/values/ItemListValue.java deleted file mode 100644 index 164b6a2a3c..0000000000 --- a/core/src/mindustry/world/meta/values/ItemListValue.java +++ /dev/null @@ -1,27 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -public class ItemListValue implements StatValue{ - private final ItemStack[] stacks; - private final boolean displayName; - - public ItemListValue(ItemStack... stacks){ - this(true, stacks); - } - - public ItemListValue(boolean displayName, ItemStack... stacks){ - this.stacks = stacks; - this.displayName = displayName; - } - - @Override - public void display(Table table){ - for(ItemStack stack : stacks){ - table.add(new ItemDisplay(stack.item, stack.amount, displayName)).padRight(5); - } - } -} diff --git a/core/src/mindustry/world/meta/values/LiquidFilterValue.java b/core/src/mindustry/world/meta/values/LiquidFilterValue.java deleted file mode 100644 index d5aef23376..0000000000 --- a/core/src/mindustry/world/meta/values/LiquidFilterValue.java +++ /dev/null @@ -1,39 +0,0 @@ -package mindustry.world.meta.values; - -import arc.func.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -import static mindustry.Vars.*; - -public class LiquidFilterValue implements StatValue{ - private final Boolf filter; - private final float amount; - private final boolean perSecond; - - public LiquidFilterValue(Boolf filter, float amount, boolean perSecond){ - this.filter = filter; - this.amount = amount; - this.perSecond = perSecond; - } - - @Override - public void display(Table table){ - Seq list = new Seq<>(); - - for(Liquid item : content.liquids()){ - if(!item.isHidden() && filter.get(item)) list.add(item); - } - - for(int i = 0; i < list.size; i++){ - table.add(new LiquidDisplay(list.get(i), amount, perSecond)).padRight(5); - - if(i != list.size - 1){ - table.add("/"); - } - } - } -} diff --git a/core/src/mindustry/world/meta/values/LiquidValue.java b/core/src/mindustry/world/meta/values/LiquidValue.java deleted file mode 100644 index 6139aed5ce..0000000000 --- a/core/src/mindustry/world/meta/values/LiquidValue.java +++ /dev/null @@ -1,23 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -public class LiquidValue implements StatValue{ - private final Liquid liquid; - private final float amount; - private final boolean perSecond; - - public LiquidValue(Liquid liquid, float amount, boolean perSecond){ - this.liquid = liquid; - this.amount = amount; - this.perSecond = perSecond; - } - - @Override - public void display(Table table){ - table.add(new LiquidDisplay(liquid, amount, perSecond)); - } -} diff --git a/core/src/mindustry/world/meta/values/NumberValue.java b/core/src/mindustry/world/meta/values/NumberValue.java deleted file mode 100644 index fa815f4d49..0000000000 --- a/core/src/mindustry/world/meta/values/NumberValue.java +++ /dev/null @@ -1,27 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import arc.util.*; -import mindustry.world.meta.*; - -/** - * A stat that is a number with a unit attacked. - * The number is rounded to 2 decimal places by default. - */ -public class NumberValue implements StatValue{ - private final StatUnit unit; - private final float value; - - public NumberValue(float value, StatUnit unit){ - this.unit = unit; - this.value = value; - } - - @Override - public void display(Table table){ - int precision = Math.abs((int)value - value) <= 0.001f ? 0 : Math.abs((int)(value * 10) - value * 10) <= 0.001f ? 1 : 2; - - table.add(Strings.fixed(value, precision)); - table.add((unit.space ? " " : "") + unit.localized()); - } -} diff --git a/core/src/mindustry/world/meta/values/StringValue.java b/core/src/mindustry/world/meta/values/StringValue.java deleted file mode 100644 index b36a9d47e4..0000000000 --- a/core/src/mindustry/world/meta/values/StringValue.java +++ /dev/null @@ -1,18 +0,0 @@ -package mindustry.world.meta.values; - -import arc.scene.ui.layout.*; -import arc.util.*; -import mindustry.world.meta.*; - -public class StringValue implements StatValue{ - private final String value; - - public StringValue(String value, Object... args){ - this.value = Strings.format(value, args); - } - - @Override - public void display(Table table){ - table.add(value); - } -} diff --git a/core/src/mindustry/world/meta/values/WeaponListValue.java b/core/src/mindustry/world/meta/values/WeaponListValue.java deleted file mode 100644 index 6833837f5c..0000000000 --- a/core/src/mindustry/world/meta/values/WeaponListValue.java +++ /dev/null @@ -1,55 +0,0 @@ -package mindustry.world.meta.values; - -import arc.graphics.g2d.*; -import arc.scene.ui.layout.*; -import arc.struct.*; -import arc.util.*; -import mindustry.gen.*; -import mindustry.type.*; -import mindustry.ui.*; -import mindustry.world.meta.*; - -public class WeaponListValue implements StatValue{ - private final Seq weapons; - private final UnitType unit; - - public WeaponListValue(UnitType unit, Seq weapons){ - this.weapons = weapons; - this.unit = unit; - } - - @Override - public void display(Table table){ - table.row(); - for(int i = 0; i < weapons.size;i ++){ - Weapon weapon = weapons.get(i); - - if(weapon.flipSprite){ - //flipped weapons are not given stats - continue; - } - - TextureRegion region = !weapon.name.equals("") && weapon.outlineRegion.found() ? weapon.outlineRegion : unit.icon(Cicon.full); - - table.image(region).size(60).scaling(Scaling.bounded).right().top(); - - table.table(Tex.underline, w -> { - w.left().defaults().padRight(3).left(); - - if(weapon.inaccuracy > 0){ - sep(w, "[lightgray]" + Stat.inaccuracy.localized() + ": [white]" + (int)weapon.inaccuracy + " " + StatUnit.degrees.localized()); - } - sep(w, "[lightgray]" + Stat.reload.localized() + ": " + (weapon.mirror ? "2x " : "") + "[white]" + Strings.autoFixed(60f / weapon.reload * weapon.shots, 2)); - - var bullet = new AmmoListValue(OrderedMap.of(unit, weapon.bullet)); - bullet.display(w); - }).padTop(-9).left(); - table.row(); - } - } - - void sep(Table table, String text){ - table.row(); - table.add(text); - } -} diff --git a/core/src/mindustry/world/modules/ConsumeModule.java b/core/src/mindustry/world/modules/ConsumeModule.java index 431fa3fd1f..4714903c5d 100644 --- a/core/src/mindustry/world/modules/ConsumeModule.java +++ b/core/src/mindustry/world/modules/ConsumeModule.java @@ -66,6 +66,10 @@ public class ConsumeModule extends BlockModule{ return valid && entity.shouldConsume() && entity.enabled; } + public boolean canConsume(){ + return valid && entity.enabled; + } + public boolean optionalValid(){ return valid() && optionalValid && entity.enabled; } diff --git a/desktop/build.gradle b/desktop/build.gradle index 81459ed156..948b5413f9 100644 --- a/desktop/build.gradle +++ b/desktop/build.gradle @@ -18,7 +18,7 @@ task run(dependsOn: classes, type: JavaExec){ ignoreExitValue = true if(System.getProperty("os.name").toLowerCase().contains("mac")){ - jvmArgs("-XstartOnFirstThread", "-Djava.awt.headless=true") + jvmArgs("-XstartOnFirstThread") } jvmArgs += "-XX:+ShowCodeDetailsInExceptionMessages" @@ -42,6 +42,7 @@ task dist(type: Jar, dependsOn: configurations.runtimeClasspath){ from files(sourceSets.main.output.resourcesDir) from {configurations.runtimeClasspath.collect{ it.isDirectory() ? it : zipTree(it) }} from files(project.assetsDir) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE archiveFileName = "${appName}.jar" @@ -56,6 +57,7 @@ if(!project.ext.hasSprites()){ dist.dependsOn ":tools:pack" } +//this is only for local testing task steamtest(dependsOn: dist){ doLast{ copy{ @@ -70,6 +72,7 @@ task steamtest(dependsOn: dist){ } } +//TODO replace with jpackage templates PackrConfig.Platform.values().each{ platform -> task "packr${platform.toString()}"{ dependsOn dist diff --git a/desktop/src/mindustry/desktop/DesktopLauncher.java b/desktop/src/mindustry/desktop/DesktopLauncher.java index 29fe405905..a1438cbbcd 100644 --- a/desktop/src/mindustry/desktop/DesktopLauncher.java +++ b/desktop/src/mindustry/desktop/DesktopLauncher.java @@ -4,13 +4,14 @@ import arc.*; import arc.Files.*; import arc.backend.sdl.*; import arc.backend.sdl.jni.*; +import arc.discord.*; +import arc.discord.DiscordRPC.*; import arc.files.*; import arc.func.*; import arc.math.*; import arc.struct.*; import arc.util.*; import arc.util.serialization.*; -import club.minnced.discord.rpc.*; import com.codedisaster.steamworks.*; import mindustry.*; import mindustry.core.*; @@ -19,6 +20,7 @@ import mindustry.game.EventType.*; import mindustry.gen.*; import mindustry.net.*; import mindustry.net.Net.*; +import mindustry.service.*; import mindustry.type.*; import java.io.*; @@ -26,8 +28,8 @@ import java.io.*; import static mindustry.Vars.*; public class DesktopLauncher extends ClientLauncher{ - public final static String discordID = "610508934456934412"; - boolean useDiscord = OS.is64Bit && !OS.isARM && !OS.hasProp("nodiscord"), loadError = false; + public final static long discordID = 610508934456934412L; + boolean useDiscord = !OS.hasProp("nodiscord"), loadError = false; Throwable steamError; public static void main(String[] arg){ @@ -38,6 +40,10 @@ public class DesktopLauncher extends ClientLauncher{ maximized = true; width = 900; height = 700; + //enable gl3 with command-line argument + if(Structs.contains(arg, "-gl3")){ + gl30 = true; + } setWindowIcon(FileType.internal, "icons/icon_64.png"); }}); }catch(Throwable e){ @@ -52,13 +58,15 @@ public class DesktopLauncher extends ClientLauncher{ if(useDiscord){ try{ - DiscordRPC.INSTANCE.Discord_Initialize(discordID, null, true, "1127400"); + DiscordRPC.connect(discordID); Log.info("Initialized Discord rich presence."); - Runtime.getRuntime().addShutdownHook(new Thread(DiscordRPC.INSTANCE::Discord_Shutdown)); + Runtime.getRuntime().addShutdownHook(new Thread(DiscordRPC::close)); + }catch(NoDiscordClientException none){ + //don't log if no client is found + useDiscord = false; }catch(Throwable t){ useDiscord = false; - Log.err("Failed to initialize discord. Enable debug logging for details."); - Log.debug("Discord init error: \n@\n", Strings.getStackTrace(t)); + Log.warn("Failed to initialize Discord RPC - you are likely using a JVM <16."); } } @@ -122,6 +130,40 @@ public class DesktopLauncher extends ClientLauncher{ SVars.user = new SUser(); boolean[] isShutdown = {false}; + service = new GameService(){ + + @Override + public boolean enabled(){ + return true; + } + + @Override + public void completeAchievement(String name){ + SVars.stats.stats.setAchievement(name); + SVars.stats.stats.storeStats(); + } + + @Override + public boolean isAchieved(String name){ + return SVars.stats.stats.isAchieved(name, false); + } + + @Override + public int getStat(String name, int def){ + return SVars.stats.stats.getStatI(name, def); + } + + @Override + public void setStat(String name, int amount){ + SVars.stats.stats.setStatI(name, amount); + } + + @Override + public void storeStats(){ + SVars.stats.onUpdate(); + } + }; + Events.on(ClientLoadEvent.class, event -> { Core.settings.defaults("name", SVars.net.friends.getPersonaName()); if(player.name.isEmpty()){ @@ -246,7 +288,6 @@ public class DesktopLauncher extends ClientLauncher{ String uiState = ""; if(inGame){ - //TODO implement nice name for sector gameMapWithWave = Strings.capitalize(Strings.stripColors(state.map.name())); if(state.rules.waves){ @@ -267,7 +308,7 @@ public class DesktopLauncher extends ClientLauncher{ } if(useDiscord){ - DiscordRichPresence presence = new DiscordRichPresence(); + RichPresence presence = new RichPresence(); if(inGame){ presence.state = gameMode + gamePlayersSuffix; @@ -281,7 +322,7 @@ public class DesktopLauncher extends ClientLauncher{ presence.largeImageKey = "logo"; - DiscordRPC.INSTANCE.Discord_UpdatePresence(presence); + DiscordRPC.send(presence); } if(steam){ diff --git a/desktop/src/mindustry/desktop/steam/SNet.java b/desktop/src/mindustry/desktop/steam/SNet.java index d016cac1a4..750c40cd42 100644 --- a/desktop/src/mindustry/desktop/steam/SNet.java +++ b/desktop/src/mindustry/desktop/steam/SNet.java @@ -4,9 +4,7 @@ import arc.*; import arc.func.*; import arc.struct.*; import arc.util.*; -import arc.util.pooling.*; import com.codedisaster.steamworks.*; -import com.codedisaster.steamworks.SteamFriends.*; import com.codedisaster.steamworks.SteamMatchmaking.*; import com.codedisaster.steamworks.SteamNetworking.*; import mindustry.core.*; @@ -58,6 +56,11 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, int fromID = from.getAccountID(); Object output = serializer.read(readBuffer); + //it may be theoretically possible for this to be a framework message, if the packet is malicious or corrupted + if(!(output instanceof Packet)) return; + + Packet pack = (Packet)output; + if(net.server()){ SteamConnection con = steamConnections.get(fromID); try{ @@ -74,13 +77,13 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, net.handleServerReceived(con, c); } - net.handleServerReceived(con, output); + net.handleServerReceived(con, pack); }catch(Throwable e){ Log.err(e); } }else if(currentServer != null && fromID == currentServer.getAccountID()){ try{ - net.handleClientReceived(output); + net.handleClientReceived(pack); }catch(Throwable t){ net.handleException(t); } @@ -120,7 +123,7 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } @Override - public void sendClient(Object object, SendMode mode){ + public void sendClient(Object object, boolean reliable){ if(isSteamClient()){ if(currentServer == null){ Log.info("Not connected, quitting."); @@ -134,17 +137,15 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, int length = writeBuffer.position(); writeBuffer.flip(); - snet.sendP2PPacket(currentServer, writeBuffer, mode == SendMode.tcp || length >= 1200 ? P2PSend.Reliable : P2PSend.UnreliableNoDelay, 0); + snet.sendP2PPacket(currentServer, writeBuffer, reliable || length >= 1200 ? P2PSend.Reliable : P2PSend.UnreliableNoDelay, 0); }catch(Exception e){ net.showError(e); } - Pools.free(object); }else{ - provider.sendClient(object, mode); + provider.sendClient(object, reliable); } } - @Override public void disconnectClient(){ if(isSteamClient()){ @@ -163,6 +164,7 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, @Override public void discoverServers(Cons callback, Runnable done){ smat.addRequestLobbyListResultCountFilter(32); + smat.addRequestLobbyListDistanceFilter(LobbyDistanceFilter.Worldwide); smat.requestLobbyList(); lobbyCallback = callback; @@ -226,11 +228,6 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } } - @Override - public void onFavoritesListChanged(int i, int i1, int i2, int i3, int i4, boolean b, int i5){ - - } - @Override public void onLobbyInvite(SteamID steamIDUser, SteamID steamIDLobby, long gameID){ Log.info("onLobbyInvite @ @ @", steamIDLobby.getAccountID(), steamIDUser.getAccountID(), gameID); @@ -279,11 +276,6 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, Core.app.post(() -> Core.app.post(() -> Core.app.post(() -> Log.info("Server: @\nClient: @\nActive: @", net.server(), net.client(), net.active())))); } - @Override - public void onLobbyDataUpdate(SteamID steamID, SteamID steamID1, boolean b){ - - } - @Override public void onLobbyChatUpdate(SteamID lobby, SteamID who, SteamID changer, ChatMemberStateChange change){ Log.info("lobby @: @ caused @'s change: @", lobby.getAccountID(), who.getAccountID(), changer.getAccountID(), change); @@ -301,16 +293,6 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } } - @Override - public void onLobbyChatMessage(SteamID steamID, SteamID steamID1, ChatEntryType chatEntryType, int i){ - - } - - @Override - public void onLobbyGameCreated(SteamID steamID, SteamID steamID1, int i, short i1){ - - } - @Override public void onLobbyMatchList(int matches){ Log.info("found @ matches @", matches, lobbyDoneCallback); @@ -331,7 +313,7 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, smat.getNumLobbyMembers(lobby), Strings.parseInt(smat.getLobbyData(lobby, "version"), -1), smat.getLobbyData(lobby, "versionType"), - mode == null || mode.isEmpty() ? Gamemode.survival : Gamemode.valueOf(mode), + Gamemode.valueOf(mode), smat.getLobbyMemberLimit(lobby), "", null @@ -349,11 +331,6 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } } - @Override - public void onLobbyKicked(SteamID steamID, SteamID steamID1, boolean b){ - Log.info("Kicked: @ @ @", steamID, steamID1, b); - } - @Override public void onLobbyCreated(SteamResult result, SteamID steamID){ if(!net.server()){ @@ -381,11 +358,6 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } } - @Override - public void onFavoritesListAccountsUpdated(SteamResult steamResult){ - - } - @Override public void onP2PSessionConnectFail(SteamID steamIDRemote, P2PSessionError sessionError){ if(net.server()){ @@ -406,50 +378,14 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } } - @Override - public void onSetPersonaNameResponse(boolean b, boolean b1, SteamResult steamResult){ - - } - - @Override - public void onPersonaStateChange(SteamID steamID, PersonaChange personaChange){ - - } - - @Override - public void onGameOverlayActivated(boolean b){ - - } - @Override public void onGameLobbyJoinRequested(SteamID lobby, SteamID steamIDFriend){ Log.info("onGameLobbyJoinRequested @ @", lobby, steamIDFriend); smat.joinLobby(lobby); } - @Override - public void onAvatarImageLoaded(SteamID steamID, int i, int i1, int i2){ - - } - - @Override - public void onFriendRichPresenceUpdate(SteamID steamID, int i){ - - } - - @Override - public void onGameRichPresenceJoinRequested(SteamID steamID, String connect){ - Log.info("onGameRichPresenceJoinRequested @ @", steamID, connect); - } - - @Override - public void onGameServerChangeRequested(String server, String password){ - - } - public class SteamConnection extends NetConnection{ final SteamID sid; - final P2PSessionState state = new P2PSessionState(); public SteamConnection(SteamID sid){ super(sid.getAccountID() + ""); @@ -458,7 +394,7 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, } @Override - public void send(Object object, SendMode mode){ + public void send(Object object, boolean reliable){ try{ writeBuffer.limit(writeBuffer.capacity()); writeBuffer.position(0); @@ -466,7 +402,7 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, int length = writeBuffer.position(); writeBuffer.flip(); - snet.sendP2PPacket(sid, writeBuffer, mode == SendMode.tcp || length >= 1200 ? object instanceof StreamChunk ? P2PSend.ReliableWithBuffering : P2PSend.Reliable : P2PSend.UnreliableNoDelay, 0); + snet.sendP2PPacket(sid, writeBuffer, reliable || length >= 1200 ? object instanceof StreamChunk ? P2PSend.ReliableWithBuffering : P2PSend.Reliable : P2PSend.UnreliableNoDelay, 0); }catch(Exception e){ Log.err(e); Log.info("Error sending packet. Disconnecting invalid client!"); @@ -479,7 +415,8 @@ public class SNet implements SteamNetworkingCallback, SteamMatchmakingCallback, @Override public boolean isConnected(){ - snet.getP2PSessionState(sid, state); + //TODO ??? + //snet.getP2PSessionState(sid, state); return true;//state.isConnectionActive(); } diff --git a/desktop/src/mindustry/desktop/steam/SStats.java b/desktop/src/mindustry/desktop/steam/SStats.java index 204c8a98b2..1a5ed54b77 100644 --- a/desktop/src/mindustry/desktop/steam/SStats.java +++ b/desktop/src/mindustry/desktop/steam/SStats.java @@ -1,63 +1,27 @@ package mindustry.desktop.steam; import arc.*; -import arc.struct.*; import arc.util.*; import com.codedisaster.steamworks.*; -import mindustry.*; -import mindustry.content.*; -import mindustry.entities.units.*; import mindustry.game.EventType.*; -import mindustry.game.SectorInfo.*; -import mindustry.gen.*; -import mindustry.type.*; -import mindustry.world.*; -import mindustry.world.blocks.distribution.*; import static mindustry.Vars.*; -import static mindustry.desktop.steam.SAchievement.*; -@SuppressWarnings("unchecked") public class SStats implements SteamUserStatsCallback{ public final SteamUserStats stats = new SteamUserStats(this); private boolean updated = false; private int statSavePeriod = 4; //in minutes - private ObjectSet blocksBuilt = new ObjectSet<>(), unitsBuilt = new ObjectSet<>(); - private ObjectSet t5s = new ObjectSet<>(); - private IntSet checked = new IntSet(); - public SStats(){ stats.requestCurrentStats(); Events.on(ClientLoadEvent.class, e -> { - unitsBuilt = Core.settings.getJson("units-built" , ObjectSet.class, String.class, ObjectSet::new); - blocksBuilt = Core.settings.getJson("blocks-built" , ObjectSet.class, String.class, ObjectSet::new); - t5s = ObjectSet.with(UnitTypes.omura, UnitTypes.reign, UnitTypes.toxopid, UnitTypes.eclipse, UnitTypes.oct, UnitTypes.corvus); - - Core.app.addListener(new ApplicationListener(){ - Interval i = new Interval(); - - @Override - public void update(){ - if(i.get(60f)){ - checkUpdate(); - } - } - }); - Timer.schedule(() -> { if(updated){ stats.storeStats(); } }, statSavePeriod * 60, statSavePeriod * 60); - - if(Items.thorium.unlocked()) obtainThorium.complete(); - if(Items.titanium.unlocked()) obtainTitanium.complete(); - if(!content.sectors().contains(s -> s.locked())){ - unlockAllZones.complete(); - } }); } @@ -65,287 +29,9 @@ public class SStats implements SteamUserStatsCallback{ this.updated = true; } - void checkUpdate(){ - if(campaign()){ - SStat.maxUnitActive.max(Groups.unit.count(t -> t.team == player.team())); - - if(Groups.unit.count(u -> u.type == UnitTypes.poly && u.team == player.team()) >= 10){ - active10Polys.complete(); - } - - for(Building entity : player.team().cores()){ - if(!content.items().contains(i -> entity.items.get(i) < entity.block.itemCapacity)){ - fillCoreAllCampaign.complete(); - break; - } - } - } - } - - private void registerEvents(){ - - Events.on(UnitDestroyEvent.class, e -> { - if(campaign()){ - if(e.unit.team != Vars.player.team()){ - SStat.unitsDestroyed.add(); - - if(e.unit.isBoss()){ - SStat.bossesDefeated.add(); - } - } - } - }); - - Events.on(TurnEvent.class, e -> { - float total = 0; - for(Planet planet : content.planets()){ - for(Sector sec : planet.sectors){ - if(sec.hasBase()){ - for(ExportStat v : sec.info.production.values()){ - if(v.mean > 0) total += v.mean * 60; - } - } - } - } - - SStat.maxProduction.max(Math.round(total)); - }); - - Events.run(Trigger.newGame, () -> Core.app.post(() -> { - if(campaign() && player.core() != null && player.core().items.total() >= 10 * 1000){ - drop10kitems.complete(); - } - })); - - Events.on(CommandIssueEvent.class, e -> { - if(campaign() && e.command == UnitCommand.attack){ - issueAttackCommand.complete(); - } - }); - - Events.on(BlockBuildEndEvent.class, e -> { - if(campaign() && e.unit != null && e.unit.isLocal() && !e.breaking){ - SStat.blocksBuilt.add(); - - if(e.tile.block() == Blocks.router && e.tile.build.proximity().contains(t -> t.block == Blocks.router)){ - chainRouters.complete(); - } - - if(e.tile.block() == Blocks.groundFactory){ - buildGroundFactory.complete(); - } - - if(blocksBuilt.add(e.tile.block().name)){ - if(blocksBuilt.contains("meltdown") && blocksBuilt.contains("spectre") && blocksBuilt.contains("foreshadow")){ - buildMeltdownSpectre.complete(); - } - - save(); - } - - if(e.tile.block() instanceof Conveyor){ - checked.clear(); - check: { - Tile current = e.tile; - for(int i = 0; i < 4; i++){ - checked.add(current.pos()); - if(current.build == null) break check; - Tile next = current.nearby(current.build.rotation); - if(next != null && next.block() instanceof Conveyor){ - current = next; - }else{ - break check; - } - } - - if(current == e.tile && checked.size == 4){ - circleConveyor.complete(); - } - } - } - } - }); - - Events.on(UnitCreateEvent.class, e -> { - if(campaign()){ - if(unitsBuilt.add(e.unit.type.name)){ - SStat.unitTypesBuilt.set(content.units().count(u -> unitsBuilt.contains(u.name) && !u.isHidden())); - } - - if(t5s.contains(e.unit.type)){ - buildT5.complete(); - } - } - }); - - Events.on(UnitControlEvent.class, e -> { - if(e.unit instanceof BlockUnitc && ((BlockUnitc)e.unit).tile().block == Blocks.router){ - becomeRouter.complete(); - } - }); - - Events.on(SchematicCreateEvent.class, e -> { - SStat.schematicsCreated.add(); - }); - - Events.on(BlockDestroyEvent.class, e -> { - if(campaign() && e.tile.team() != player.team()){ - SStat.blocksDestroyed.add(); - } - }); - - Events.on(MapMakeEvent.class, e -> SStat.mapsMade.add()); - - Events.on(MapPublishEvent.class, e -> SStat.mapsPublished.add()); - - Events.on(UnlockEvent.class, e -> { - if(e.content == Items.thorium) obtainThorium.complete(); - if(e.content == Items.titanium) obtainTitanium.complete(); - if(e.content instanceof SectorPreset && !content.sectors().contains(s -> s.locked())){ - unlockAllZones.complete(); - } - }); - - Events.run(Trigger.openWiki, openWiki::complete); - - Events.run(Trigger.exclusionDeath, dieExclusion::complete); - - Events.on(UnitDrownEvent.class, e -> { - if(campaign() && e.unit.isPlayer()){ - drown.complete(); - } - }); - - trigger(Trigger.acceleratorUse, useAccelerator); - - trigger(Trigger.impactPower, powerupImpactReactor); - - trigger(Trigger.flameAmmo, useFlameAmmo); - - trigger(Trigger.turretCool, coolTurret); - - trigger(Trigger.suicideBomb, suicideBomb); - - Events.run(Trigger.enablePixelation, enablePixelation::complete); - - Events.run(Trigger.thoriumReactorOverheat, () -> { - if(campaign()){ - SStat.reactorsOverheated.add(); - } - }); - - trigger(Trigger.shock, shockWetEnemy); - - trigger(Trigger.phaseDeflectHit, killEnemyPhaseWall); - - Events.on(LaunchItemEvent.class, e -> { - if(campaign()){ - launchItemPad.complete(); - } - }); - - Events.on(PickupEvent.class, e -> { - if(e.carrier.isPlayer() && campaign() && e.unit != null && t5s.contains(e.unit.type)){ - pickupT5.complete(); - } - }); - - Events.on(UnitCreateEvent.class, e -> { - if(campaign() && e.unit.team() == player.team()){ - SStat.unitsBuilt.add(); - } - }); - - Events.on(SectorLaunchEvent.class, e -> { - SStat.timesLaunched.add(); - }); - - Events.on(LaunchItemEvent.class, e -> { - SStat.itemsLaunched.add(e.stack.amount); - }); - - Events.on(WaveEvent.class, e -> { - if(campaign()){ - SStat.maxWavesSurvived.max(Vars.state.wave); - - if(state.stats.buildingsBuilt == 0 && state.wave >= 10){ - survive10WavesNoBlocks.complete(); - } - } - }); - - Events.on(PlayerJoin.class, e -> { - if(Vars.net.server()){ - SStat.maxPlayersServer.max(Groups.player.size()); - } - }); - - Runnable checkUnlocks = () -> { - if(Blocks.router.unlocked()) researchRouter.complete(); - - if(!TechTree.all.contains(t -> t.content.locked())){ - researchAll.complete(); - } - }; - - //check unlocked stuff on load as well - Events.on(ResearchEvent.class, e -> checkUnlocks.run()); - Events.on(UnlockEvent.class, e -> checkUnlocks.run()); - Events.on(ClientLoadEvent.class, e -> checkUnlocks.run()); - - Events.on(WinEvent.class, e -> { - if(state.rules.pvp){ - SStat.pvpsWon.add(); - } - }); - - Events.on(SectorCaptureEvent.class, e -> { - if(e.sector.isBeingPlayed() || net.client()){ - if(Vars.state.wave <= 5 && state.rules.attackMode){ - defeatAttack5Waves.complete(); - } - - if(state.stats.buildingsDestroyed == 0){ - captureNoBlocksBroken.complete(); - } - } - - if(Vars.state.rules.attackMode){ - SStat.attacksWon.add(); - } - - if(!e.sector.isBeingPlayed() && !net.client()){ - captureBackground.complete(); - } - - if(!e.sector.planet.sectors.contains(s -> !s.hasBase())){ - captureAllSectors.complete(); - } - - SStat.sectorsControlled.set(e.sector.planet.sectors.count(Sector::hasBase)); - }); - } - - private void save(){ - Core.settings.putJson("units-built" , String.class, unitsBuilt); - Core.settings.putJson("blocks-built" , String.class, blocksBuilt); - } - - private void trigger(Trigger trigger, SAchievement ach){ - Events.run(trigger, () -> { - if(campaign()){ - ach.complete(); - } - }); - } - - private boolean campaign(){ - return Vars.state.isCampaign(); - } - @Override public void onUserStatsReceived(long gameID, SteamID steamID, SteamResult result){ - registerEvents(); + service.init(); if(result != SteamResult.OK){ Log.err("Failed to receive steam stats: @", result); @@ -362,34 +48,4 @@ public class SStats implements SteamUserStatsCallback{ updated = true; } } - - @Override - public void onUserStatsUnloaded(SteamID steamID){ - - } - - @Override - public void onUserAchievementStored(long l, boolean b, String s, int i, int i1){ - - } - - @Override - public void onLeaderboardFindResult(SteamLeaderboardHandle steamLeaderboardHandle, boolean b){ - - } - - @Override - public void onLeaderboardScoresDownloaded(SteamLeaderboardHandle steamLeaderboardHandle, SteamLeaderboardEntriesHandle steamLeaderboardEntriesHandle, int i){ - - } - - @Override - public void onLeaderboardScoreUploaded(boolean b, SteamLeaderboardHandle steamLeaderboardHandle, int i, boolean b1, int i1, int i2){ - - } - - @Override - public void onGlobalStatsReceived(long l, SteamResult steamResult){ - - } } diff --git a/desktop/src/mindustry/desktop/steam/SUser.java b/desktop/src/mindustry/desktop/steam/SUser.java index 730b12763e..1b368e7982 100644 --- a/desktop/src/mindustry/desktop/steam/SUser.java +++ b/desktop/src/mindustry/desktop/steam/SUser.java @@ -1,23 +1,7 @@ package mindustry.desktop.steam; import com.codedisaster.steamworks.*; -import com.codedisaster.steamworks.SteamAuth.*; public class SUser implements SteamUserCallback{ public final SteamUser user = new SteamUser(this); - - @Override - public void onValidateAuthTicket(SteamID steamID, AuthSessionResponse authSessionResponse, SteamID ownerSteamID){ - - } - - @Override - public void onMicroTxnAuthorization(int appID, long orderID, boolean authorized){ - - } - - @Override - public void onEncryptedAppTicket(SteamResult result){ - - } } diff --git a/desktop/src/mindustry/desktop/steam/SWorkshop.java b/desktop/src/mindustry/desktop/steam/SWorkshop.java index ac6e5f5060..23a22360d4 100644 --- a/desktop/src/mindustry/desktop/steam/SWorkshop.java +++ b/desktop/src/mindustry/desktop/steam/SWorkshop.java @@ -1,18 +1,19 @@ package mindustry.desktop.steam; import arc.*; -import com.codedisaster.steamworks.*; -import com.codedisaster.steamworks.SteamRemoteStorage.*; -import com.codedisaster.steamworks.SteamUGC.*; -import arc.struct.*; import arc.files.*; import arc.func.*; import arc.scene.ui.*; +import arc.struct.*; import arc.util.*; +import com.codedisaster.steamworks.*; +import com.codedisaster.steamworks.SteamRemoteStorage.*; +import com.codedisaster.steamworks.SteamUGC.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.maps.*; import mindustry.mod.Mods.*; +import mindustry.service.*; import mindustry.type.*; import mindustry.ui.dialogs.*; @@ -42,7 +43,7 @@ public class SWorkshop implements SteamUGCCallback{ workshopFiles.put(LoadedMod.class, folders.select(f -> f.child("mod.json").exists() || f.child("mod.hjson").exists())); if(!workshopFiles.get(Map.class).isEmpty()){ - SAchievement.downloadMapWorkshop.complete(); + Achievement.downloadMapWorkshop.complete(); } workshopFiles.each((type, list) -> { @@ -171,7 +172,7 @@ public class SWorkshop implements SteamUGCCallback{ ugc.submitItemUpdate(h, changelog == null ? "" : changelog); if(p instanceof Map){ - SAchievement.publishMap.complete(); + Achievement.publishMap.complete(); } }, () -> p.addSteamID(sid)); } @@ -228,11 +229,6 @@ public class SWorkshop implements SteamUGCCallback{ } } - @Override - public void onRequestUGCDetails(SteamUGCDetails details, SteamResult result){ - - } - @Override public void onUGCQueryCompleted(SteamUGCQuery query, int numResultsReturned, int totalMatchingResults, boolean isCachedData, SteamResult result){ Log.info("GET QUERY " + query); @@ -263,7 +259,7 @@ public class SWorkshop implements SteamUGCCallback{ ItemInstallInfo info = new ItemInstallInfo(); ugc.getItemInstallInfo(publishedFileID, info); Log.info("Item subscribed from @", info.getFolder()); - SAchievement.downloadMapWorkshop.complete(); + Achievement.downloadMapWorkshop.complete(); } @Override @@ -311,42 +307,12 @@ public class SWorkshop implements SteamUGCCallback{ @Override public void onDownloadItemResult(int appID, SteamPublishedFileID publishedFileID, SteamResult result){ - SAchievement.downloadMapWorkshop.complete(); + Achievement.downloadMapWorkshop.complete(); ItemInstallInfo info = new ItemInstallInfo(); ugc.getItemInstallInfo(publishedFileID, info); Log.info("Item downloaded to @", info.getFolder()); } - @Override - public void onUserFavoriteItemsListChanged(SteamPublishedFileID publishedFileID, boolean wasAddRequest, SteamResult result){ - - } - - @Override - public void onSetUserItemVote(SteamPublishedFileID publishedFileID, boolean voteUp, SteamResult result){ - - } - - @Override - public void onGetUserItemVote(SteamPublishedFileID publishedFileID, boolean votedUp, boolean votedDown, boolean voteSkipped, SteamResult result){ - - } - - @Override - public void onStartPlaytimeTracking(SteamResult result){ - - } - - @Override - public void onStopPlaytimeTracking(SteamResult result){ - - } - - @Override - public void onStopPlaytimeTrackingForAllItems(SteamResult result){ - - } - @Override public void onDeleteItem(SteamPublishedFileID publishedFileID, SteamResult result){ ItemInstallInfo info = new ItemInstallInfo(); diff --git a/fastlane/metadata/android/en-US/changelogs/29791.txt b/fastlane/metadata/android/en-US/changelogs/29791.txt new file mode 100644 index 0000000000..cd220923ab --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/29791.txt @@ -0,0 +1,3 @@ +[This is a truncated changelog, see Github for full notes] +- Fixed flying unit AI +- Made conduits/conveyors upgrades follow paths (Contributed by @Slava0135) diff --git a/gradle.properties b/gradle.properties index 2fa78cb0d0..0152a046ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ org.gradle.daemon=true -org.gradle.jvmargs=-Xms256m -Xmx1024m +org.gradle.jvmargs=-Xms256m -Xmx1024m --illegal-access=permit # Don't recompute annotations if sources haven't been changed kapt.incremental.apt = true # Multithreaded @@ -8,4 +8,4 @@ kapt.use.worker.api=true kapt.include.compile.classpath=false # I don't need to use the kotlin stdlib yet, so remove it to prevent extra bloat & method count issues kotlin.stdlib.default.dependency=false -archash=0e99b0291f81d74d335dca8b0cf3bf26931f1197 +archash=5364d0187882a5e2f09850fe2505608dd7cf93f3 diff --git a/ios/robovm.xml b/ios/robovm.xml index 98de82cd7b..c94ef0fe26 100644 --- a/ios/robovm.xml +++ b/ios/robovm.xml @@ -19,12 +19,7 @@ net.jpountz.lz4.** arc.scene.** - mindustry.gen.Call - mindustry.net.** - mindustry.world.blocks.** - mindustry.logic.** - mindustry.world.blocks.** - mindustry.entities.effect.** + mindustry.** com.android.okhttp.HttpHandler com.android.okhttp.HttpsHandler com.android.org.conscrypt.** diff --git a/ios/src/mindustry/ios/IOSLauncher.java b/ios/src/mindustry/ios/IOSLauncher.java index eda03585bc..4171862d2b 100644 --- a/ios/src/mindustry/ios/IOSLauncher.java +++ b/ios/src/mindustry/ios/IOSLauncher.java @@ -75,7 +75,6 @@ public class IOSLauncher extends IOSApplication.Delegate{ class ChooserDelegate extends NSObject implements UIDocumentBrowserViewControllerDelegate{ @Override public void didPickDocumentURLs(UIDocumentBrowserViewController controller, NSArray documentURLs){ - } @Override diff --git a/settings.gradle b/settings.gradle index 52b0980311..ef9b57e54a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -39,6 +39,9 @@ if(!hasProperty("release")){ ':Arc:extensions:packer', ':Arc:extensions:g3d', ':Arc:extensions:fx', + ':Arc:extensions:flabel', + ':Arc:extensions:discord', + ':Arc:extensions:profiling', ':Arc:natives', ':Arc:natives:natives-desktop', ':Arc:natives:natives-android', diff --git a/tools/build.gradle b/tools/build.gradle index 173cae9721..3a1141fb1f 100644 --- a/tools/build.gradle +++ b/tools/build.gradle @@ -1,14 +1,17 @@ sourceSets.main.java.srcDirs = ["src/"] -import arc.struct.* -import arc.graphics.* -import arc.packer.* -import arc.util.* -import javax.imageio.* -import java.awt.Graphics2D -import java.awt.image.* -import java.util.concurrent.* + +import arc.files.Fi +import arc.graphics.Color +import arc.graphics.Pixmap +import arc.packer.TexturePacker +import arc.struct.IntIntMap +import arc.struct.IntMap + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit def genFolder = "../core/assets-raw/sprites_out/generated/" def doAntialias = !project.hasProperty("disableAntialias") @@ -25,16 +28,17 @@ def transformColors = { List> list -> } newColors.each{ color -> - colorMap.put(color.argb8888(), newColors) - colorIndexMap.put(color.argb8888(), newColors.indexOf(color)) + colorMap.put(color.rgba(), newColors) + colorIndexMap.put(color.rgba(), newColors.indexOf(color)) } } } - +//TODO implementing this in gradle is a bad idea //d4816b transformColors([["a387ea", "8a73c6", "5c5e9f"], ["6e7080", "989aa4", "b0bac0"], ["bc5452", "ea8878", "feb380"], - ["de9458", "f8c266", "ffe18f"], ["feb380", "ea8878", "bc5452"], ["d4816b", "eab678", "ffd37f"], ["ffffff", "dcc6c6", "9d7f7f"]]) + ["de9458", "f8c266", "ffe18f"], ["feb380", "ea8878", "bc5452"], ["d4816b", "eab678", "ffd37f"], + ["ffffff", "dcc6c6", "9d7f7f"], ["df7646", "b23a4d", "752249"], ["3c3837", "515151", "646567"]]) def antialias = { File file -> if(!doAntialias) return @@ -44,11 +48,10 @@ def antialias = { File file -> return } - def image = ImageIO.read(file) - def out = ImageIO.read(file) + def image = new Pixmap(new Fi(file)) + def out = image.copy() def getRGB = { int ix, int iy -> - //if(ix <= 0 || iy <= 0 || ix >= image.width || iy >= image.height) return 0 - return image.getRGB(Math.max(Math.min(ix, image.width - 1), 0), Math.max(Math.min(iy, image.height - 1), 0)) + return image.getRaw(Math.max(Math.min(ix, image.width - 1), 0), Math.max(Math.min(iy, image.height - 1), 0)) } def color = new Color() @@ -56,8 +59,8 @@ def antialias = { File file -> def suma = new Color() int[] p = new int[9] - for(int x = 0; x < image.getWidth(); x++){ - for(int y = 0; y < image.getHeight(); y++){ + for(int x = 0; x < image.width; x++){ + for(int y = 0; y < image.height; y++){ int A = getRGB(x - 1, y + 1), B = getRGB(x, y + 1), C = getRGB(x + 1, y + 1), @@ -68,7 +71,7 @@ def antialias = { File file -> H = getRGB(x, y - 1), I = getRGB(x + 1, y - 1) - Arrays.fill(p, E); + Arrays.fill(p, E) if(D == B && D != H && B != F) p[0] = D if((D == B && D != H && B != F && E != C) || (B == F && B != D && F != H && E != A)) p[1] = B @@ -82,21 +85,21 @@ def antialias = { File file -> suma.set(0) for(int val : p){ - color.argb8888(val) + color.rgba8888(val) suma.r += color.r * color.a suma.g += color.g * color.a suma.b += color.b * color.a suma.a += color.a } - float fm = suma.a <= 0.001f ? 0f : (float) (1f / suma.a) + float fm = suma.a <= 0.001f ? 0f : (float)(1f / suma.a) suma.mul(fm, fm, fm, fm) float total = 0 sum.set(0) for(int val : p){ - color.argb8888(val) + color.rgba8888(val) float a = color.a color.lerp(suma, (float) (1f - a)) sum.r += color.r @@ -108,92 +111,19 @@ def antialias = { File file -> fm = (float)(1f / total) sum.mul(fm, fm, fm, fm) - out.setRGB(x, y, sum.argb8888()) + out.setRaw(x, y, sum.rgba8888()) sum.set(0) } } - ImageIO.write(out, "png", file) -} + image.dispose() + out.dispose() -def medianBlur = { File file -> - def image = ImageIO.read(file) - def result = new BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_ARGB) - def radius = 4 - IntSeq array = new IntSeq() - - def getRGB = { int ix, int iy -> - return image.getRGB(Math.max(Math.min(ix, image.width - 1), 0), Math.max(Math.min(iy, image.height - 1), 0)) - } - - for(int x = 0; x < image.width; x++){ - for(int y = 0; y < image.height; y++){ - array.clear() - - for(int dx = -radius; dx <= radius; dx ++){ - for(int dy = -radius; dy <= radius; dy ++){ - if(dx*dx + dy*dy <= radius*radius){ - array.add(getRGB(x + dx, y + dy)) - } - } - } - - array.sort() - - result.setRGB(x, y, array.get((int)(array.size / 2))) - } - } - - ImageIO.write(result, "png", file) -} - -def scaleImage = { File file -> - def image = ImageIO.read(file) - for(int iteration in 0..1){ - def scaled = new BufferedImage(image.width * 2, image.height * 2, BufferedImage.TYPE_INT_ARGB) - - def getRGB = { int ix, int iy -> - return image.getRGB(Math.max(Math.min(ix, image.width - 1), 0), Math.max(Math.min(iy, image.height - 1), 0)) - } - - for(int x = 0; x < image.width; x++){ - for(int y = 0; y < image.height; y++){ - int p = image.getRGB(x, y) - int p1 = p, p2 = p, p3 = p, p4 = p - - int A = getRGB(x - 1, y + 1), - B = getRGB(x, y + 1), - C = getRGB(x + 1, y + 1), - D = getRGB(x - 1, y), - E = getRGB(x, y), - F = getRGB(x + 1, y), - G = getRGB(x - 1, y - 1), - H = getRGB(x, y - 1), - I = getRGB(x + 1, y - 1), - J = getRGB(x, y + 2), - K = getRGB(x - 2, y), - L = getRGB(x + 2, y), - M = getRGB(x, y - 2) - - if(B == D && B != F && D != H && (E != A || E == C || E == G || A == J || A == K)) p1 = B - if(B == F && B != D && F != H && (E != C || E == A || E == I || C == J || C == L)) p2 = F - if(D == H && B != D && F != H && (E != G || E == A || E == I || G == K || G == M)) p3 = D - if(F == H && B != F && D != H && (E != I || E == C || E == G || I == L || I == M)) p4 = H - - scaled.setRGB(x * 2, y * 2 + 1, p1) - scaled.setRGB(x * 2 + 1, y * 2 + 1, p2) - scaled.setRGB(x * 2, y * 2, p3) - scaled.setRGB(x * 2 + 1, y * 2, p4) - } - } - image = scaled - } - - ImageIO.write(image, "png", file) + new Fi(file).writePng(out) } def tileImage = { File file -> - def image = ImageIO.read(file) + def image = new Pixmap(new Fi(file)) for(x in 0..image.width-1){ for(y in 0..image.height-1){ @@ -201,104 +131,36 @@ def tileImage = { File file -> def rx = image.height - 1 - y def ry = x - image.setRGB(x, y, image.getRGB(rx, image.height - 1 - ry)) + image.setRaw(x, y, image.getRaw(rx, image.height - 1 - ry)) } } } - def result = new BufferedImage(image.width * 2, image.height * 2, image.getType()) - Graphics2D graphics = result.createGraphics() - graphics.drawImage(image, image.width, 0, -image.width, image.height, null) + def result = new Pixmap(image.width * 2, image.height * 2) - graphics.drawImage(image, image.width, 0, image.width, image.height, null) + result.draw(image.flipX(), 0, 0) + result.draw(image, image.width, 0) + result.draw(image.flipX().flipY(), 0, image.height) + result.draw(image.flipY(), image.width, image.height) - graphics.drawImage(image, image.width, image.height*2, -image.width, -image.height, null) - - graphics.drawImage(image, image.width, image.height*2, image.width, -image.height, null) - - for(int x = 0; x < result.width; x++){ - for(int y = 0; y < result.height; y++){ - int p = result.getRGB(x, y) + for(x in 0..result.width-1){ + for(y in 0..result.height-1){ + int p = result.getRaw(x, y) if(x <= y){ List list = colorMap.get(p) int index = colorIndexMap.get(p, -1) if(index != -1){ int resultIndex = (x == y ? 1 : index == 2 ? 0 : index == 0 ? 2 : 1); - result.setRGB(x, y, list[resultIndex].argb8888()) + result.setRaw(x, y, list[resultIndex].rgba()) } } } } - ImageIO.write(result, "png", file) -} - -task swapColors(){ - doLast{ - if(project.hasProperty("colors")){ - def carr = new File(getProperty("colors")).text.split("\n") - def map = [:] - def swaps = 0 - carr.each{ str -> map[Color.valueOf(str.split("=")[0]).argb8888()] = Color.valueOf(str.split("=")[1]).argb8888() } - def tmpc = new Color() - - fileTree(dir: '../core/assets-raw/sprites', include: "**/*.png").visit{ file -> - if(file.isDirectory()) return - - boolean found = false - - def img = ImageIO.read(file.file) - for(x in (0..img.getWidth() - 1)){ - for(y in (0..img.getHeight() - 1)){ - def c = img.getRGB(x, y) - tmpc.argb8888(c) - if(tmpc.a < 0.1f) continue - if(map.containsKey(c)){ - img.setRGB(x, y, (int)map.get(c)) - found = true - } - } - } - if(found){ - swaps++ - ImageIO.write(img, "png", file.file) - } - } - println "Swapped $swaps images." - }else{ - throw new InvalidUserDataException("No replacement colors specified. Use -Pcolors=\"\"") - } - } -} - -task genPalette(){ - doLast{ - def total = 0 - def size = 32 - def outImage = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB) - def colorsUsed = new IntSet() - - fileTree(dir: '../core/assets-raw/sprites/blocks', include: "**/*.png").visit{ file -> - if(file.isDirectory()) return - - def img = ImageIO.read(file.file) - for(x in (0..img.getWidth() - 1)){ - for(y in (0..img.getHeight() - 1)){ - def c = img.getRGB(x, y) - - if(Tmp.c1.argb8888(c).a > 0.999f && colorsUsed.add(c)){ - outImage.setRGB((int)(total / size), total % size, c) - total ++ - } - } - } - } - - ImageIO.write(outImage, "png", new File("palette.png")) - println "Found $total colors." - - } + new Fi(file).writePng(result) + result.dispose() + image.dispose() } task antialiasImages(){ @@ -310,15 +172,6 @@ task antialiasImages(){ } } -task scaleImages(){ - doLast{ - for(def img : project.getProperty("images").split(",")){ - println(project.getProperty("startdir") + "/" + img) - scaleImage(new File(project.getProperty("startdir") + "/" + img)) - } - } -} - task tileImages(){ doLast{ for(def img : project.getProperty("images").split(",")){ @@ -383,7 +236,7 @@ task pack(dependsOn: [classes, configurations.runtimeClasspath]){ println("\n\nPacking normal 4096 sprites...\n\n") //pack normal sprites - TexturePacker.process(new File(rootDir, "core/assets-raw/sprites_out/").absolutePath, new File(rootDir, "core/assets/sprites/").absolutePath, "sprites.atlas") + TexturePacker.process(new File(rootDir, "core/assets-raw/sprites_out/").absolutePath, new File(rootDir, "core/assets/sprites/").absolutePath, "sprites.aatls") println("\n\nPacking fallback 2048 sprites...\n\n") @@ -393,7 +246,7 @@ task pack(dependsOn: [classes, configurations.runtimeClasspath]){ } //pack fallback 2048x2048 sprites - TexturePacker.process(new File(rootDir, "core/assets-raw/sprites_out/").absolutePath, new File(rootDir, "core/assets/sprites/fallback/").absolutePath, "sprites.atlas") + TexturePacker.process(new File(rootDir, "core/assets-raw/sprites_out/").absolutePath, new File(rootDir, "core/assets/sprites/fallback/").absolutePath, "sprites.aatls") } } @@ -402,7 +255,6 @@ task genSprites(dependsOn: classes, type: JavaExec){ main = "mindustry.tools.ImagePacker" classpath = sourceSets.main.runtimeClasspath - jvmArgs("-Djava.awt.headless=true") standardInput = System.in workingDir = genFolder } @@ -435,11 +287,4 @@ task updateScripts(dependsOn: classes, type: JavaExec){ classpath = sourceSets.main.runtimeClasspath standardInput = System.in workingDir = "../" -} - -task genBindings(dependsOn: classes, type: JavaExec){ - main = "mindustry.tools.BindingsGenerator" - classpath = sourceSets.main.runtimeClasspath - standardInput = System.in - workingDir = "../" } \ No newline at end of file diff --git a/tools/src/mindustry/tools/Edgifier.java b/tools/src/mindustry/tools/Edgifier.java deleted file mode 100644 index f83694d13a..0000000000 --- a/tools/src/mindustry/tools/Edgifier.java +++ /dev/null @@ -1,88 +0,0 @@ -package mindustry.tools; - -import arc.files.*; -import arc.graphics.*; -import arc.struct.*; -import arc.util.*; - -public class Edgifier{ - - public static void main(String[] args){ - ArcNativesLoader.load(); - - Pixmap pixmap = new Pixmap(Fi.get("/home/anuke/Projects/Mindustry/core/assets-raw/sprites/units/reaper.png")); - - Fi.get("/home/anuke/out.png").writePNG(edgify(pixmap, 5)); - } - - private static Pixmap edgify(Pixmap in, int chunk){ - Pixmap out = new Pixmap(in.getWidth(), in.getHeight()); - IntSeq side1 = new IntSeq(), side2 = new IntSeq(); - - for(int x = 0; x < in.getWidth(); x += chunk){ - for(int y = 0; y < in.getHeight(); y += chunk){ - int bestErrors = Integer.MAX_VALUE; - int bestRotation = 0; - int bestSide1 = 0, bestSide2 = 0; - - for(int rotation = 0; rotation < 8; rotation++){ - side1.clear(); - side2.clear(); - - //assign pixels present on each side - for(int cx = 0; cx < chunk; cx++){ - for(int cy = 0; cy < chunk; cy++){ - boolean side = classify(rotation, cx, cy, chunk); - - int pixel = in.getPixel(x + cx, y + cy); - if(Pixmaps.empty(pixel)) pixel = 0; //all alpha=0 pixels are treated as 0 - - (side ? side1 : side2).add(pixel); - } - } - - //find most popular element here - int mode1 = side1.mode(), mode2 = side2.mode(); - //total errors; 'incorrect' pixels - int errors = (side1.size - side1.count(mode1)) + (side2.size - side2.count(mode2)); - - //Log.info("errors for rotation={0}: {1}", rotation, errors); - - //update if better - if(errors < bestErrors){ - bestRotation = rotation; - bestSide1 = mode1; - bestSide2 = mode2; - bestErrors = errors; - } - } - - //Log.info("Best result for {0},{1}: rotation={2} 1={3} 2={4} errors={5}", x, y, bestRotation, bestSide1, bestSide2, bestErrors); - - //draw with the best result - for(int cx = 0; cx < chunk; cx++){ - for(int cy = 0; cy < chunk; cy++){ - boolean side = classify(bestRotation, cx, cy, chunk); - out.draw(x + cx, y + cy, side ? bestSide1 : bestSide2); - } - } - } - } - - return out; - } - - private static boolean classify(int rotation, int x, int y, int chunk){ - switch(rotation){ - case 0: //return y >= chunk / 2; - case 1: return y < x; - case 2: //return x <= chunk / 2; - case 3: return (chunk - 1 - y) < x; - case 4: //return (y > chunk / 2); - case 5: return (y <= x); - case 6: //return (x < chunk / 2); - case 7: return ((chunk - 1 - y) <= x); - default: throw new IllegalArgumentException("Invalid rotation: " + rotation); - } - } -} diff --git a/tools/src/mindustry/tools/Generators.java b/tools/src/mindustry/tools/Generators.java index 5535b11c49..5c6efbd2ec 100644 --- a/tools/src/mindustry/tools/Generators.java +++ b/tools/src/mindustry/tools/Generators.java @@ -14,48 +14,25 @@ import mindustry.ctype.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; -import mindustry.tools.ImagePacker.*; import mindustry.type.*; -import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.*; import mindustry.world.blocks.environment.*; import mindustry.world.blocks.legacy.*; import mindustry.world.meta.*; +import java.util.concurrent.*; + import static mindustry.Vars.*; +import static mindustry.tools.ImagePacker.*; public class Generators{ - //used for changing colors in the UI - testing only - static final IntIntMap paletteMap = IntIntMap.with( - //empty for now - 0x454545ff, 0x00000000,//0x32394bff, - 0x00000099, 0x00000000//0x000000ff - ); - static final Cicon logicIcon = Cicon.medium; + static final int logicIconSize = (int)iconMed, maxUiIcon = 128; - public static void generate(){ - ObjectMap gens = new ObjectMap<>(); + public static void run(){ + ObjectMap gens = new ObjectMap<>(); - if(!paletteMap.isEmpty()){ - ImagePacker.generate("uipalette", () -> { - Fi.get("../ui").walk(fi -> { - if(!fi.extEquals("png")) return; - - Pixmap pix = new Pixmap(fi); - pix.setBlending(Pixmap.Blending.sourceOver); - pix.each((x, y) -> { - int value = pix.getPixel(x, y); - pix.draw(x, y, paletteMap.get(value, value)); - }); - - fi.writePNG(pix); - }); - }); - } - - ImagePacker.generate("splashes", () -> { - ArcNativesLoader.load(); + generate("splashes", () -> { int frames = 12; int size = 32; @@ -70,90 +47,129 @@ public class Generators{ pixmap.each((x, y) -> { float dst = Mathf.dst(x, y, size/2f, size/2f); if(Math.abs(dst - radius) <= stroke){ - pixmap.draw(x, y, Color.white); + pixmap.set(x, y, Color.white); } }); - Fi.get("splash-" + i + ".png").writePNG(pixmap); + Fi.get("splash-" + i + ".png").writePng(pixmap); pixmap.dispose(); } }); - ImagePacker.generate("cliffs", () -> { - int size = 64; - Color dark = new Color(0.5f, 0.5f, 0.6f, 1f).mul(0.98f); - Color mid = Color.lightGray; + generate("bubbles", () -> { - Image[] images = new Image[8]; + int frames = 16; + int size = 40; + for(int i = 0; i < frames; i++){ + float fin = (float)i / (frames); + float fout = 1f - fin; + float stroke = 3.5f * fout; + float radius = (size/2f) * fin; + float shinelen = radius / 2.5f, shinerad = stroke*1.5f + 0.3f; + float shinex = size/2f + shinelen / Mathf.sqrt2, shiney = size/2f - shinelen / Mathf.sqrt2; + + Pixmap pixmap = new Pixmap(size, size); + + pixmap.each((x, y) -> { + float dst = Mathf.dst(x, y, size/2f, size/2f); + if(Math.abs(dst - radius) <= stroke || Mathf.within(x, y, shinex, shiney, shinerad)){ + pixmap.set(x, y, Color.white); + } + }); + + Fi.get("bubble-" + i + ".png").writePng(pixmap); + + pixmap.dispose(); + } + }); + + generate("cliffs", () -> { + ExecutorService exec = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + int size = 64; + int dark = new Color(0.5f, 0.5f, 0.6f, 1f).mul(0.98f).rgba(); + int mid = Color.lightGray.rgba(); + + Pixmap[] images = new Pixmap[8]; for(int i = 0; i < 8; i++){ - images[i] = ImagePacker.get("cliff" + i); + images[i] = new Pixmap(((GenRegion)Core.atlas.find("cliff" + i)).path); } for(int i = Byte.MIN_VALUE; i <= Byte.MAX_VALUE; i++){ - Image result = new Image(size, size); - byte[][] mask = new byte[size][size]; + int bi = i; + exec.execute(() -> { + Color color = new Color(); + Pixmap result = new Pixmap(size, size); + byte[][] mask = new byte[size][size]; - byte val = (byte)i; - //check each bit/direction - for(int j = 0; j < 8; j++){ - if((val & (1 << j)) != 0){ - if(j % 2 == 1 && (((val & (1 << (j + 1))) != 0) != ((val & (1 << (j - 1))) != 0))){ - continue; - } - - Image image = images[j]; - image.each((x, y) -> { - Color color = image.getColor(x, y); - if(color.a > 0.1){ - //white -> bit 1 -> top - //black -> bit 2 -> bottom - mask[x][y] |= (color.r > 0.5f ? 1 : 2); + byte val = (byte)bi; + //check each bit/direction + for(int j = 0; j < 8; j++){ + if((val & (1 << j)) != 0){ + if(j % 2 == 1 && (((val & (1 << (j + 1))) != 0) != ((val & (1 << (j - 1))) != 0))){ + continue; } - }); - } - } - result.each((x, y) -> { - byte m = mask[x][y]; - if(m != 0){ - //mid - if(m == 3){ - //find nearest non-mid color - byte best = 0; - float bestDst = 0; - boolean found = false; - //expand search range until found - for(int rad = 9; rad < 64; rad += 7){ - for(int cx = Math.max(x - rad, 0); cx <= Math.min(x + rad, size - 1); cx++){ - for(int cy = Math.max(y - rad, 0); cy <= Math.min(y + rad, size - 1); cy++){ - byte nval = mask[cx][cy]; - if(nval == 1 || nval == 2){ - float dst2 = Mathf.dst2(cx, cy, x, y); - if(dst2 <= rad * rad && (!found || dst2 < bestDst)){ - best = nval; - bestDst = dst2; - found = true; + Pixmap image = images[j]; + image.each((x, y) -> { + color.set(image.getRaw(x, y)); + if(color.a > 0.1){ + //white -> bit 1 -> top + //black -> bit 2 -> bottom + mask[x][y] |= (color.r > 0.5f ? 1 : 2); + } + }); + } + } + + result.each((x, y) -> { + byte m = mask[x][y]; + if(m != 0){ + //mid + if(m == 3){ + //find nearest non-mid color + byte best = 0; + float bestDst = 0; + boolean found = false; + //expand search range until found + for(int rad = 9; rad < 64; rad += 7){ + for(int cx = Math.max(x - rad, 0); cx <= Math.min(x + rad, size - 1); cx++){ + for(int cy = Math.max(y - rad, 0); cy <= Math.min(y + rad, size - 1); cy++){ + byte nval = mask[cx][cy]; + if(nval == 1 || nval == 2){ + float dst2 = Mathf.dst2(cx, cy, x, y); + if(dst2 <= rad * rad && (!found || dst2 < bestDst)){ + best = nval; + bestDst = dst2; + found = true; + } } } } } + + if(found){ + m = best; + } } - if(found){ - m = best; - } + result.setRaw(x, y, m == 1 ? Color.whiteRgba : m == 2 ? dark : mid); } + }); - result.draw(x, y, m == 1 ? Color.white : m == 2 ? dark : mid); - } + Fi.get("../blocks/environment/cliffmask" + (val & 0xff) + ".png").writePng(result); }); + } - result.save("../blocks/environment/cliffmask" + (val & 0xff)); + try{ + exec.shutdown(); + exec.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); + }catch(Exception e){ + throw new RuntimeException("go away", e); } }); - ImagePacker.generate("cracks", () -> { + generate("cracks", () -> { RidgedPerlin r = new RidgedPerlin(1, 3); for(int size = 1; size <= BlockRenderer.maxCrackSize; size++){ int dim = size * 32; @@ -161,17 +177,17 @@ public class Generators{ for(int i = 0; i < steps; i++){ float fract = i / (float)steps; - Image image = new Image(dim, dim); + Pixmap image = new Pixmap(dim, dim); for(int x = 0; x < dim; x++){ for(int y = 0; y < dim; y++){ float dst = Mathf.dst((float)x/dim, (float)y/dim, 0.5f, 0.5f) * 2f; if(dst < 1.2f && r.getValue(x, y, 1f / 40f) - dst*(1f-fract) > 0.16f){ - image.draw(x, y, Color.white); + image.setRaw(x, y, Color.whiteRgba); } } } - Image output = new Image(image.width, image.height); + Pixmap output = new Pixmap(image.width, image.height); int rad = 3; //median filter @@ -181,30 +197,31 @@ public class Generators{ for(int cx = -rad; cx < rad; cx++){ for(int cy = -rad; cy < rad; cy++){ int wx = Mathf.clamp(cx + x, 0, output.width - 1), wy = Mathf.clamp(cy + y, 0, output.height - 1); - Color color = image.getColor(wx, wy); - if(color.a > 0.5f){ + int color = image.getRaw(wx, wy); + if((color & 0xff) > 127){ whites ++; }else{ clears ++; } } } - output.draw(x, y, whites >= clears ? Color.white : Color.clear); + output.setRaw(x, y, whites >= clears ? Color.whiteRgba : Color.clearRgba); } } - output.save("cracks-" + size + "-" + i); + Fi.get("cracks-" + size + "-" + i + ".png").writePng(output); } } }); - ImagePacker.generate("block-icons", () -> { - Image colors = new Image(content.blocks().size, 1); + generate("block-icons", () -> { + Pixmap colors = new Pixmap(content.blocks().size, 1); for(Block block : content.blocks()){ if(block.isAir() || block instanceof ConstructBlock || block instanceof OreBlock || block instanceof LegacyBlock) continue; block.load(); + block.loadIcon(); TextureRegion[] regions = block.getGeneratedIcons(); @@ -216,20 +233,25 @@ public class Generators{ } } - Image shardTeamTop = null; + for(TextureRegion region : block.makeIconRegions()){ + GenRegion gen = (GenRegion)region; + save(get(region).outline(block.outlineColor, block.outlineRadius), gen.name + "-outline"); + } + + Pixmap shardTeamTop = null; if(block.teamRegion.found()){ - Image teamr = ImagePacker.get(block.teamRegion); + Pixmap teamr = get(block.teamRegion); for(Team team : Team.all){ if(team.hasPalette){ - Image out = new Image(teamr.width, teamr.height); + Pixmap out = new Pixmap(teamr.width, teamr.height); teamr.each((x, y) -> { - int color = teamr.getColor(x, y).rgba8888(); + int color = teamr.getRaw(x, y); int index = color == 0xffffffff ? 0 : color == 0xdcc6c6ff ? 1 : color == 0x9d7f7fff ? 2 : -1; - out.draw(x, y, index == -1 ? teamr.getColor(x, y) : team.palette[index]); + out.setRaw(x, y, index == -1 ? teamr.getRaw(x, y) : team.palette[index].rgba()); }); - out.save(block.name + "-team-" + team.name); + save(out, block.name + "-team-" + team.name); if(team == Team.sharded){ shardTeamTop = out; @@ -243,87 +265,63 @@ public class Generators{ } try{ - Image last = null; + Pixmap last = null; if(block.outlineIcon){ - int radius = 4; GenRegion region = (GenRegion)regions[block.outlinedIcon >= 0 ? block.outlinedIcon : regions.length -1]; - Image base = ImagePacker.get(region); - Image out = last = new Image(region.width, region.height); - for(int x = 0; x < out.width; x++){ - for(int y = 0; y < out.height; y++){ - - Color color = base.getColor(x, y); - out.draw(x, y, color); - if(color.a < 1f){ - boolean found = false; - outer: - for(int rx = -radius; rx <= radius; rx++){ - for(int ry = -radius; ry <= radius; ry++){ - if(Mathf.dst(rx, ry) <= radius && base.getColor(rx + x, ry + y).a > 0.01f){ - found = true; - break outer; - } - } - } - if(found){ - out.draw(x, y, block.outlineColor); - } - } - } - } + Pixmap base = get(region); + Pixmap out = last = base.outline(block.outlineColor, block.outlineRadius); //do not run for legacy ones if(block.outlinedIcon >= 0){ //prevents the regions above from being ignored/invisible/etc for(int i = block.outlinedIcon + 1; i < regions.length; i++){ - out.draw(ImagePacker.get(regions[i])); + out.draw(get(regions[i]), true); } } region.path.delete(); - out.save(block.name); + save(out, block.name); } - Image image = ImagePacker.get(regions[0]); + if(!regions[0].found()){ + continue; + } + + Pixmap image = get(regions[0]); int i = 0; for(TextureRegion region : regions){ i++; if(i != regions.length || last == null){ - image.draw(region); + image.draw(get(region), true); }else{ - image.draw(last); + image.draw(last, true); } //draw shard (default team top) on top of first sprite if(region == block.teamRegions[Team.sharded.id] && shardTeamTop != null){ - image.draw(shardTeamTop); + image.draw(shardTeamTop, true); } } if(!(regions.length == 1 && regions[0] == Core.atlas.find(block.name) && shardTeamTop == null)){ - image.save("block-" + block.name + "-full"); + save(image, "block-" + block.name + "-full"); } - image.save("../editor/" + block.name + "-icon-editor"); + save(image, "../editor/" + block.name + "-icon-editor"); - for(Cicon icon : Cicon.scaled){ - Image scaled = new Image(icon.size, icon.size); - scaled.drawScaled(image); - scaled.save("../ui/block-" + block.name + "-" + icon.name()); - - if(icon == logicIcon && block.synthetic() && block.buildVisibility != BuildVisibility.hidden){ - image.save(block.name + "-icon-logic"); - } + if(block.buildVisibility != BuildVisibility.hidden){ + saveScaled(image, block.name + "-icon-logic", logicIconSize); } + saveScaled(image, "../ui/block-" + block.name + "-ui", Math.min(image.width, maxUiIcon)); boolean hasEmpty = false; - Color average = new Color(); + Color average = new Color(), c = new Color(); float asum = 0f; for(int x = 0; x < image.width; x++){ for(int y = 0; y < image.height; y++){ - Color color = image.getColor(x, y); + Color color = c.set(image.get(x, y)); average.r += color.r*color.a; average.g += color.g*color.a; average.b += color.b*color.a; @@ -343,94 +341,81 @@ public class Generators{ } //encode square sprite in alpha channel average.a = hasEmpty ? 0.1f : 1f; - colors.draw(block.id, 0, average); + colors.setRaw(block.id, 0, average.rgba()); }catch(NullPointerException e){ Log.err("Block &ly'@'&lr has an null region!", block); } } - colors.save("../../../assets/sprites/block_colors"); + save(colors, "../../../assets/sprites/block_colors"); }); - ImagePacker.generate("shallows", () -> { + generate("shallows", () -> { content.blocks().each(b -> b instanceof ShallowLiquid, floor -> { - Image overlay = ImagePacker.get(floor.liquidBase.region); + Pixmap overlay = get(floor.liquidBase.region); int index = 0; for(TextureRegion region : floor.floorBase.variantRegions()){ - Image res = new Image(32, 32); - res.draw(ImagePacker.get(region)); + Pixmap res = get(region).copy(); for(int x = 0; x < res.width; x++){ for(int y = 0; y < res.height; y++){ - Color color = overlay.getColor(x, y).a(floor.liquidOpacity); - res.draw(x, y, color); + res.set(x, y, Pixmap.blend((overlay.getRaw(x, y) & 0xffffff00) | (int)(floor.liquidOpacity * 255), res.getRaw(x, y))); } } String name = floor.name + "" + (++index); - res.save("../blocks/environment/" + name); - res.save("../editor/editor-" + name); + save(res, "../blocks/environment/" + name); + save(res, "../editor/editor-" + name); gens.put(floor, res); } }); }); - ImagePacker.generate("item-icons", () -> { + generate("item-icons", () -> { for(UnlockableContent item : Seq.withArrays(content.items(), content.liquids(), content.statusEffects())){ - if(item instanceof StatusEffect && !ImagePacker.has(item.getContentType().name() + "-" + item.name)){ + if(item instanceof StatusEffect && !has(item.getContentType().name() + "-" + item.name)){ continue; } - Image base = ImagePacker.get(item.getContentType().name() + "-" + item.name); + Pixmap base = get(item.getContentType().name() + "-" + item.name); //tint status effect icon color if(item instanceof StatusEffect){ StatusEffect stat = (StatusEffect)item; - Image tint = base; - base.each((x, y) -> tint.draw(x, y, tint.getColor(x, y).mul(stat.color))); + Pixmap tint = base; + base.each((x, y) -> tint.setRaw(x, y, Color.muli(tint.getRaw(x, y), stat.color.rgba()))); //outline the image - Image container = new Image(38, 38); - container.draw(base, 3, 3); - base = container.outline(3, Pal.gray); + Pixmap container = new Pixmap(38, 38); + container.draw(base, 3, 3, true); + base = container.outline(Pal.gray, 3); } - for(Cicon icon : Cicon.scaled){ - //if(icon.size == base.width) continue; - Image image = new Image(icon.size, icon.size); - image.drawScaled(base); - image.save((item instanceof StatusEffect ? "../ui/" : "") + item.getContentType().name() + "-" + item.name + "-" + icon.name(), !(item instanceof StatusEffect)); - - if(icon == Cicon.medium){ - image.save("../ui/" + item.getContentType() + "-" + item.name + "-icon"); - } - - if(icon == logicIcon){ - image.save(item.name + "-icon-logic"); - } - } + saveScaled(base, item.name + "-icon-logic", logicIconSize); + save(base, "../ui/" + item.getContentType().name() + "-" + item.name + "-ui"); } }); - ImagePacker.generate("unit-icons", () -> content.units().each(type -> { + //TODO broken, freezes + generate("unit-icons", () -> content.units().each(type -> { if(type.isHidden()) return; //hidden units don't generate ObjectSet outlined = new ObjectSet<>(); try{ type.load(); + type.loadIcon(); type.init(); - Color outc = Pal.darkerMetal; - Func outline = i -> i.outline(3, outc); + Func outline = i -> i.outline(Pal.darkerMetal, 3); Cons outliner = t -> { if(t != null && t.found()){ - ImagePacker.replace(t, outline.get(ImagePacker.get(t))); + replace(t, outline.get(get(t))); } }; for(Weapon weapon : type.weapons){ - if(outlined.add(weapon.name) && ImagePacker.has(weapon.name)){ - outline.get(ImagePacker.get(weapon.name)).save(weapon.name + "-outline"); + if(outlined.add(weapon.name) && has(weapon.name)){ + save(outline.get(get(weapon.name)), weapon.name + "-outline"); } } @@ -440,47 +425,54 @@ public class Generators{ outliner.get(type.baseJointRegion); if(type.constructor.get() instanceof Legsc) outliner.get(type.legRegion); - Image image = outline.get(ImagePacker.get(type.region)); + Pixmap image = outline.get(get(type.region)); - image.save(type.name + "-outline"); + save(image, type.name + "-outline"); //draw mech parts if(type.constructor.get() instanceof Mechc){ - image.drawCenter(type.baseRegion); - image.drawCenter(type.legRegion); - image.drawCenter(type.legRegion, true, false); - image.draw(type.region); + drawCenter(image, get(type.baseRegion)); + drawCenter(image, get(type.legRegion)); + drawCenter(image, get(type.legRegion).flipX()); + image.draw(get(type.region), true); } //draw outlines for(Weapon weapon : type.weapons){ weapon.load(); - image.draw(outline.get(ImagePacker.get(weapon.region)), + image.draw(weapon.flipSprite ? outline.get(get(weapon.region)).flipX() : outline.get(get(weapon.region)), (int)(weapon.x / Draw.scl + image.width / 2f - weapon.region.width / 2f), (int)(-weapon.y / Draw.scl + image.height / 2f - weapon.region.height / 2f), - weapon.flipSprite, false); + true + ); } //draw base region on top to mask weapons - image.draw(type.region); + image.draw(get(type.region), true); + int baseColor = Color.valueOf("ffa665").rgba(); - Image baseCell = ImagePacker.get(type.cellRegion); - Image cell = new Image(type.cellRegion.width, type.cellRegion.height); - cell.each((x, y) -> cell.draw(x, y, baseCell.getColor(x, y).mul(Color.valueOf("ffa665")))); + Pixmap baseCell = get(type.cellRegion); + Pixmap cell = new Pixmap(type.cellRegion.width, type.cellRegion.height); + cell.each((x, y) -> cell.set(x, y, Color.muli(baseCell.getRaw(x, y), baseColor))); - image.draw(cell, image.width / 2 - cell.width / 2, image.height / 2 - cell.height / 2); + image.draw(cell, image.width / 2 - cell.width / 2, image.height / 2 - cell.height / 2, true); for(Weapon weapon : type.weapons){ weapon.load(); - image.draw(weapon.top ? outline.get(ImagePacker.get(weapon.region)) : ImagePacker.get(weapon.region), + Pixmap wepReg = weapon.top ? outline.get(get(weapon.region)) : get(weapon.region); + if(weapon.flipSprite){ + wepReg = wepReg.flipX(); + } + + image.draw(wepReg, (int)(weapon.x / Draw.scl + image.width / 2f - weapon.region.width / 2f), (int)(-weapon.y / Draw.scl + image.height / 2f - weapon.region.height / 2f), - weapon.flipSprite, false); + true); } - image.save("unit-" + type.name + "-full"); + save(image, "unit-" + type.name + "-full"); Rand rand = new Rand(); rand.setSeed(type.name.hashCode()); @@ -492,9 +484,9 @@ public class Generators{ float offsetRange = Math.max(image.width, image.height) * 0.15f; Vec2 offset = new Vec2(1, 1).rotate(rand.random(360f)).setLength(rand.random(0, offsetRange)).add(image.width/2f, image.height/2f); - Image[] wrecks = new Image[splits]; + Pixmap[] wrecks = new Pixmap[splits]; for(int i = 0; i < wrecks.length; i++){ - wrecks[i] = new Image(image.width, image.height); + wrecks[i] = new Pixmap(image.width, image.height); } RidgedPerlin r = new RidgedPerlin(1, 3); @@ -510,92 +502,82 @@ public class Generators{ //distort edges with random noise float noise = (float)Noise.rawNoise(dst / (9f + image.width/70f)) * (60 + image.width/30f); int section = (int)Mathf.clamp(Mathf.mod(offset.angleTo(x, y) + noise + degrees, 360f) / 360f * splits, 0, splits - 1); - if(!vval) wrecks[section].draw(x, y, image.getColor(x, y).mul(rValue ? 0.7f : 1f)); + if(!vval) wrecks[section].setRaw(x, y, Color.muli(image.getRaw(x, y), rValue ? 0.7f : 1f)); }); for(int i = 0; i < wrecks.length; i++){ - wrecks[i].save(type.name + "-wreck" + i); + save(wrecks[i], "../rubble/" + type.name + "-wreck" + i); } - for(Cicon icon : Cicon.scaled){ - Vec2 size = Scaling.fit.apply(image.width, image.height, icon.size, icon.size); - Image scaled = new Image((int)size.x, (int)size.y); + int maxd = Math.min(Math.max(image.width, image.height), maxUiIcon); + Pixmap fit = new Pixmap(maxd, maxd); + drawScaledFit(fit, image); - scaled.drawScaled(image); - scaled.save("../ui/unit-" + type.name + "-" + icon.name()); - - if(icon == logicIcon){ - scaled.save(type.name + "-icon-logic"); - } - } + saveScaled(fit, type.name + "-icon-logic", logicIconSize); + save(fit, "../ui/unit-" + type.name + "-ui"); }catch(IllegalArgumentException e){ Log.err("WARNING: Skipping unit @: @", type.name, e.getMessage()); } })); - ImagePacker.generate("ore-icons", () -> { + generate("ore-icons", () -> { content.blocks().each(b -> b instanceof OreBlock, ore -> { + String prefix = ore instanceof WallOreBlock ? "wall-ore-" : "ore-"; Item item = ore.itemDrop; + int shadowColor = Color.rgba8888(0, 0, 0, 0.3f); - for(int i = 0; i < 3; i++){ + for(int i = 0; i < ore.variants; i++){ //get base image to draw on - Image image = new Image(32, 32); - Image shadow = ImagePacker.get(item.name + (i + 1)); + Pixmap base = get((ore instanceof WallOreBlock ? "wall-" : "") + item.name + (i + 1)); + Pixmap image = base.copy(); int offset = image.width / tilesize - 1; for(int x = 0; x < image.width; x++){ for(int y = offset; y < image.height; y++){ - Color color = shadow.getColor(x, y - offset); - //draw semi transparent background - if(color.a > 0.001f){ - color.set(0, 0, 0, 0.3f); - image.draw(x, y, color); + if(base.getA(x, y - offset) != 0){ + image.setRaw(x, y, Pixmap.blend(shadowColor, base.getRaw(x, y))); } } } - image.draw(ImagePacker.get(item.name + (i + 1))); - image.save("../blocks/environment/ore-" + item.name + (i + 1)); - image.save("../editor/editor-ore-" + item.name + (i + 1)); + image.draw(base, true); + save(image, "../blocks/environment/" + prefix + item.name + (i + 1)); + save(image, "../editor/editor-" + prefix + item.name + (i + 1)); - image.save("block-" + ore.name + "-full"); - for(Cicon icon : Cicon.scaled){ - Image scaled = new Image(icon.size, icon.size); - scaled.drawScaled(image); - scaled.save("../ui/block-" + ore.name + "-" + icon.name()); - } + save(image, "block-" + ore.name + "-full"); + save(image, "../ui/block-" + ore.name + "-ui"); } }); }); - ImagePacker.generate("edges", () -> { + generate("edges", () -> { content.blocks().each(b -> b instanceof Floor && !(b instanceof OverlayFloor), floor -> { - if(ImagePacker.has(floor.name + "-edge") || floor.blendGroup != floor){ + if(has(floor.name + "-edge") || floor.blendGroup != floor){ return; } try{ - Image image = gens.get(floor, ImagePacker.get(floor.getGeneratedIcons()[0])); - Image edge = ImagePacker.get("edge-stencil"); - Image result = new Image(edge.width, edge.height); + Pixmap image = gens.get(floor, get(floor.getGeneratedIcons()[0])); + Pixmap edge = get("edge-stencil"); + Pixmap result = new Pixmap(edge.width, edge.height); for(int x = 0; x < edge.width; x++){ for(int y = 0; y < edge.height; y++){ - result.draw(x, y, edge.getColor(x, y).mul(image.getColor(x % image.width, y % image.height))); + result.set(x, y, Color.muli(edge.getRaw(x, y), image.get(x % image.width, y % image.height))); } } - result.save("../blocks/environment/" + floor.name + "-edge"); + save(result, "../blocks/environment/" + floor.name + "-edge"); }catch(Exception ignored){} }); }); - ImagePacker.generate("scorches", () -> { + generate("scorches", () -> { for(int size = 0; size < 10; size++){ for(int i = 0; i < 3; i++){ ScorchGenerator gen = new ScorchGenerator(); @@ -615,7 +597,7 @@ public class Generators{ Pixmap out = gen.generate(); Pixmap median = Pixmaps.median(out, 2, 0.75); - Fi.get("../rubble/scorch-" + size + "-" + i + ".png").writePNG(median); + Fi.get("../rubble/scorch-" + size + "-" + i + ".png").writePng(median); out.dispose(); median.dispose(); } @@ -623,4 +605,30 @@ public class Generators{ }); } + /** Generates a scorch pixmap based on parameters. Thread safe, unless multiple scorch generators are running in parallel. */ + public static class ScorchGenerator{ + private static final Simplex sim = new Simplex(); + + public int size = 80, seed = 0, color = Color.whiteRgba; + public double scale = 18, pow = 2, octaves = 4, pers = 0.4, add = 2, nscl = 4.5f; + + public Pixmap generate(){ + Pixmap pix = new Pixmap(size, size); + sim.setSeed(seed); + + pix.each((x, y) -> { + double dst = Mathf.dst(x, y, size/2, size/2) / (size / 2f); + double scaled = Math.abs(dst - 0.5f) * 5f + add; + scaled -= noise(Angles.angle(x, y, size/2, size/2))*nscl; + if(scaled < 1.5f) pix.setRaw(x, y, color); + }); + + return pix; + } + + private double noise(float angle){ + return Math.pow(sim.octaveNoise2D(octaves, pers, 1 / scale, Angles.trnsx(angle, size/2f) + size/2f, Angles.trnsy(angle, size/2f) + size/2f), pow); + } + } + } diff --git a/tools/src/mindustry/tools/IconConverter.java b/tools/src/mindustry/tools/IconConverter.java index c672e2a119..50a7f1bb86 100644 --- a/tools/src/mindustry/tools/IconConverter.java +++ b/tools/src/mindustry/tools/IconConverter.java @@ -17,7 +17,6 @@ public class IconConverter{ Fi.get("fontgen/icon_parts").deleteDirectory(); Fi[] list = new Fi("icons").list(); - ArcNativesLoader.load(); Seq files = new Seq<>(); for(Fi img : list){ @@ -42,25 +41,25 @@ public class IconConverter{ } void convert(Pixmap pixmap, Fi output){ - boolean[][] grid = new boolean[pixmap.getWidth()][pixmap.getHeight()]; + boolean[][] grid = new boolean[pixmap.width][pixmap.height]; - for(int x = 0; x < pixmap.getWidth(); x++){ - for(int y = 0; y < pixmap.getHeight(); y++){ - grid[x][pixmap.getHeight() - 1 - y] = !Pixmaps.empty(pixmap.getPixel(x, y)); + for(int x = 0; x < pixmap.width; x++){ + for(int y = 0; y < pixmap.height; y++){ + grid[x][pixmap.height - 1 - y] = !pixmap.empty(x, y); } } float xscl = 1f, yscl = 1f;//resolution / (float)pixmap.getWidth(), yscl = resolution / (float)pixmap.getHeight(); float scl = xscl; - width = pixmap.getWidth(); - height = pixmap.getHeight(); + width = pixmap.width; + height = pixmap.height; - out.append("\n"); + out.append("\n"); - for(int x = -1; x < pixmap.getWidth(); x++){ - for(int y = -1; y < pixmap.getHeight(); y++){ - int index = index(x, y, pixmap.getWidth(), pixmap.getHeight(), grid); + for(int x = -1; x < pixmap.width; x++){ + for(int y = -1; y < pixmap.height; y++){ + int index = index(x, y, pixmap.width, pixmap.height, grid); float leftx = x * xscl, boty = y * yscl, rightx = x * xscl + xscl, topy = y * xscl + yscl, midx = x * xscl + xscl / 2f, midy = y * yscl + yscl / 2f; diff --git a/tools/src/mindustry/tools/Image.java b/tools/src/mindustry/tools/Image.java deleted file mode 100644 index 83594f22be..0000000000 --- a/tools/src/mindustry/tools/Image.java +++ /dev/null @@ -1,229 +0,0 @@ -package mindustry.tools; - -import arc.func.*; -import arc.graphics.Color; -import arc.graphics.g2d.*; -import arc.math.*; -import arc.math.geom.*; -import arc.struct.*; -import arc.util.*; -import mindustry.tools.ImagePacker.*; - -import javax.imageio.*; -import java.awt.*; -import java.awt.image.*; -import java.io.*; - -class Image{ - private static Seq toDispose = new Seq<>(); - - private BufferedImage image; - private Graphics2D graphics; - private Color color = new Color(); - - public final int width, height; - - Image(TextureRegion region){ - this(ImagePacker.buf(region)); - } - - Image(BufferedImage src){ - this.image = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_ARGB); - this.graphics = image.createGraphics(); - this.graphics.drawImage(src, 0, 0, null); - this.width = image.getWidth(); - this.height = image.getHeight(); - - toDispose.add(this); - } - - Image(int width, int height){ - this(new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)); - } - - Image copy(){ - Image out =new Image(width, height); - out.draw(this); - return out; - } - - boolean isEmpty(int x, int y){ - if(!Structs.inBounds(x, y, width, height)){ - return true; - } - Color color = getColor(x, y); - return color.a <= 0.001f; - } - - Color getColor(int x, int y){ - if(!Structs.inBounds(x, y, width, height)) return color.set(0, 0, 0, 0); - int i = image.getRGB(x, y); - color.argb8888(i); - return color; - } - - Image outline(int radius, Color outlineColor){ - Image out = copy(); - for(int x = 0; x < out.width; x++){ - for(int y = 0; y < out.height; y++){ - - Color color = getColor(x, y); - out.draw(x, y, color); - if(color.a < 1f){ - boolean found = false; - outer: - for(int rx = -radius; rx <= radius; rx++){ - for(int ry = -radius; ry <= radius; ry++){ - if(Mathf.dst(rx, ry) <= radius && getColor(rx + x, ry + y).a > 0.01f){ - found = true; - break outer; - } - } - } - if(found){ - out.draw(x, y, outlineColor); - } - } - } - } - return out; - } - - Image shadow(float alpha, int rad){ - Image out = silhouette(new Color(0f, 0f, 0f, alpha)).blur(rad); - out.draw(this); - return out; - } - - Image silhouette(Color color){ - Image out = copy(); - - each((x, y) -> out.draw(x, y, getColor(x, y).set(color.r, color.g, color.b, this.color.a * color.a))); - - return out; - } - - Image blur(int radius){ - Image out = copy(); - Color c = new Color(); - int[] sum = {0}; - - for(int x = 0; x < out.width; x++){ - for(int y = 0; y < out.height; y++){ - sum[0] = 0; - - Geometry.circle(x, y, radius, (cx, cy) -> { - int rx = Mathf.clamp(cx, 0, out.width - 1), ry = Mathf.clamp(cy, 0, out.height - 1); - - Color other = getColor(rx, ry); - c.r += other.r; - c.g += other.g; - c.b += other.b; - c.a += other.a; - sum[0] ++; - }); - - c.mula(1f / sum[0]); - - out.draw(x, y, c); - } - } - - return out; - } - - void each(Intc2 cons){ - for(int x = 0; x < width; x++){ - for(int y = 0; y < height; y++){ - cons.get(x, y); - } - } - } - - void draw(int x, int y, Color color){ - graphics.setColor(new java.awt.Color(color.r, color.g, color.b, color.a)); - graphics.fillRect(x, y, 1, 1); - } - - - /** Draws a region at the top left corner. */ - void draw(TextureRegion region){ - draw(region, 0, 0, false, false); - } - - /** Draws a region at the center. */ - void drawCenter(TextureRegion region){ - draw(region, (width - region.width) / 2, (height - region.height) / 2, false, false); - } - - /** Draws a region at the center. */ - void drawCenter(TextureRegion region, boolean flipx, boolean flipy){ - draw(region, (width - region.width) / 2, (height - region.height) / 2, flipx, flipy); - } - - void drawScaled(Image image){ - graphics.drawImage(image.image.getScaledInstance(width, height, java.awt.Image.SCALE_AREA_AVERAGING), 0, 0, width, height, null); - } - - /** Draws an image at the top left corner. */ - void draw(Image image){ - draw(image, 0, 0); - } - - /** Draws an image at the coordinates specified. */ - void draw(Image image, int x, int y){ - graphics.drawImage(image.image, x, y, null); - } - - void draw(TextureRegion region, boolean flipx, boolean flipy){ - draw(region, 0, 0, flipx, flipy); - } - - void draw(TextureRegion region, int x, int y, boolean flipx, boolean flipy){ - GenRegion.validate(region); - - draw(ImagePacker.get(region), x, y, flipx, flipy); - } - - void draw(Image region, int x, int y, boolean flipx, boolean flipy){ - - int ofx = 0, ofy = 0; - - graphics.drawImage(region.image, - x, y, - x + region.width, - y + region.height, - (flipx ? region.width : 0) + ofx, - (flipy ? region.height : 0) + ofy, - (flipx ? 0 : region.width) + ofx, - (flipy ? 0 : region.height) + ofy, - null); - } - - /** @param name Name of texture file name to create, without any extensions. */ - void save(String name){ - try{ - ImageIO.write(image, "png", new File(name + ".png")); - }catch(IOException e){ - throw new RuntimeException(e); - } - } - - void save(String name, boolean antialias){ - save(name); - if(!antialias){ - new File(name + ".png").setLastModified(0); - } - } - - static int total(){ - return toDispose.size; - } - - static void dispose(){ - for(Image image : toDispose){ - image.graphics.dispose(); - } - toDispose.clear(); - } -} diff --git a/tools/src/mindustry/tools/ImagePacker.java b/tools/src/mindustry/tools/ImagePacker.java index 25c5fa6b27..a787540c15 100644 --- a/tools/src/mindustry/tools/ImagePacker.java +++ b/tools/src/mindustry/tools/ImagePacker.java @@ -2,8 +2,10 @@ package mindustry.tools; import arc.*; import arc.files.*; +import arc.graphics.*; import arc.graphics.g2d.*; import arc.graphics.g2d.TextureAtlas.*; +import arc.math.geom.*; import arc.struct.*; import arc.util.*; import arc.util.Log.*; @@ -12,20 +14,16 @@ import mindustry.*; import mindustry.content.*; import mindustry.core.*; import mindustry.ctype.*; -import mindustry.type.*; -import mindustry.world.*; import mindustry.world.blocks.*; -import javax.imageio.*; -import java.awt.image.*; import java.io.*; public class ImagePacker{ - static ObjectMap regionCache = new ObjectMap<>(); - static ObjectMap imageCache = new ObjectMap<>(); + static ObjectMap cache = new ObjectMap<>(); public static void main(String[] args) throws Exception{ Vars.headless = true; + //makes PNG loading slightly faster ArcNativesLoader.load(); Log.logger = new NoopLogHandler(); @@ -36,66 +34,59 @@ public class ImagePacker{ Fi.get("../../../assets-raw/sprites_out").walk(path -> { if(!path.extEquals("png")) return; - String fname = path.nameWithoutExtension(); - - try{ - BufferedImage image = ImageIO.read(path.file()); - - if(image == null) throw new IOException("image " + path.absolutePath() + " is null for terrible reasons"); - GenRegion region = new GenRegion(fname, path){{ - width = image.getWidth(); - height = image.getHeight(); - u2 = v2 = 1f; - u = v = 0f; - }}; - - regionCache.put(fname, region); - imageCache.put(fname, image); - }catch(IOException e){ - throw new RuntimeException(e); - } + cache.put(path.nameWithoutExtension(), new PackIndex(path)); }); Core.atlas = new TextureAtlas(){ @Override public AtlasRegion find(String name){ - if(!regionCache.containsKey(name)){ + if(!cache.containsKey(name)){ GenRegion region = new GenRegion(name, null); region.invalid = true; return region; } - return (AtlasRegion)regionCache.get(name); + + PackIndex index = cache.get(name); + if(index.pixmap == null){ + index.pixmap = new Pixmap(index.file); + index.region = new GenRegion(name, index.file){{ + width = index.pixmap.width; + height = index.pixmap.height; + u2 = v2 = 1f; + u = v = 0f; + }}; + } + return index.region; } @Override public AtlasRegion find(String name, TextureRegion def){ - if(!regionCache.containsKey(name)){ + if(!cache.containsKey(name)){ return (AtlasRegion)def; } - return (AtlasRegion)regionCache.get(name); + return find(name); } @Override public AtlasRegion find(String name, String def){ - if(!regionCache.containsKey(name)){ - return (AtlasRegion)regionCache.get(def); + if(!cache.containsKey(name)){ + return find(def); } - return (AtlasRegion)regionCache.get(name); + return find(name); } @Override public boolean has(String s){ - return regionCache.containsKey(s); + return cache.containsKey(s); } }; Draw.scl = 1f / Core.atlas.find("scale_marker").width; Time.mark(); - Generators.generate(); + Generators.run(); Log.info("&ly[Generator]&lc Total time to generate: &lg@&lcms", Time.elapsed()); - Log.info("&ly[Generator]&lc Total images created: &lg@", Image.total()); - Image.dispose(); + //Log.info("&ly[Generator]&lc Total images created: &lg@", Image.total()); //format: //character-ID=contentname:texture-name @@ -130,9 +121,7 @@ public class ImagePacker{ } static String texname(UnlockableContent c){ - if(c instanceof Block) return "block-" + c.name + "-medium"; - if(c instanceof UnitType) return "unit-" + c.name + "-medium"; - return c.getContentType() + "-" + c.name + "-icon"; + return c.getContentType() + "-" + c.name + "-ui"; } static void generate(String name, Runnable run){ @@ -141,15 +130,7 @@ public class ImagePacker{ Log.info("&ly[Generator]&lc Time to generate &lm@&lc: &lg@&lcms", name, Time.elapsed()); } - static BufferedImage buf(TextureRegion region){ - return imageCache.get(((AtlasRegion)region).name); - } - - static Image create(int width, int height){ - return new Image(width, height); - } - - static Image get(String name){ + static Pixmap get(String name){ return get(Core.atlas.find(name)); } @@ -157,18 +138,40 @@ public class ImagePacker{ return Core.atlas.has(name); } - static Image get(TextureRegion region){ - GenRegion.validate(region); + static Pixmap get(TextureRegion region){ + validate(region); - return new Image(imageCache.get(((AtlasRegion)region).name)); + return cache.get(((AtlasRegion)region).name).pixmap.copy(); } - static void replace(String name, Image image){ - image.save(name); + static void save(Pixmap pix, String path){ + Fi.get(path + ".png").writePng(pix); + } + + static void drawCenter(Pixmap pix, Pixmap other){ + pix.draw(other, pix.width/2 - other.width/2, pix.height/2 - other.height/2, true); + } + + static void saveScaled(Pixmap pix, String name, int size){ + Pixmap scaled = new Pixmap(size, size); + //TODO bad linear scaling + scaled.draw(pix, 0, 0, pix.width, pix.height, 0, 0, size, size, true, true); + save(scaled, name); + } + + static void drawScaledFit(Pixmap base, Pixmap image){ + Vec2 size = Scaling.fit.apply(image.width, image.height, base.width, base.height); + int wx = (int)size.x, wy = (int)size.y; + //TODO bad linear scaling + base.draw(image, 0, 0, image.width, image.height, base.width/2 - wx/2, base.height/2 - wy/2, wx, wy, true, true); + } + + static void replace(String name, Pixmap image){ + Fi.get(name + ".png").writePng(image); ((GenRegion)Core.atlas.find(name)).path.delete(); } - static void replace(TextureRegion region, Image image){ + static void replace(TextureRegion region, Pixmap image){ replace(((GenRegion)region).name, image); } @@ -176,6 +179,12 @@ public class ImagePacker{ throw new IllegalArgumentException(Strings.format(message, args)); } + static void validate(TextureRegion region){ + if(((GenRegion)region).invalid){ + ImagePacker.err("Region does not exist: @", ((GenRegion)region).name); + } + } + static class GenRegion extends AtlasRegion{ boolean invalid; Fi path; @@ -190,11 +199,15 @@ public class ImagePacker{ public boolean found(){ return !invalid; } + } - static void validate(TextureRegion region){ - if(((GenRegion)region).invalid){ - ImagePacker.err("Region does not exist: @", ((GenRegion)region).name); - } + static class PackIndex{ + @Nullable AtlasRegion region; + @Nullable Pixmap pixmap; + Fi file; + + public PackIndex(Fi file){ + this.file = file; } } } diff --git a/tools/src/mindustry/tools/ScriptMainGenerator.java b/tools/src/mindustry/tools/ScriptMainGenerator.java index 0406783661..14ffde2b2b 100644 --- a/tools/src/mindustry/tools/ScriptMainGenerator.java +++ b/tools/src/mindustry/tools/ScriptMainGenerator.java @@ -79,15 +79,18 @@ public class ScriptMainGenerator{ "mindustry.entities.abilities", "mindustry.ai.types", "mindustry.type.weather", + "mindustry.type.weapons", "mindustry.game.Objectives", "mindustry.world.blocks", - "mindustry.world.draw" + "mindustry.world.draw", + "mindustry.type" ); String classTemplate = "package mindustry.mod;\n" + "\n" + "import arc.struct.*;\n" + "/** Generated class. Maps simple class names to concrete classes. For use in JSON mods. */\n" + + "@SuppressWarnings(\"deprecation\")\n" + "public class ClassMap{\n" + " public static final ObjectMap> classes = new ObjectMap<>();\n" + " \n" +