Merging changes from private branch

This commit is contained in:
Anuken 2025-04-04 11:47:35 -04:00
parent cf5c6d0905
commit b7dbe54d76
161 changed files with 2484 additions and 1137 deletions

1
.gitignore vendored
View file

@ -23,6 +23,7 @@ ios/libs/
/tools/build/ /tools/build/
/tests/build/ /tests/build/
/server/build/ /server/build/
ios/libs/
changelog changelog
saves/ saves/
/core/assets-raw/fontgen/out/ /core/assets-raw/fontgen/out/

View file

@ -13,11 +13,13 @@ If you are submitting a new block, make sure it has a name and description, and
### Do not make large changes before discussing them first. ### Do not make large changes before discussing them first.
If you are interested in adding a large mechanic/feature or changing large amounts of code, first contact me (Anuken) via [Discord](https://discord.gg/mindustry) - either via PM or by posting in the `#pulls` channel. If you are interested in adding a large mechanic/feature or changing large amounts of code, first contact me (Anuken) via [Discord](https://discord.gg/mindustry) - either via PM or by posting in the `#pulls` channel.
For most changes, this should not be necessary. I just want to know if you're doing something big so I can offer advice and/or make sure you're not wasting your time on it. For most changes, this should not be necessary. I just want to know if you're doing something big, so I can offer advice and/or make sure you're not wasting your time on it.
### Do not make formatting PRs. ### Do not make formatting or "cleanup" PRs.
Yes, there are occurrences of trailing spaces, extra newlines, empty indents, and other tiny errors. No, I don't want to merge, view, or get notified by your 1-line PR fixing it. If you're implementing a PR with modification of *actual code*, feel free to fix formatting in the general vicinity of your changes, but please don't waste everyone's time with pointless changes. Yes, there are occurrences of trailing spaces, extra newlines, empty indents, and other tiny errors. No, I don't want to merge, view, or get notified by your 1-line PR fixing it. If you're implementing a PR with modification of *actual code*, feel free to fix formatting in the general vicinity of your changes, but please don't waste everyone's time with pointless changes.
I **especially** do not want to see PRs that apply any kind of automated analysis to the source code to "optimize" anything - my IDE can do that already. If the PR doesn't actually change anything useful, I'm not going to review or merge it.
## Style Guidelines ## Style Guidelines
### Follow the formatting guidelines. ### Follow the formatting guidelines.
@ -34,7 +36,7 @@ This means:
Import [this style file](.github/Mindustry-CodeStyle-IJ.xml) into IntelliJ to get correct formatting when developing Mindustry. Import [this style file](.github/Mindustry-CodeStyle-IJ.xml) into IntelliJ to get correct formatting when developing Mindustry.
### Do not use incompatible Java features (java.util.function, java.awt). ### Do not use incompatible Java features (java.util.function, java.awt, java.lang.Objects).
Android and RoboVM (iOS) do not support many of Java 8's features, such as the packages `java.util.function`, `java.util.stream` or `forEach` in collections. Do not use these in your code. Android and RoboVM (iOS) do not support many of Java 8's features, such as the packages `java.util.function`, `java.util.stream` or `forEach` in collections. Do not use these in your code.
If you need to use functional interfaces, use the ones in `arc.func`, which are more or less the same with different naming schemes. If you need to use functional interfaces, use the ones in `arc.func`, which are more or less the same with different naming schemes.
@ -66,7 +68,7 @@ Otherwise, use the `Tmp` variables for things like vector/shape operations, or c
If using a list, make it a static variable and clear it every time it is used. Re-use as much as possible. If using a list, make it a static variable and clear it every time it is used. Re-use as much as possible.
### Avoid bloated code and unnecessary getters/setters. ### Avoid bloated code and unnecessary getters/setters.
This is situational, but in essence what it means is to avoid using any sort of getters and setters unless absolutely necessary. Public or protected fields should suffice for most things. This is situational, but in essence, what it means is to avoid using any sort of getters and setters unless absolutely necessary. Public or protected fields should suffice for most things.
If something needs to be encapsulated in the future, IntelliJ can handle it with a few clicks. If something needs to be encapsulated in the future, IntelliJ can handle it with a few clicks.

View file

@ -54,6 +54,8 @@ public class Annotations{
/** Whether to generate a base class for this components. /** Whether to generate a base class for this components.
* An entity cannot have two base classes, so only one component can have base be true. */ * An entity cannot have two base classes, so only one component can have base be true. */
boolean base() default false; boolean base() default false;
/** Whether to generate a proper interface for this component class. */
boolean genInterface() default true;
} }
/** Indicates that a method is implemented by the annotation processor. */ /** Indicates that a method is implemented by the annotation processor. */

View file

@ -19,6 +19,7 @@ import javax.annotation.processing.*;
import javax.lang.model.element.*; import javax.lang.model.element.*;
import javax.lang.model.type.*; import javax.lang.model.type.*;
import java.lang.annotation.*; import java.lang.annotation.*;
import java.util.*;
@SupportedAnnotationTypes({ @SupportedAnnotationTypes({
"mindustry.annotations.Annotations.EntityDef", "mindustry.annotations.Annotations.EntityDef",
@ -97,6 +98,8 @@ public class EntityProcess extends BaseProcessor{
//create component interfaces //create component interfaces
for(Stype component : allComponents){ for(Stype component : allComponents){
TypeSpec.Builder inter = TypeSpec.interfaceBuilder(interfaceName(component)) TypeSpec.Builder inter = TypeSpec.interfaceBuilder(interfaceName(component))
.addModifiers(Modifier.PUBLIC).addAnnotation(EntityInterface.class); .addModifiers(Modifier.PUBLIC).addAnnotation(EntityInterface.class);
@ -116,45 +119,47 @@ public class EntityProcess extends BaseProcessor{
inter.addSuperinterface(ClassName.get(packageName, interfaceName(type))); inter.addSuperinterface(ClassName.get(packageName, interfaceName(type)));
} }
ObjectSet<String> signatures = new ObjectSet<>(); if(component.annotation(Component.class).genInterface()){
ObjectSet<String> signatures = new ObjectSet<>();
//add utility methods to interface //add utility methods to interface
for(Smethod method : component.methods()){ for(Smethod method : component.methods()){
//skip private methods, those are for internal use. //skip private methods, those are for internal use.
if(method.isAny(Modifier.PRIVATE, Modifier.STATIC)) continue; if(method.isAny(Modifier.PRIVATE, Modifier.STATIC)) continue;
//keep track of signatures used to prevent dupes //keep track of signatures used to prevent dupes
signatures.add(method.e.toString()); signatures.add(method.e.toString());
inter.addMethod(MethodSpec.methodBuilder(method.name()) inter.addMethod(MethodSpec.methodBuilder(method.name())
.addJavadoc(method.doc() == null ? "" : method.doc()) .addJavadoc(method.doc() == null ? "" : method.doc())
.addExceptions(method.thrownt()) .addExceptions(method.thrownt())
.addTypeVariables(method.typeVariables().map(TypeVariableName::get)) .addTypeVariables(method.typeVariables().map(TypeVariableName::get))
.returns(method.ret().toString().equals("void") ? TypeName.VOID : method.retn()) .returns(method.ret().toString().equals("void") ? TypeName.VOID : method.retn())
.addParameters(method.params().map(v -> ParameterSpec.builder(v.tname(), v.name()) .addParameters(method.params().map(v -> ParameterSpec.builder(v.tname(), v.name())
.build())).addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT).build()); .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();
//getter
if(!signatures.contains(cname + "()")){
inter.addMethod(MethodSpec.methodBuilder(cname).addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addAnnotations(Seq.with(field.annotations()).select(a -> a.toString().contains("Null") || a.toString().contains("Deprecated")).map(AnnotationSpec::get))
.addJavadoc(field.doc() == null ? "" : field.doc())
.returns(field.tname()).build());
} }
//setter //generate interface getters and setters for all "standard" fields
if(!field.is(Modifier.FINAL) && !signatures.contains(cname + "(" + field.mirror().toString() + ")") && for(Svar field : component.fields().select(e -> !e.is(Modifier.STATIC) && !e.is(Modifier.PRIVATE) && !e.has(Import.class))){
!field.annotations().contains(f -> f.toString().equals("@mindustry.annotations.Annotations.ReadOnly"))){ String cname = field.name();
inter.addMethod(MethodSpec.methodBuilder(cname).addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addJavadoc(field.doc() == null ? "" : field.doc()) //getter
.addParameter(ParameterSpec.builder(field.tname(), field.name()) if(!signatures.contains(cname + "()")){
.addAnnotations(Seq.with(field.annotations()) inter.addMethod(MethodSpec.methodBuilder(cname).addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.select(a -> a.toString().contains("Null") || a.toString().contains("Deprecated")).map(AnnotationSpec::get)).build()).build()); .addAnnotations(Seq.with(field.annotations()).select(a -> a.toString().contains("Null") || a.toString().contains("Deprecated")).map(AnnotationSpec::get))
.addJavadoc(field.doc() == null ? "" : field.doc())
.returns(field.tname()).build());
}
//setter
if(!field.is(Modifier.FINAL) && !signatures.contains(cname + "(" + field.mirror().toString() + ")") &&
!field.annotations().contains(f -> f.toString().equals("@mindustry.annotations.Annotations.ReadOnly"))){
inter.addMethod(MethodSpec.methodBuilder(cname).addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addJavadoc(field.doc() == null ? "" : field.doc())
.addParameter(ParameterSpec.builder(field.tname(), field.name())
.addAnnotations(Seq.with(field.annotations())
.select(a -> a.toString().contains("Null") || a.toString().contains("Deprecated")).map(AnnotationSpec::get)).build()).build());
}
} }
} }
@ -416,19 +421,34 @@ public class EntityProcess extends BaseProcessor{
//add all methods from components //add all methods from components
for(ObjectMap.Entry<String, Seq<Smethod>> entry : methods){ for(ObjectMap.Entry<String, Seq<Smethod>> entry : methods){
if(entry.value.contains(m -> m.has(Replace.class))){
//check replacements
if(entry.value.count(m -> m.has(Replace.class)) > 1){
err("Type " + type + " has multiple components replacing method " + entry.key + ".");
}
Smethod base = entry.value.find(m -> m.has(Replace.class));
entry.value.clear();
entry.value.add(base);
}
//check multi return //there are multiple @Replace implementations, or multiple non-void implementations.
if(entry.value.count(m -> !m.isAny(Modifier.NATIVE, Modifier.ABSTRACT) && !m.isVoid()) > 1){ if(entry.value.size > 1 && (entry.value.contains(m -> m.has(Replace.class)) || entry.value.count(m -> !m.isAny(Modifier.NATIVE, Modifier.ABSTRACT) && !m.isVoid()) > 1)){
err("Type " + type + " has multiple components implementing non-void method " + entry.key + ".");
//remove clutter
entry.value.removeAll(s -> s.is(Modifier.ABSTRACT));
Comparator<Smethod> comp = Structs.comps(
Structs.comps(
//highest priority first
Structs.comparingFloat(m -> m.has(MethodPriority.class) ? m.annotation(MethodPriority.class).value() : 0f),
//replacement means priority
Structs.comparingBool(m -> m.has(Replace.class))
),
//otherwise, the 'highest' subclass (most dependencies)
Structs.comparingInt(m -> getDependencies(m.type()).size)
);
Smethod best = entry.value.max(comp);
if(entry.value.contains(s -> best != s && comp.compare(s, best) == 0)){
err("Type " + type + " has multiple components implementing method " + entry.value.first() + " in an ambiguous way. Use MethodPriority to designate which one should be used. Implementations: " +
entry.value.map(s -> s.descString()));
}
entry.value.clear();
entry.value.add(best);
} }
entry.value.sort(Structs.comps(Structs.comparingFloat(m -> m.has(MethodPriority.class) ? m.annotation(MethodPriority.class).value() : 0), Structs.comparing(s -> s.up().getSimpleName().toString()))); entry.value.sort(Structs.comps(Structs.comparingFloat(m -> m.has(MethodPriority.class) ? m.annotation(MethodPriority.class).value() : 0), Structs.comparing(s -> s.up().getSimpleName().toString())));

View file

@ -28,6 +28,10 @@ public class Stype extends Selement<TypeElement>{
return interfaces().flatMap(s -> s.allInterfaces().add(s)).distinct(); return interfaces().flatMap(s -> s.allInterfaces().add(s)).distinct();
} }
public boolean isInterface(){
return e.getKind() == ElementKind.INTERFACE;
}
public Seq<Stype> superclasses(){ public Seq<Stype> superclasses(){
return Seq.with(BaseProcessor.typeu.directSupertypes(mirror())).map(Stype::of); return Seq.with(BaseProcessor.typeu.directSupertypes(mirror())).map(Stype::of);
} }

View file

@ -26,8 +26,8 @@ buildscript{
} }
plugins{ plugins{
id "org.jetbrains.kotlin.jvm" version "1.6.0" id "org.jetbrains.kotlin.jvm" version "2.1.10"
id "org.jetbrains.kotlin.kapt" version "1.6.0" id "org.jetbrains.kotlin.kapt" version "2.1.10"
} }
allprojects{ allprojects{
@ -37,7 +37,7 @@ allprojects{
group = 'com.github.Anuken' group = 'com.github.Anuken'
ext{ ext{
versionNumber = '7' versionNumber = '8'
if(!project.hasProperty("versionModifier")) versionModifier = 'release' if(!project.hasProperty("versionModifier")) versionModifier = 'release'
if(!project.hasProperty("versionType")) versionType = 'official' if(!project.hasProperty("versionType")) versionType = 'official'
appName = 'Mindustry' appName = 'Mindustry'
@ -366,7 +366,6 @@ project(":core"){
//these are completely unnecessary //these are completely unnecessary
tasks.kaptGenerateStubsKotlin.onlyIf{ false } tasks.kaptGenerateStubsKotlin.onlyIf{ false }
tasks.compileKotlin.onlyIf{ false } tasks.compileKotlin.onlyIf{ false }
tasks.inspectClassesForKotlinIC.onlyIf{ false }
} }
//comp** classes are only used for code generation //comp** classes are only used for code generation

View file

@ -46,9 +46,5 @@ void main(){
color.rgb = S2; color.rgb = S2;
} }
if(orig.g > 0.01){ gl_FragColor = vec4(max(S1, color).rgb, orig.a);
color = max(S1, color);
}
gl_FragColor = color;
} }

View file

@ -15,16 +15,22 @@ uniform float u_time;
varying vec2 v_texCoords; varying vec2 v_texCoords;
void main(){ void main(){
vec2 c = v_texCoords.xy; vec2 coords = v_texCoords * u_resolution + u_campos;
vec2 coords = vec2(c.x * u_resolution.x + u_campos.x, c.y * u_resolution.y + u_campos.y);
float btime = u_time / 5000.0; float btime = u_time / 5000.0;
float noise = (texture2D(u_noise, (coords) / NSCALE + vec2(btime) * vec2(-0.9, 0.8)).r + texture2D(u_noise, (coords) / NSCALE + vec2(btime * 1.1) * vec2(0.8, -1.0)).r) / 2.0; float noise = (texture2D(u_noise, (coords) / NSCALE + vec2(btime) * vec2(-0.9, 0.8)).r + texture2D(u_noise, (coords) / NSCALE + vec2(btime * 1.1) * vec2(0.8, -1.0)).r) / 2.0;
//TODO: pack noise texture
vec2 c = v_texCoords + (vec2(
texture2D(u_noise, (coords) / 170.0 + vec2(btime) * vec2(-0.9, 0.8)).r,
texture2D(u_noise, (coords) / 170.0 + vec2(btime * 1.1) * vec2(0.8, -1.0)).r
) - vec2(0.5)) * 8.0 / u_resolution;
vec4 color = texture2D(u_texture, c); vec4 color = texture2D(u_texture, c);
if(noise > 0.6){ if(noise > 0.6){
color.rgb = S2; color.rgb = S2;
}else if (noise > 0.54){ }else if(noise > 0.54){
color.rgb = S1; color.rgb = S1;
} }

View file

@ -55,6 +55,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
Log.info("[GL] Version: @", graphics.getGLVersion()); Log.info("[GL] Version: @", graphics.getGLVersion());
Log.info("[GL] Max texture size: @", maxTextureSize); Log.info("[GL] Max texture size: @", maxTextureSize);
Log.info("[GL] Using @ context.", gl30 != null ? "OpenGL 3" : "OpenGL 2"); Log.info("[GL] Using @ context.", gl30 != null ? "OpenGL 3" : "OpenGL 2");
if(gl30 == null) Log.warn("[GL] Your device or video drivers do not support OpenGL 3. This will cause performance issues.");
if(NvGpuInfo.hasMemoryInfo()){ if(NvGpuInfo.hasMemoryInfo()){
Log.info("[GL] Total available VRAM: @mb", NvGpuInfo.getMaxMemoryKB()/1024); Log.info("[GL] Total available VRAM: @mb", NvGpuInfo.getMaxMemoryKB()/1024);
} }
@ -206,6 +207,8 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
@Override @Override
public void update(){ public void update(){
PerfCounter.update.begin();
int targetfps = Core.settings.getInt("fpscap", 120); int targetfps = Core.settings.getInt("fpscap", 120);
boolean changed = lastTargetFps != targetfps && lastTargetFps != -1; boolean changed = lastTargetFps != targetfps && lastTargetFps != -1;
boolean limitFps = targetfps > 0 && targetfps <= 240; boolean limitFps = targetfps > 0 && targetfps <= 240;
@ -256,6 +259,8 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
Threads.sleep(toSleep / 1000000, (int)(toSleep % 1000000)); Threads.sleep(toSleep / 1000000, (int)(toSleep % 1000000));
} }
} }
PerfCounter.update.end();
} }
@Override @Override

View file

@ -28,7 +28,6 @@ import mindustry.net.*;
import mindustry.service.*; import mindustry.service.*;
import mindustry.ui.dialogs.*; import mindustry.ui.dialogs.*;
import mindustry.world.*; import mindustry.world.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.meta.*; import mindustry.world.meta.*;
import java.io.*; import java.io.*;
@ -47,6 +46,10 @@ public class Vars implements Loadable{
public static boolean loadedLogger = false, loadedFileLogger = false; public static boolean loadedLogger = false, loadedFileLogger = false;
/** Name of current Steam player. */ /** Name of current Steam player. */
public static String steamPlayerName = ""; public static String steamPlayerName = "";
/** Min game version for all mods. */
public static final int minModGameVersion = 136;
/** Min game version for java mods specifically - this is higher, as Java mods have more breaking changes. */
public static final int minJavaModGameVersion = 147;
/** If true, the BE server list is always used. */ /** If true, the BE server list is always used. */
public static boolean forceBeServers = false; public static boolean forceBeServers = false;
/** If true, mod code and scripts do not run. For internal testing only. This WILL break mods if enabled. */ /** If true, mod code and scripts do not run. For internal testing only. This WILL break mods if enabled. */
@ -71,7 +74,7 @@ public class Vars implements Loadable{
public static final String ghApi = "https://api.github.com"; public static final String ghApi = "https://api.github.com";
/** URL for discord invite. */ /** URL for discord invite. */
public static final String discordURL = "https://discord.gg/mindustry"; public static final String discordURL = "https://discord.gg/mindustry";
/** URL the links to the wiki's modding guide.*/ /** Link to the wiki's modding guide.*/
public static final String modGuideURL = "https://mindustrygame.github.io/wiki/modding/1-modding/"; public static final String modGuideURL = "https://mindustrygame.github.io/wiki/modding/1-modding/";
/** URLs to the JSON file containing all the BE servers. Only queried in BE. */ /** URLs to the JSON file containing all the BE servers. Only queried in BE. */
public static final String[] serverJsonBeURLs = {"https://raw.githubusercontent.com/Anuken/MindustryServerList/master/servers_be.json", "https://cdn.jsdelivr.net/gh/anuken/mindustryserverlist/servers_be.json"}; public static final String[] serverJsonBeURLs = {"https://raw.githubusercontent.com/Anuken/MindustryServerList/master/servers_be.json", "https://cdn.jsdelivr.net/gh/anuken/mindustryserverlist/servers_be.json"};
@ -111,8 +114,6 @@ public class Vars implements Loadable{
public static final float invasionGracePeriod = 20; public static final float invasionGracePeriod = 20;
/** min armor fraction damage; e.g. 0.05 = at least 5% damage */ /** min armor fraction damage; e.g. 0.05 = at least 5% damage */
public static final float minArmorDamage = 0.1f; public static final float minArmorDamage = 0.1f;
/** @deprecated see {@link CoreBlock#landDuration} instead! */
public static final @Deprecated float coreLandDuration = 160f;
/** size of tiles in units */ /** size of tiles in units */
public static final int tilesize = 8; public static final int tilesize = 8;
/** size of one tile payload (^2) */ /** size of one tile payload (^2) */
@ -267,7 +268,7 @@ public class Vars implements Loadable{
public static NetServer netServer; public static NetServer netServer;
public static NetClient netClient; public static NetClient netClient;
public static Player player; public static @Nullable Player player;
@Override @Override
public void loadAsync(){ public void loadAsync(){

View file

@ -7,6 +7,8 @@ import arc.math.geom.*;
import arc.struct.*; import arc.struct.*;
import arc.util.*; import arc.util.*;
import mindustry.content.*; import mindustry.content.*;
import mindustry.entities.*;
import mindustry.entities.Units.*;
import mindustry.game.EventType.*; import mindustry.game.EventType.*;
import mindustry.game.*; import mindustry.game.*;
import mindustry.game.Teams.*; import mindustry.game.Teams.*;
@ -14,6 +16,7 @@ import mindustry.gen.*;
import mindustry.logic.*; import mindustry.logic.*;
import mindustry.type.*; import mindustry.type.*;
import mindustry.world.*; import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.meta.*; import mindustry.world.meta.*;
import static mindustry.Vars.*; import static mindustry.Vars.*;
@ -28,11 +31,13 @@ public class BlockIndexer{
private int quadWidth, quadHeight; 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. */ /** Stores all ore quadrants on the map. Maps ID to qX to qY to a list of tiles with that ore. */
private IntSeq[][][] ores; private IntSeq[][][] ores, wallOres;
/** Stores all damaged tile entities by team. */ /** Stores all damaged tile entities by team. */
private Seq<Building>[] damagedTiles = new Seq[Team.all.length]; private Seq<Building>[] damagedTiles = new Seq[Team.all.length];
/** All ores present on the map - can be wall or floor. */
private Seq<Item> allPresentOres = new Seq<>();
/** All ores available on this map. */ /** All ores available on this map. */
private ObjectIntMap<Item> allOres = new ObjectIntMap<>(); private ObjectIntMap<Item> allOres = new ObjectIntMap<>(), allWallOres = new ObjectIntMap<>();
/** Stores teams that are present here as tiles. */ /** Stores teams that are present here as tiles. */
private Seq<Team> activeTeams = new Seq<>(Team.class); private Seq<Team> activeTeams = new Seq<>(Team.class);
/** Maps teams to a map of flagged tiles by flag. */ /** Maps teams to a map of flagged tiles by flag. */
@ -41,6 +46,8 @@ public class BlockIndexer{
private boolean[] blocksPresent; private boolean[] blocksPresent;
/** Array used for returning and reusing. */ /** Array used for returning and reusing. */
private Seq<Building> breturnArray = new Seq<>(Building.class); private Seq<Building> breturnArray = new Seq<>(Building.class);
/** Maps block flag to a list of floor tiles that have it. */
private Seq<Tile>[] floorMap;
public BlockIndexer(){ public BlockIndexer(){
clearFlags(); clearFlags();
@ -53,15 +60,23 @@ public class BlockIndexer{
addIndex(event.tile); addIndex(event.tile);
}); });
Events.on(TileFloorChangeEvent.class, event -> {
removeFloorIndex(event.tile, event.previous);
addFloorIndex(event.tile, event.floor);
});
Events.on(WorldLoadEvent.class, event -> { Events.on(WorldLoadEvent.class, event -> {
damagedTiles = new Seq[Team.all.length]; damagedTiles = new Seq[Team.all.length];
flagMap = new Seq[Team.all.length][BlockFlag.all.length]; flagMap = new Seq[Team.all.length][BlockFlag.all.length];
floorMap = new Seq[BlockFlag.all.length];
activeTeams = new Seq<>(Team.class); activeTeams = new Seq<>(Team.class);
clearFlags(); clearFlags();
allOres.clear(); allOres.clear();
allWallOres.clear();
ores = new IntSeq[content.items().size][][]; ores = new IntSeq[content.items().size][][];
wallOres = new IntSeq[content.items().size][][];
quadWidth = Mathf.ceil(world.width() / (float)quadrantSize); quadWidth = Mathf.ceil(world.width() / (float)quadrantSize);
quadHeight = Mathf.ceil(world.height() / (float)quadrantSize); quadHeight = Mathf.ceil(world.height() / (float)quadrantSize);
blocksPresent = new boolean[content.blocks().size]; blocksPresent = new boolean[content.blocks().size];
@ -78,28 +93,67 @@ public class BlockIndexer{
for(Tile tile : world.tiles){ for(Tile tile : world.tiles){
process(tile); process(tile);
var drop = tile.drop(); addFloorIndex(tile, tile.floor());
if(drop != null){ Item drop;
int qx = (tile.x / quadrantSize); int qx = tile.x / quadrantSize, qy = tile.y / quadrantSize;
int qy = (tile.y / quadrantSize); if(tile.block() == Blocks.air){
if((drop = tile.drop()) != null){
//add position of quadrant to list //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] == null){ if(ores[drop.id][qx][qy] == null) ores[drop.id][qx][qy] = new IntSeq(false, 16);
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()); ores[drop.id][qx][qy].add(tile.pos());
allOres.increment(drop); allOres.increment(drop);
} }
}else if((drop = tile.wallDrop()) != null){
//add position of quadrant to list
if(wallOres[drop.id] == null) wallOres[drop.id] = new IntSeq[quadWidth][quadHeight];
if(wallOres[drop.id][qx][qy] == null) wallOres[drop.id][qx][qy] = new IntSeq(false, 16);
wallOres[drop.id][qx][qy].add(tile.pos());
allWallOres.increment(drop);
} }
} }
updatePresentOres();
}); });
} }
public Seq<Item> getAllPresentOres(){
return allPresentOres;
}
private void updatePresentOres(){
allPresentOres.clear();
for(Item item : content.items()){
if(hasOre(item) || hasWallOre(item)){
allPresentOres.add(item);
}
}
}
private void removeFloorIndex(Tile tile, Floor floor){
if(floor.flags.size == 0) return;
for(var flag : floor.flags.array){
getFlaggedFloors(flag).remove(tile);
}
}
private void addFloorIndex(Tile tile, Floor floor){
if(floor.flags.size == 0 || !floor.shouldIndex(tile)) return;
for(var flag : floor.flags.array){
getFlaggedFloors(flag).add(tile);
}
}
public Seq<Tile> getFlaggedFloors(BlockFlag flag){
if(floorMap[flag.ordinal()] == null){
floorMap[flag.ordinal()] = new Seq<>(false);
}
return floorMap[flag.ordinal()];
}
public void removeIndex(Tile tile){ public void removeIndex(Tile tile){
var team = tile.team(); var team = tile.team();
if(tile.build != null && tile.isCenter()){ if(tile.build != null && tile.isCenter()){
@ -143,30 +197,37 @@ public class BlockIndexer{
public void addIndex(Tile tile){ public void addIndex(Tile tile){
process(tile); process(tile);
var drop = tile.drop(); Item drop = tile.drop(), wallDrop = tile.wallDrop();
if(drop != null && ores != null){ if(drop == null && wallDrop == null) return;
int qx = tile.x / quadrantSize; int qx = tile.x / quadrantSize, qy = tile.y / quadrantSize;
int qy = tile.y / quadrantSize; int pos = tile.pos();
if(ores[drop.id] == null){ if(tile.block() == Blocks.air){
ores[drop.id] = new IntSeq[quadWidth][quadHeight]; if(drop != null){ //floor
} if(ores[drop.id] == null) ores[drop.id] = new IntSeq[quadWidth][quadHeight];
if(ores[drop.id][qx][qy] == null){ if(ores[drop.id][qx][qy] == null) ores[drop.id][qx][qy] = new IntSeq(false, 16);
ores[drop.id][qx][qy] = new IntSeq(false, 16); if(ores[drop.id][qx][qy].addUnique(pos)){
} int old = allOres.increment(drop); //increment ore count only if not already counted
if(old == 0) updatePresentOres();
int pos = tile.pos();
var seq = ores[drop.id][qx][qy];
if(tile.block() == Blocks.air){
//add the index if it is a valid new spot to mine at
if(!seq.contains(pos)){
seq.add(pos);
allOres.increment(drop);
} }
}else if(seq.contains(pos)){ //otherwise, it likely became blocked, remove it }
seq.removeValue(pos); if(wallDrop != null && wallOres != null && wallOres[wallDrop.id] != null && wallOres[wallDrop.id][qx][qy] != null && wallOres[wallDrop.id][qx][qy].removeValue(pos)){ //wall
allOres.increment(drop, -1); int old = allWallOres.increment(wallDrop, -1);
if(old == 1) updatePresentOres();
}
}else{
if(wallDrop != null){ //wall
if(wallOres[wallDrop.id] == null) wallOres[wallDrop.id] = new IntSeq[quadWidth][quadHeight];
if(wallOres[wallDrop.id][qx][qy] == null) wallOres[wallDrop.id][qx][qy] = new IntSeq(false, 16);
if(wallOres[wallDrop.id][qx][qy].addUnique(pos)){
int old = allWallOres.increment(wallDrop); //increment ore count only if not already counted
if(old == 0) updatePresentOres();
}
}
if(drop != null && ores != null && ores[drop.id] != null&& ores[drop.id][qx][qy] != null && ores[drop.id][qx][qy].removeValue(pos)){ //floor
int old = allOres.increment(drop, -1);
if(old == 1) updatePresentOres();
} }
} }
@ -194,6 +255,11 @@ public class BlockIndexer{
return allOres.get(item) > 0; return allOres.get(item) > 0;
} }
/** @return whether this item is present on this map as a wall ore. */
public boolean hasWallOre(Item item){
return allWallOres.get(item) > 0;
}
/** Returns all damaged tiles by team. */ /** Returns all damaged tiles by team. */
public Seq<Building> getDamaged(Team team){ public Seq<Building> getDamaged(Team team){
if(damagedTiles[team.id] == null){ if(damagedTiles[team.id] == null){
@ -348,7 +414,7 @@ public class BlockIndexer{
breturnArray.size = 0; breturnArray.size = 0;
} }
public Building findEnemyTile(Team team, float x, float y, float range, Boolf<Building> pred){ public Building findEnemyTile(Team team, float x, float y, float range, BuildingPriorityf priority, Boolf<Building> pred){
Building target = null; Building target = null;
float targetDist = 0; float targetDist = 0;
@ -362,10 +428,10 @@ public class BlockIndexer{
//if a block has the same priority, the closer one should be targeted //if a block has the same priority, the closer one should be targeted
float dist = candidate.dst(x, y) - candidate.hitSize() / 2f; float dist = candidate.dst(x, y) - candidate.hitSize() / 2f;
if(target == null || if(target == null ||
//if its closer and is at least equal priority //if it is closer and is at least equal priority
(dist < targetDist && candidate.block.priority >= target.block.priority) || (dist < targetDist && priority.priority(candidate) >= priority.priority(target)) ||
// block has higher priority (so range doesnt matter) // block has higher priority (so range doesn't matter)
(candidate.block.priority > target.block.priority)){ priority.priority(candidate) > priority.priority(target)){
target = candidate; target = candidate;
targetDist = dist; targetDist = dist;
} }
@ -374,6 +440,10 @@ public class BlockIndexer{
return target; return target;
} }
public Building findEnemyTile(Team team, float x, float y, float range, Boolf<Building> pred){
return findEnemyTile(team, x, y, range, UnitSorts.buildingDefault, pred);
}
public Building findTile(Team team, float x, float y, float range, Boolf<Building> pred){ public Building findTile(Team team, float x, float y, float range, Boolf<Building> pred){
return findTile(team, x, y, range, pred, false); return findTile(team, x, y, range, pred, false);
} }
@ -432,11 +502,43 @@ public class BlockIndexer{
return null; return null;
} }
/** Find the closest ore wall relative to a position. */
public Tile findClosestWallOre(float xp, float yp, Item item){
//(stolen from foo's client :))))
if(wallOres[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 = wallOres[item.id][qx][qy];
if(arr != null && arr.size > 0){
Tile tile = world.tile(arr.first());
if(tile.block() != Blocks.air){
float dst = Mathf.dst2(xp, yp, tile.worldx(), tile.worldy());
if(closest == null || dst < minDst){
closest = tile;
minDst = dst;
}
}
}
}
}
return closest;
}
return null;
}
/** Find the closest ore block relative to a position. */ /** Find the closest ore block relative to a position. */
public Tile findClosestOre(Unit unit, Item item){ public Tile findClosestOre(Unit unit, Item item){
return findClosestOre(unit.x, unit.y, item); return findClosestOre(unit.x, unit.y, item);
} }
/** Find the closest ore block relative to a position. */
public Tile findClosestWallOre(Unit unit, Item item){
return findClosestWallOre(unit.x, unit.y, item);
}
private void process(Tile tile){ private void process(Tile tile){
var team = tile.team(); var team = tile.team();
//only process entity changes with centered tiles //only process entity changes with centered tiles

View file

@ -1082,21 +1082,6 @@ public class ControlPathfinder implements Runnable{
return raycast(unit.team().id, unit.type.pathCost, x1, y1, x2, y2); return raycast(unit.team().id, unit.type.pathCost, x1, y1, x2, y2);
} }
@Deprecated
public int nextTargetId(){
return 0;
}
@Deprecated
public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out){
return getPathPosition(unit, pathId, destination, out, null);
}
@Deprecated
public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out, @Nullable boolean[] noResultFound){
return getPathPosition(unit, destination, destination, out, noResultFound);
}
public boolean getPathPosition(Unit unit, Vec2 destination, Vec2 out, @Nullable boolean[] noResultFound){ public boolean getPathPosition(Unit unit, Vec2 destination, Vec2 out, @Nullable boolean[] noResultFound){
return getPathPosition(unit, destination, destination, out, noResultFound); return getPathPosition(unit, destination, destination, out, noResultFound);
} }

View file

@ -5,6 +5,7 @@ import arc.func.*;
import arc.math.*; import arc.math.*;
import arc.math.geom.*; import arc.math.geom.*;
import arc.struct.*; import arc.struct.*;
import arc.util.TaskQueue;
import arc.util.*; import arc.util.*;
import mindustry.annotations.Annotations.*; import mindustry.annotations.Annotations.*;
import mindustry.core.*; import mindustry.core.*;
@ -16,11 +17,14 @@ import mindustry.world.blocks.environment.*;
import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.*;
import mindustry.world.meta.*; import mindustry.world.meta.*;
import java.util.*;
import static mindustry.Vars.*; import static mindustry.Vars.*;
import static mindustry.world.meta.BlockFlag.*; import static mindustry.world.meta.BlockFlag.*;
public class Pathfinder implements Runnable{ public class Pathfinder implements Runnable{
private static final long maxUpdate = Time.millisToNanos(8); private static final long maxUpdate = Time.millisToNanos(8);
private static final int neverRefresh = Integer.MAX_VALUE;
private static final int updateFPS = 60; private static final int updateFPS = 60;
private static final int updateInterval = 1000 / updateFPS; private static final int updateInterval = 1000 / updateFPS;
@ -30,51 +34,66 @@ public class Pathfinder implements Runnable{
static final int impassable = -1; static final int impassable = -1;
public static final int public static final int
fieldCore = 0; fieldCore = 0,
maxFields = 10;
public static final Seq<Prov<Flowfield>> fieldTypes = Seq.with( public static final Seq<Prov<Flowfield>> fieldTypes = Seq.with(
EnemyCoreField::new EnemyCoreField::new
); );
public static final int public static final int
costGround = 0, costGround = 0,
costLegs = 1, costLegs = 1,
costNaval = 2, costNaval = 2,
costHover = 3; costNeoplasm = 3,
costNone = 4,
costHover = 5,
maxCosts = 8;
public static final Seq<PathCost> costTypes = Seq.with( public static final Seq<PathCost> costTypes = Seq.with(
//ground //ground
(team, tile) -> (team, tile) ->
(PathTile.allDeep(tile) || ((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0) && PathTile.solid(tile)) ? impassable : 1 + (PathTile.allDeep(tile) || ((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0) && PathTile.solid(tile)) ? impassable : 1 +
PathTile.health(tile) * 5 + PathTile.health(tile) * 5 +
(PathTile.nearSolid(tile) ? 2 : 0) + (PathTile.nearSolid(tile) ? 2 : 0) +
(PathTile.nearLiquid(tile) ? 6 : 0) + (PathTile.nearLiquid(tile) ? 6 : 0) +
(PathTile.deep(tile) ? 6000 : 0) + (PathTile.deep(tile) ? 6000 : 0) +
(PathTile.damages(tile) ? 30 : 0), (PathTile.damages(tile) ? 30 : 0),
//legs //legs
(team, tile) -> (team, tile) ->
PathTile.legSolid(tile) ? impassable : 1 + PathTile.legSolid(tile) ? impassable : 1 +
(PathTile.deep(tile) ? 6000 : 0) + //leg units can now drown (PathTile.deep(tile) ? 6000 : 0) + //leg units can now drown
(PathTile.solid(tile) ? 5 : 0), (PathTile.solid(tile) ? 5 : 0),
//water //water
(team, tile) -> (team, tile) ->
(!PathTile.liquid(tile) ? 6000 : 1) + (!PathTile.liquid(tile) ? 6000 : 1) +
PathTile.health(tile) * 5 + PathTile.health(tile) * 5 +
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) + (PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) +
(PathTile.deep(tile) ? 0 : 1) + (PathTile.deep(tile) ? 0 : 1) +
(PathTile.damages(tile) ? 35 : 0), (PathTile.damages(tile) ? 35 : 0),
//hover //neoplasm veins
(team, tile) -> (team, tile) ->
(((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0) && PathTile.solid(tile)) ? impassable : 1 + (PathTile.deep(tile) || (PathTile.team(tile) == 0 && PathTile.solid(tile))) ? impassable : 1 +
PathTile.health(tile) * 5 + (PathTile.health(tile) * 3) +
(PathTile.nearSolid(tile) ? 2 : 0) (PathTile.nearSolid(tile) ? 2 : 0) +
(PathTile.nearLiquid(tile) ? 2 : 0),
//none (flat cost)
(team, tile) -> 1,
//hover
(team, tile) ->
(((PathTile.team(tile) == team && !PathTile.teamPassable(tile)) || PathTile.team(tile) == 0) && PathTile.solid(tile)) ? impassable : 1 +
PathTile.health(tile) * 5 +
(PathTile.nearSolid(tile) ? 2 : 0)
); );
/** tile data, see PathTileStruct - kept as a separate array for threading reasons */ /** tile data, see PathTileStruct - kept as a separate array for threading reasons */
int[] tiles = new int[0]; int[] tiles = {};
/** maps team, cost, type to flow field*/ /** maps team, cost, type to flow field*/
Flowfield[][][] cache; Flowfield[][][] cache;
@ -86,6 +105,8 @@ public class Pathfinder implements Runnable{
@Nullable Thread thread; @Nullable Thread thread;
IntSeq tmpArray = new IntSeq(); IntSeq tmpArray = new IntSeq();
boolean needsRefresh;
public Pathfinder(){ public Pathfinder(){
clearCache(); clearCache();
@ -100,6 +121,7 @@ public class Pathfinder implements Runnable{
mainList = new Seq<>(); mainList = new Seq<>();
clearCache(); clearCache();
for(int i = 0; i < tiles.length; i++){ for(int i = 0; i < tiles.length; i++){
Tile tile = world.tiles.geti(i); Tile tile = world.tiles.geti(i);
tiles[i] = packTile(tile); tiles[i] = packTile(tile);
@ -153,10 +175,36 @@ public class Pathfinder implements Runnable{
} }
} }
}); });
Events.run(Trigger.afterGameUpdate, () -> {
//only refresh periodically (every 2 frames) to batch flowfield updates
//TODO: is it worth switching to a timestamp based system instead that updates every X milliseconds?
if(needsRefresh && Core.graphics.getFrameId() % 2 == 0){
needsRefresh = false;
//can't iterate through array so use the map, which should not lead to problems
for(Flowfield path : mainList){
//paths with a refresh rate should not be updated by tiles changing
if(path != null && path.needsRefresh()){
synchronized(path.targets){
//TODO: this is super slow and forces a refresh for every tile changed!
path.updateTargetPositions();
}
}
}
//mark every flow field as dirty, so it updates when it's done
queue.post(() -> {
for(Flowfield data : threadList){
data.dirty = true;
}
});
}
});
} }
private void clearCache(){ private void clearCache(){
cache = new Flowfield[256][5][5]; cache = new Flowfield[256][maxCosts][maxFields];
} }
/** Packs a tile into its internal representation. */ /** Packs a tile into its internal representation. */
@ -185,19 +233,19 @@ public class Pathfinder implements Runnable{
int tid = tile.getTeamID(); int tid = tile.getTeamID();
return PathTile.get( return PathTile.get(
tile.build == null || !solid || tile.block() instanceof CoreBlock ? 0 : Math.min((int)(tile.build.health / 40), 80), tile.build == null || !solid || tile.block() instanceof CoreBlock ? 0 : Math.min((int)(tile.build.health / 40), 80),
tid == 0 && tile.build != null && state.rules.coreCapture ? 255 : tid, //use teamid = 255 when core capture is enabled to mark out derelict structures tid == 0 && tile.build != null && state.rules.coreCapture ? 255 : tid, //use teamid = 255 when core capture is enabled to mark out derelict structures
solid, solid,
tile.floor().isLiquid, tile.floor().isLiquid,
tile.legSolid(), tile.legSolid(),
nearLiquid, nearLiquid,
nearGround, nearGround,
nearSolid, nearSolid,
nearLegSolid, nearLegSolid,
tile.floor().isDeep(), tile.floor().isDeep(),
tile.floor().damageTaken > 0.00001f, tile.floor().damageTaken > 0.00001f,
allDeep, allDeep,
tile.block().teamPassable tile.block().teamPassable
); );
} }
@ -223,6 +271,7 @@ public class Pathfinder implements Runnable{
thread = null; thread = null;
} }
queue.clear(); queue.clear();
needsRefresh = false;
} }
/** Update a tile in the internal pathfinding grid. /** Update a tile in the internal pathfinding grid.
@ -237,23 +286,10 @@ public class Pathfinder implements Runnable{
} }
}); });
//can't iterate through array so use the map, which should not lead to problems
for(Flowfield path : mainList){
if(path != null){
synchronized(path.targets){
path.updateTargetPositions();
}
}
}
//mark every flow field as dirty, so it updates when it's done
queue.post(() -> {
for(Flowfield data : threadList){
data.dirty = true;
}
});
controlPath.updateTile(tile); controlPath.updateTile(tile);
//queue a refresh sometime in the future
needsRefresh = true;
} }
/** Thread implementation. */ /** Thread implementation. */
@ -307,48 +343,55 @@ public class Pathfinder implements Runnable{
/** Gets next tile to travel to. Main thread only. */ /** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Flowfield path){ public @Nullable Tile getTargetTile(Tile tile, Flowfield path){
return getTargetTile(tile, path, true);
}
/** Gets next tile to travel to. Main thread only. */
public @Nullable Tile getTargetTile(Tile tile, Flowfield path, boolean diagonals){
if(tile == null) return null; if(tile == null) return null;
//uninitialized flowfields are not applicable //uninitialized flowfields are not applicable
if(!path.initialized){ //also ignore paths with no targets, there is no destination
if(!path.initialized || path.targets.size == 0){
return tile; return tile;
} }
//if refresh rate is positive, queue a refresh //if refresh rate is positive, queue a refresh
if(path.refreshRate > 0 && Time.timeSinceMillis(path.lastUpdateTime) > path.refreshRate){ if(path.refreshRate > 0 && path.refreshRate != neverRefresh && Time.timeSinceMillis(path.lastUpdateTime) > path.refreshRate && path.frontier.size == 0){
path.lastUpdateTime = Time.millis(); path.lastUpdateTime = Time.millis();
tmpArray.clear(); tmpArray.clear();
path.getPositions(tmpArray); path.getPositions(tmpArray);
synchronized(path.targets){ synchronized(path.targets){
//make sure the position actually changed path.updateTargetPositions();
if(!(path.targets.size == 1 && tmpArray.size == 1 && path.targets.first() == tmpArray.first())){
path.updateTargetPositions();
//queue an update //queue an update
queue.post(() -> updateTargets(path)); queue.post(() -> updateTargets(path));
}
} }
} }
//use complete weights if possible; these contain a complete flow field that is not being updated //use complete weights if possible; these contain a complete flow field that is not being updated
int[] values = path.hasComplete ? path.completeWeights : path.weights; int[] values = path.hasComplete ? path.completeWeights : path.weights;
int apos = tile.array(); int res = path.resolution;
int ww = path.width;
int apos = tile.x/res + tile.y/res * ww;
int value = values[apos]; int value = values[apos];
var points = diagonals ? Geometry.d8 : Geometry.d4;
Tile current = null; Tile current = null;
int tl = 0; int tl = 0;
for(Point2 point : Geometry.d8){ for(Point2 point : points){
int dx = tile.x + point.x, dy = tile.y + point.y; int dx = tile.x + point.x * res, dy = tile.y + point.y * res;
Tile other = world.tile(dx, dy); Tile other = world.tile(dx, dy);
if(other == null) continue; if(other == null) continue;
int packed = world.packArray(dx, dy); int packed = dx/res + dy/res * ww;
if(values[packed] < value && (current == null || values[packed] < tl) && path.passable(packed) && if(values[packed] < value && (current == null || values[packed] < tl) && path.passable(packed) &&
!(point.x != 0 && point.y != 0 && (!path.passable(world.packArray(tile.x + point.x, tile.y)) || !path.passable(world.packArray(tile.x, tile.y + point.y))))){ //diagonal corner trap !(point.x != 0 && point.y != 0 && (!path.passable(((tile.x + point.x)/res + tile.y/res*ww)) || !path.passable((tile.x/res + (tile.y + point.y)/res*ww))))){ //diagonal corner trap
current = other; current = other;
tl = values[packed]; tl = values[packed];
} }
@ -365,13 +408,21 @@ public class Pathfinder implements Runnable{
//increment search, but do not clear the frontier //increment search, but do not clear the frontier
path.search++; path.search++;
//search overflow; reset everything.
if(path.search >= Short.MAX_VALUE){
Arrays.fill(path.searches, (short)0);
path.search = 1;
}
synchronized(path.targets){ synchronized(path.targets){
//add targets //add targets
for(int i = 0; i < path.targets.size; i++){ for(int i = 0; i < path.targets.size; i++){
int pos = path.targets.get(i); int pos = path.targets.get(i);
if(pos >= path.weights.length) continue;
path.weights[pos] = 0; path.weights[pos] = 0;
path.searches[pos] = path.search; path.searches[pos] = (short)path.search;
path.frontier.addFirst(pos); path.frontier.addFirst(pos);
} }
} }
@ -390,7 +441,7 @@ public class Pathfinder implements Runnable{
*/ */
private void registerPath(Flowfield path){ private void registerPath(Flowfield path){
path.lastUpdateTime = Time.millis(); path.lastUpdateTime = Time.millis();
path.setup(tiles.length); path.setup();
threadList.add(path); threadList.add(path);
@ -398,9 +449,7 @@ public class Pathfinder implements Runnable{
Core.app.post(() -> mainList.add(path)); Core.app.post(() -> mainList.add(path));
//fill with impassables by default //fill with impassables by default
for(int i = 0; i < tiles.length; i++){ Arrays.fill(path.weights, impassable);
path.weights[i] = impassable;
}
//add targets //add targets
for(int i = 0; i < path.targets.size; i++){ for(int i = 0; i < path.targets.size; i++){
@ -416,6 +465,7 @@ public class Pathfinder implements Runnable{
long start = Time.nanos(); long start = Time.nanos();
int counter = 0; int counter = 0;
int w = path.width, h = path.height;
while(path.frontier.size > 0){ while(path.frontier.size > 0){
int tile = path.frontier.removeLast(); int tile = path.frontier.removeLast();
@ -423,7 +473,7 @@ public class Pathfinder implements Runnable{
int cost = path.weights[tile]; int cost = path.weights[tile];
//pathfinding overflowed for some reason, time to bail. the next block update will handle this, hopefully //pathfinding overflowed for some reason, time to bail. the next block update will handle this, hopefully
if(path.frontier.size >= world.width() * world.height()){ if(path.frontier.size >= w * h){
path.frontier.clear(); path.frontier.clear();
return; return;
} }
@ -431,12 +481,12 @@ public class Pathfinder implements Runnable{
if(cost != impassable){ if(cost != impassable){
for(Point2 point : Geometry.d4){ for(Point2 point : Geometry.d4){
int dx = (tile % wwidth) + point.x, dy = (tile / wwidth) + point.y; int dx = (tile % w) + point.x, dy = (tile / w) + point.y;
if(dx < 0 || dy < 0 || dx >= wwidth || dy >= wheight) continue; if(dx < 0 || dy < 0 || dx >= w || dy >= h) continue;
int newPos = tile + point.x + point.y * wwidth; int newPos = dx + dy * w;
int otherCost = path.cost.getCost(path.team.id, tiles[newPos]); int otherCost = path.getCost(tiles, newPos);
if((path.weights[newPos] > cost + otherCost || path.searches[newPos] < path.search) && otherCost != impassable){ if((path.weights[newPos] > cost + otherCost || path.searches[newPos] < path.search) && otherCost != impassable){
path.frontier.addFirst(newPos); path.frontier.addFirst(newPos);
@ -523,7 +573,7 @@ public class Pathfinder implements Runnable{
* Concrete subclasses must specify a way to fetch costs and destinations. * Concrete subclasses must specify a way to fetch costs and destinations.
*/ */
public static abstract class Flowfield{ public static abstract class Flowfield{
/** Refresh rate in milliseconds. Return any number <= 0 to disable. */ /** Refresh rate in milliseconds. <= 0 to disable. */
protected int refreshRate; protected int refreshRate;
/** Team this path is for. Set before using. */ /** Team this path is for. Set before using. */
protected Team team = Team.derelict; protected Team team = Team.derelict;
@ -537,12 +587,16 @@ public class Pathfinder implements Runnable{
/** costs of getting to a specific tile */ /** costs of getting to a specific tile */
public int[] weights; public int[] weights;
/** search IDs of each position - the highest, most recent search is prioritized and overwritten */ /** search IDs of each position - the highest, most recent search is prioritized and overwritten */
public int[] searches; public short[] searches;
/** the last "complete" weights of this tilemap. */ /** the last "complete" weights of this tilemap. */
public int[] completeWeights; public int[] completeWeights;
/** Scaling factor. For example, resolution = 2 means tiles are twice as large. */
public final int resolution;
public final int width, height;
/** search frontier, these are Pos objects */ /** search frontier, these are Pos objects */
IntQueue frontier = new IntQueue(); final IntQueue frontier = new IntQueue();
/** all target positions; these positions have a cost of 0, and must be synchronized on! */ /** all target positions; these positions have a cost of 0, and must be synchronized on! */
final IntSeq targets = new IntSeq(); final IntSeq targets = new IntSeq();
/** current search ID */ /** current search ID */
@ -552,14 +606,44 @@ public class Pathfinder implements Runnable{
/** whether this flow field is ready to be used */ /** whether this flow field is ready to be used */
boolean initialized; boolean initialized;
void setup(int length){ public Flowfield(){
this(1);
}
public Flowfield(int resolution){
this.resolution = resolution;
this.width = Mathf.ceil((float)wwidth / resolution);
this.height = Mathf.ceil((float)wheight / resolution);
}
void setup(){
int length = width * height;
this.weights = new int[length]; this.weights = new int[length];
this.searches = new int[length]; this.searches = new short[length];
this.completeWeights = new int[length]; this.completeWeights = new int[length];
this.frontier.ensureCapacity((length) / 4); this.frontier.ensureCapacity((length) / 4);
this.initialized = true; this.initialized = true;
} }
public int getCost(int[] tiles, int pos){
return cost.getCost(team.id, tiles[pos]);
}
public boolean hasTargets(){
return targets.size > 0;
}
/** @return the next tile to travel to for this flowfield. Main thread only. */
public @Nullable Tile getNextTile(Tile from, boolean diagonals){
return pathfinder.getTargetTile(from, this, diagonals);
}
/** @return the next tile to travel to for this flowfield. Main thread only. */
public @Nullable Tile getNextTile(Tile from){
return pathfinder.getTargetTile(from, this);
}
public boolean hasCompleteWeights(){ public boolean hasCompleteWeights(){
return hasComplete && completeWeights != null; return hasComplete && completeWeights != null;
} }
@ -569,6 +653,11 @@ public class Pathfinder implements Runnable{
getPositions(targets); getPositions(targets);
} }
/** @return whether this flow field should be refreshed after the current block update */
public boolean needsRefresh(){
return refreshRate == 0;
}
protected boolean passable(int pos){ protected boolean passable(int pos){
int amount = cost.getCost(team.id, pathfinder.tiles[pos]); int amount = cost.getCost(team.id, pathfinder.tiles[pos]);
//edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable //edge case: naval reports costs of 6000+ for non-liquids, even though they are not technically passable

View file

@ -17,7 +17,6 @@ import mindustry.gen.*;
import mindustry.graphics.*; import mindustry.graphics.*;
import mindustry.logic.*; import mindustry.logic.*;
import mindustry.ui.*; import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.defense.turrets.BaseTurret.*; import mindustry.world.blocks.defense.turrets.BaseTurret.*;
import mindustry.world.blocks.defense.turrets.*; import mindustry.world.blocks.defense.turrets.*;
import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.*;
@ -35,7 +34,7 @@ public class RtsAI{
//in order of priority?? //in order of priority??
static final BlockFlag[] flags = {BlockFlag.generator, BlockFlag.factory, BlockFlag.core, BlockFlag.battery, BlockFlag.drill}; static final BlockFlag[] flags = {BlockFlag.generator, BlockFlag.factory, BlockFlag.core, BlockFlag.battery, BlockFlag.drill};
static final ObjectFloatMap<Building> weights = new ObjectFloatMap<>(); static final ObjectFloatMap<Building> weights = new ObjectFloatMap<>();
static final boolean debug = OS.hasProp("mindustry.debug"); static final boolean debug = OS.hasProp("mindustry.debug") && false;
final Interval timer = new Interval(10); final Interval timer = new Interval(10);
final TeamData data; final TeamData data;
@ -210,12 +209,12 @@ public class RtsAI{
//defendTarget = aggressor; //defendTarget = aggressor;
defendPos = new Vec2(aggressor.x, aggressor.y); defendPos = new Vec2(aggressor.x, aggressor.y);
defendTarget = aggressor; defendTarget = aggressor;
}else if(false){ //TODO currently ignored, no use defending against nothing //}else if(false){ //TODO currently ignored, no use defending against nothing
//should it even go there if there's no aggressor found? //should it even go there if there's no aggressor found?
Tile closest = defend.findClosestEdge(units.first(), Tile::solid); // Tile closest = defend.findClosestEdge(units.first(), Tile::solid);
if(closest != null){ // if(closest != null){
defendPos = new Vec2(closest.worldx(), closest.worldy()); // defendPos = new Vec2(closest.worldx(), closest.worldy());
} // }
}else{ }else{
float mindst = Float.MAX_VALUE; float mindst = Float.MAX_VALUE;
Building build = null; Building build = null;

View file

@ -3,7 +3,6 @@ package mindustry.ai;
import arc.*; import arc.*;
import arc.func.*; import arc.func.*;
import arc.scene.style.*; import arc.scene.style.*;
import arc.struct.*;
import arc.util.*; import arc.util.*;
import mindustry.ai.types.*; import mindustry.ai.types.*;
import mindustry.ctype.*; import mindustry.ctype.*;
@ -13,10 +12,6 @@ import mindustry.input.*;
/** Defines a pattern of behavior that an RTS-controlled unit should follow. Shows up in the command UI. */ /** Defines a pattern of behavior that an RTS-controlled unit should follow. Shows up in the command UI. */
public class UnitCommand extends MappableContent{ public class UnitCommand extends MappableContent{
/** @deprecated now a content type, use the methods in Vars.content instead */
@Deprecated
public static final Seq<UnitCommand> all = new Seq<>();
public static UnitCommand moveCommand, repairCommand, rebuildCommand, assistCommand, mineCommand, boostCommand, enterPayloadCommand, loadUnitsCommand, loadBlocksCommand, unloadPayloadCommand, loopPayloadCommand; public static UnitCommand moveCommand, repairCommand, rebuildCommand, assistCommand, mineCommand, boostCommand, enterPayloadCommand, loadUnitsCommand, loadBlocksCommand, unloadPayloadCommand, loopPayloadCommand;
/** Name of UI icon (from Icon class). */ /** Name of UI icon (from Icon class). */
@ -39,8 +34,6 @@ public class UnitCommand extends MappableContent{
this.icon = icon; this.icon = icon;
this.controller = controller == null ? u -> null : controller; this.controller = controller == null ? u -> null : controller;
all.add(this);
} }
public UnitCommand(String name, String icon, Binding keybind, Func<Unit, AIController> controller){ public UnitCommand(String name, String icon, Binding keybind, Func<Unit, AIController> controller){

View file

@ -2,30 +2,23 @@ package mindustry.ai;
import arc.*; import arc.*;
import arc.scene.style.*; import arc.scene.style.*;
import arc.struct.*;
import arc.util.*; import arc.util.*;
import mindustry.ctype.*; import mindustry.ctype.*;
import mindustry.gen.*; import mindustry.gen.*;
import mindustry.input.*; import mindustry.input.*;
public class UnitStance extends MappableContent{ public class UnitStance extends MappableContent{
/** @deprecated now a content type, use the methods in Vars.content instead */
@Deprecated
public static final Seq<UnitStance> all = new Seq<>();
public static UnitStance stop, shoot, holdFire, pursueTarget, patrol, ram; public static UnitStance stop, shoot, holdFire, pursueTarget, patrol, ram;
/** Name of UI icon (from Icon class). */ /** Name of UI icon (from Icon class). */
public final String icon; public String icon;
/** Key to press for this stance. */ /** Key to press for this stance. */
public @Nullable Binding keybind = null; public @Nullable Binding keybind;
public UnitStance(String name, String icon, Binding keybind){ public UnitStance(String name, String icon, Binding keybind){
super(name); super(name);
this.icon = icon; this.icon = icon;
this.keybind = keybind; this.keybind = keybind;
all.add(this);
} }
public String localized(){ public String localized(){

View file

@ -82,9 +82,7 @@ public class WaveSpawner{
eachFlyerSpawn(group.spawn, (spawnX, spawnY) -> { eachFlyerSpawn(group.spawn, (spawnX, spawnY) -> {
for(int i = 0; i < spawnedf; i++){ for(int i = 0; i < spawnedf; i++){
Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1); spawnUnit(group, spawnX + Mathf.range(spread), spawnY + Mathf.range(spread));
unit.set(spawnX + Mathf.range(spread), spawnY + Mathf.range(spread));
spawnEffect(unit);
} }
}); });
}else{ }else{
@ -95,9 +93,7 @@ public class WaveSpawner{
for(int i = 0; i < spawnedf; i++){ for(int i = 0; i < spawnedf; i++){
Tmp.v1.rnd(spread); Tmp.v1.rnd(spread);
Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1); spawnUnit(group, spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
unit.set(spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
spawnEffect(unit);
} }
}); });
} }
@ -106,6 +102,11 @@ public class WaveSpawner{
Time.run(121f, () -> spawning = false); Time.run(121f, () -> spawning = false);
} }
public void spawnUnit(SpawnGroup group, float x, float y){
group.createUnit(group.team == null ? state.rules.waveTeam : group.team, x, y,
Angles.angle(x, y, world.width()/2f * tilesize, world.height()/2f * tilesize), state.wave - 1, this::spawnEffect);
}
public void doShockwave(float x, float y){ public void doShockwave(float x, float y){
Fx.spawnShockwave.at(x, y, state.rules.dropZoneRadius); Fx.spawnShockwave.at(x, y, state.rules.dropZoneRadius);
Damage.damage(state.rules.waveTeam, x, y, state.rules.dropZoneRadius, 99999999f, true); Damage.damage(state.rules.waveTeam, x, y, state.rules.dropZoneRadius, 99999999f, true);
@ -217,15 +218,8 @@ public class WaveSpawner{
/** Applies the standard wave spawn effects to a unit - invincibility, unmoving. */ /** Applies the standard wave spawn effects to a unit - invincibility, unmoving. */
public void spawnEffect(Unit unit){ public void spawnEffect(Unit unit){
spawnEffect(unit, unit.angleTo(world.width()/2f * tilesize, world.height()/2f * tilesize));
}
/** Applies the standard wave spawn effects to a unit - invincibility, unmoving. */
public void spawnEffect(Unit unit, float rotation){
unit.rotation = rotation;
unit.apply(StatusEffects.unmoving, 30f); unit.apply(StatusEffects.unmoving, 30f);
unit.apply(StatusEffects.invincible, 60f); unit.apply(StatusEffects.invincible, 60f);
unit.add();
unit.unloaded(); unit.unloaded();
Events.fire(new UnitSpawnEvent(unit)); Events.fire(new UnitSpawnEvent(unit));

View file

@ -118,9 +118,10 @@ public class BuilderAI extends AIController{
Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation))); Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation)));
if(valid){ if(valid){
float range = Math.min(unit.type.buildRange - 20f, 100f);
//move toward the plan //move toward the plan
moveTo(req.tile(), unit.type.buildRange - 20f, 20f); moveTo(req.tile(), range - 10f, 20f);
moving = !unit.within(req.tile(), unit.type.buildRange - 10f); moving = !unit.within(req.tile(), range);
}else{ }else{
//discard invalid plan //discard invalid plan
unit.plans.removeFirst(); unit.plans.removeFirst();

View file

@ -201,7 +201,7 @@ public class CommandAI extends AIController{
} }
targetPos.set(attackTarget); targetPos.set(attackTarget);
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.pathType() != Pathfinder.costLegs && stance != UnitStance.ram){ if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.type.pathCostId != ControlPathfinder.costIdLegs && stance != UnitStance.ram){
Tile best = build.findClosestEdge(unit, Tile::solid); Tile best = build.findClosestEdge(unit, Tile::solid);
if(best != null){ if(best != null){
targetPos.set(best); targetPos.set(best);
@ -470,7 +470,7 @@ public class CommandAI extends AIController{
@Override @Override
public boolean retarget(){ public boolean retarget(){
//retarget faster when there is an explicit target //retarget faster when there is an explicit target
return attackTarget != null ? timer.get(timerTarget, 10) : timer.get(timerTarget, 20); return timer.get(timerTarget, attackTarget != null ? 10f : 20f);
} }
public boolean hasCommand(){ public boolean hasCommand(){

View file

@ -28,7 +28,7 @@ public class DefenderAI extends AIController{
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){ public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
//Sort by max health and closer target. //Sort by max health and closer target.
var result = Units.closest(unit.team, x, y, Math.max(range, 400f), u -> !u.dead() && u.type != unit.type && u.targetable(unit.team) && u.type.playerControllable, var result = Units.closest(unit.team, x, y, Math.max(range, 400f), u -> !u.dead() && u.type != unit.type && u.targetable(unit.team) && u.playerControllable(),
(u, tx, ty) -> -u.maxHealth + Mathf.dst2(u.x, u.y, tx, ty) / 6400f); (u, tx, ty) -> -u.maxHealth + Mathf.dst2(u.x, u.y, tx, ty) / 6400f);
if(result != null) return result; if(result != null) return result;

View file

@ -15,7 +15,6 @@ public class HugAI extends AIController{
@Override @Override
public void updateMovement(){ public void updateMovement(){
Building core = unit.closestEnemyCore(); 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.1f + core.block.size * tilesize / 2f)){

View file

@ -1,6 +1,5 @@
package mindustry.ai.types; package mindustry.ai.types;
import mindustry.content.*;
import mindustry.entities.units.*; import mindustry.entities.units.*;
import mindustry.gen.*; import mindustry.gen.*;
import mindustry.type.*; import mindustry.type.*;
@ -17,7 +16,7 @@ public class MinerAI extends AIController{
public void updateMovement(){ public void updateMovement(){
Building core = unit.closestCore(); Building core = unit.closestCore();
if(!(unit.canMine()) || core == null) return; if(!unit.canMine() || core == null) return;
if(!unit.validMine(unit.mineTile)){ if(!unit.validMine(unit.mineTile)){
unit.mineTile(null); unit.mineTile(null);
@ -40,19 +39,17 @@ public class MinerAI extends AIController{
mining = false; mining = false;
}else{ }else{
if(timer.get(timerTarget3, 60) && targetItem != null){ if(timer.get(timerTarget3, 60) && targetItem != null){
ore = indexer.findClosestOre(unit, targetItem); ore = null;
if(unit.type.mineFloor) ore = indexer.findClosestOre(unit, targetItem);
if(ore == null && unit.type.mineWalls) ore = indexer.findClosestWallOre(unit, targetItem);
} }
if(ore != null){ if(ore != null){
moveTo(ore, unit.type.mineRange / 2f, 20f); moveTo(ore, unit.type.mineRange / 2f, 20f);
if(ore.block() == Blocks.air && unit.within(ore, unit.type.mineRange)){ if(unit.within(ore, unit.type.mineRange) && unit.validMine(ore)){
unit.mineTile = ore; unit.mineTile = ore;
} }
if(ore.block() != Blocks.air){
mining = false;
}
} }
} }
}else{ }else{

View file

@ -11,10 +11,11 @@ import mindustry.gen.*;
public class PhysicsProcess implements AsyncProcess{ public class PhysicsProcess implements AsyncProcess{
public static final int public static final int
layers = 3, layers = 4,
layerGround = 0, layerGround = 0,
layerLegs = 1, layerLegs = 1,
layerFlying = 2; layerFlying = 2,
layerUnderwater = 3;
private PhysicsWorld physics; private PhysicsWorld physics;
private Seq<PhysicRef> refs = new Seq<>(false); private Seq<PhysicRef> refs = new Seq<>(false);
@ -153,15 +154,15 @@ public class PhysicsProcess implements AsyncProcess{
for(int i = 0; i < bodySize; i++){ for(int i = 0; i < bodySize; i++){
PhysicsBody body = bodyItems[i]; PhysicsBody body = bodyItems[i];
if(body.layer < 0) continue;
body.collided = false; body.collided = false;
trees[body.layer].insert(body); trees[body.layer].insert(body);
} }
for(int i = 0; i < bodySize; i++){ for(int i = 0; i < bodySize; i++){
PhysicsBody body = bodyItems[i]; PhysicsBody body = bodyItems[i];
//for clients, the only body that collides is the local one; all other physics simulations are handled by the server. //for clients, the only body that collides is the local one; all other physics simulations are handled by the server.
if(!body.local) continue; if(!body.local || body.layer < 0) continue;
body.hitbox(rect); body.hitbox(rect);

View file

@ -292,7 +292,7 @@ public class Logic implements ApplicationListener{
//if there's a "win" wave and no enemies are present, win automatically //if there's a "win" wave and no enemies are present, win automatically
if(state.rules.waves && (state.enemies == 0 && state.rules.winWave > 0 && state.wave >= state.rules.winWave && !spawner.isSpawning()) || if(state.rules.waves && (state.enemies == 0 && state.rules.winWave > 0 && state.wave >= state.rules.winWave && !spawner.isSpawning()) ||
(state.rules.attackMode && state.rules.waveTeam.cores().isEmpty())){ (state.rules.attackMode && !state.rules.waveTeam.isAlive())){
if(state.rules.sector.preset != null && state.rules.sector.preset.attackAfterWaves && !state.rules.attackMode){ if(state.rules.sector.preset != null && state.rules.sector.preset.attackAfterWaves && !state.rules.attackMode){
//activate attack mode to destroy cores after waves are done. //activate attack mode to destroy cores after waves are done.
@ -309,11 +309,11 @@ public class Logic implements ApplicationListener{
Events.fire(new GameOverEvent(state.rules.waveTeam)); Events.fire(new GameOverEvent(state.rules.waveTeam));
}else if(state.rules.attackMode){ }else if(state.rules.attackMode){
//count # of teams alive //count # of teams alive
int countAlive = state.teams.getActive().count(t -> t.hasCore() && t.team != Team.derelict); int countAlive = state.teams.getActive().count(t -> t.isAlive() && t.team != Team.derelict);
if((countAlive <= 1 || (!state.rules.pvp && state.rules.defaultTeam.core() == null)) && !state.gameOver){ if((countAlive <= 1 || (!state.rules.pvp && state.rules.defaultTeam.core() == null)) && !state.gameOver){
//find team that won //find team that won
TeamData left = state.teams.getActive().find(t -> t.hasCore() && t.team != Team.derelict); TeamData left = state.teams.getActive().find(t -> t.isAlive() && t.team != Team.derelict);
Events.fire(new GameOverEvent(left == null ? Team.derelict : left.team)); Events.fire(new GameOverEvent(left == null ? Team.derelict : left.team));
state.gameOver = true; state.gameOver = true;
} }
@ -413,6 +413,9 @@ public class Logic implements ApplicationListener{
@Override @Override
public void update(){ public void update(){
PerfCounter.frame.end();
PerfCounter.frame.begin();
Events.fire(Trigger.update); Events.fire(Trigger.update);
universe.updateGlobal(); universe.updateGlobal();
@ -489,7 +492,9 @@ public class Logic implements ApplicationListener{
state.envAttrs.add(state.rules.attributes); state.envAttrs.add(state.rules.attributes);
Groups.weather.each(w -> state.envAttrs.add(w.weather.attrs, w.opacity)); Groups.weather.each(w -> state.envAttrs.add(w.weather.attrs, w.opacity));
PerfCounter.entityUpdate.begin();
Groups.update(); Groups.update();
PerfCounter.entityUpdate.end();
Events.fire(Trigger.afterGameUpdate); Events.fire(Trigger.afterGameUpdate);
} }

View file

@ -990,6 +990,7 @@ public class NetServer implements ApplicationListener{
//write all entities now //write all entities now
dataStream.writeInt(entity.id()); //write id dataStream.writeInt(entity.id()); //write id
dataStream.writeByte(entity.classId() & 0xFF); //write type ID dataStream.writeByte(entity.classId() & 0xFF); //write type ID
entity.beforeWrite();
entity.writeSync(Writes.get(dataStream)); //write entity entity.writeSync(Writes.get(dataStream)); //write entity
sent++; sent++;

View file

@ -0,0 +1,53 @@
package mindustry.core;
import arc.math.*;
import arc.util.*;
/** Simple per-frame time counter. */
public enum PerfCounter{
frame,
update,
entityUpdate,
render;
public static final PerfCounter[] all = values();
static final int meanWindow = 30;
static final int refreshTimeMillis = 500;
private long valueRefreshTime;
private float refreshValue;
private long beginTime;
private boolean began = false;
private WindowedMean mean = new WindowedMean(meanWindow);
public void begin(){
began = true;
beginTime = Time.nanos();
}
public void end(){
if(!began) return;
began = false;
mean.add(Time.timeSinceNanos(beginTime));
}
/** Value with a periodic refresh interval applied, to prevent jittery UI. */
public float valueMs(){
if(Time.timeSinceMillis(valueRefreshTime) > refreshTimeMillis){
refreshValue = rawValueMs();
valueRefreshTime = Time.millis();
}
return refreshValue;
}
/** Raw value without a refresh interval. This will be unstable. */
public float rawValueMs(){
return mean.rawMean() / Time.nanosPerMilli;
}
public long rawValueNs(){
return (long)mean.rawMean();
}
}

View file

@ -150,6 +150,7 @@ public class Renderer implements ApplicationListener{
@Override @Override
public void update(){ public void update(){
PerfCounter.render.begin();
Color.white.set(1f, 1f, 1f, 1f); Color.white.set(1f, 1f, 1f, 1f);
float baseTarget = targetscale; float baseTarget = targetscale;
@ -220,6 +221,8 @@ public class Renderer implements ApplicationListener{
camera.position.sub(camShakeOffset); camera.position.sub(camShakeOffset);
} }
PerfCounter.render.end();
} }
public void updateAllDarkness(){ public void updateAllDarkness(){
@ -313,7 +316,6 @@ public class Renderer implements ApplicationListener{
Draw.draw(Layer.block - 0.09f, () -> { Draw.draw(Layer.block - 0.09f, () -> {
blocks.floor.beginDraw(); blocks.floor.beginDraw();
blocks.floor.drawLayer(CacheLayer.walls); blocks.floor.drawLayer(CacheLayer.walls);
blocks.floor.endDraw();
}); });
Draw.drawRange(Layer.blockBuilding, () -> Draw.shader(Shaders.blockbuild, true), Draw::shader); Draw.drawRange(Layer.blockBuilding, () -> Draw.shader(Shaders.blockbuild, true), Draw::shader);

View file

@ -123,7 +123,7 @@ public class World{
Tile tile = tiles.get(x, y); Tile tile = tiles.get(x, y);
if(tile == null) return null; if(tile == null) return null;
if(tile.build != null){ if(tile.build != null){
return tile.build.tile(); return tile.build.tile;
} }
return tile; return tile;
} }
@ -458,10 +458,12 @@ public class World{
return 0; return 0;
} }
public void checkMapArea(){ public void checkMapArea(int x, int y, int w, int h){
for(var build : Groups.build){ for(var build : Groups.build){
//reset map-area-based disabled blocks. //if the map area contracts, disable the block
if(!build.enabled && build.block.autoResetEnabled){ build.checkAllowUpdate();
//reset map-area-based disabled blocks that were in the previous map area
if(!build.enabled && build.block.autoResetEnabled && Rect.contains(x, y, w, h, build.tile.x, build.tile.y)){
build.enabled = true; build.enabled = true;
} }
} }

View file

@ -147,8 +147,8 @@ public class EditorTile extends Tile{
if(block.hasBuilding()){ if(block.hasBuilding()){
build = entityprov.get().init(this, team, false, rotation); build = entityprov.get().init(this, team, false, rotation);
if(block.hasItems) build.items = new ItemModule(); if(block.hasItems) build.items = new ItemModule();
if(block.hasLiquids) build.liquids(new LiquidModule()); if(block.hasLiquids) build.liquids = new LiquidModule();
if(block.hasPower) build.power(new PowerModule()); if(block.hasPower) build.power = new PowerModule();
} }
} }

View file

@ -136,6 +136,7 @@ public class MapObjectivesDialog extends BaseDialog{
name(cont, name, remover, indexer); name(cont, name, remover, indexer);
cont.table(t -> t.left().button( cont.table(t -> t.left().button(
b -> b.image(Tex.whiteui).size(iconSmall).update(i -> i.setColor(get.get().color)), b -> b.image(Tex.whiteui).size(iconSmall).update(i -> i.setColor(get.get().color)),
Styles.squarei,
() -> showTeamSelect(set) () -> showTeamSelect(set)
).fill().pad(4f)).growX().fillY(); ).fill().pad(4f)).growX().fillY();
}); });
@ -529,6 +530,8 @@ public class MapObjectivesDialog extends BaseDialog{
public void rebuildObjectives(Seq<MapObjective> objectives){ public void rebuildObjectives(Seq<MapObjective> objectives){
canvas.clearObjectives(); canvas.clearObjectives();
objectives.each(MapObjective::validate);
if( if(
objectives.any() && ( objectives.any() && (
// If the objectives were previously programmatically made... // If the objectives were previously programmatically made...
@ -592,9 +595,23 @@ public class MapObjectivesDialog extends BaseDialog{
} }
public static void showTeamSelect(Cons<Team> cons){ public static void showTeamSelect(Cons<Team> cons){
showTeamSelect(false, cons);
}
public static void showTeamSelect(boolean allowNull, Cons<Team> cons){
BaseDialog dialog = new BaseDialog(""); BaseDialog dialog = new BaseDialog("");
dialog.cont.defaults().size(40f).pad(4f);
if(allowNull){
dialog.cont.button(Icon.cancel, Styles.emptyi, () -> {
cons.get(null);
dialog.hide();
}).tooltip("@none");
}
for(var team : Team.baseTeams){ for(var team : Team.baseTeams){
dialog.cont.image(Tex.whiteui).size(iconMed).color(team.color).pad(4) dialog.cont.image(Tex.whiteui).color(team.color)
.with(i -> i.addListener(new HandCursorListener())) .with(i -> i.addListener(new HandCursorListener()))
.tooltip(team.localized()).get().clicked(() -> { .tooltip(team.localized()).get().clicked(() -> {
cons.get(team); cons.get(team);

View file

@ -288,6 +288,13 @@ public class WaveInfoDialog extends BaseDialog{
buildGroups(); buildGroups();
}).padTop(4).update(b -> b.setChecked(group.effect == StatusEffects.boss)).padBottom(8f).row(); }).padTop(4).update(b -> b.setChecked(group.effect == StatusEffects.boss)).padBottom(8f).row();
t.table(a -> {
a.add("@waves.team").padRight(8);
a.button(b -> b.image(Tex.whiteui).size(iconSmall).update(i -> i.setColor(group.team == null ? Color.clear : group.team.color)), Styles.squarei,
() -> MapObjectivesDialog.showTeamSelect(true, team -> group.team = team)).size(38f);
}).padTop(0).row();
t.table(a -> { t.table(a -> {
a.add("@waves.spawn").padRight(8); a.add("@waves.spawn").padRight(8);

View file

@ -70,23 +70,27 @@ public class Damage{
} }
} }
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage){ public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null, Fx.dynamicExplosion); dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null, Fx.dynamicExplosion);
} }
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, Effect explosionFx){ public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, Effect explosionFx){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null, explosionFx); dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null, explosionFx);
} }
/** Creates a dynamic explosion based on specified parameters. */ public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, Effect explosionFx, float baseShake){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, true, null, explosionFx, baseShake);
}
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, boolean fire, @Nullable Team ignoreTeam){ public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, boolean fire, @Nullable Team ignoreTeam){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, fire, ignoreTeam, Fx.dynamicExplosion); dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, fire, ignoreTeam, Fx.dynamicExplosion);
} }
/** Creates a dynamic explosion based on specified parameters. */
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, boolean fire, @Nullable Team ignoreTeam, Effect explosionFx){ public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, boolean fire, @Nullable Team ignoreTeam, Effect explosionFx){
dynamicExplosion(x, y, flammability, explosiveness, power, radius, damage, fire, ignoreTeam, explosionFx, 3f);
}
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, boolean damage, boolean fire, @Nullable Team ignoreTeam, Effect explosionFx, float baseShake){
if(damage){ if(damage){
for(int i = 0; i < Mathf.clamp(power / 700, 0, 8); i++){ for(int i = 0; i < Mathf.clamp(power / 700, 0, 8); i++){
int length = 5 + Mathf.clamp((int)(Mathf.pow(power, 0.98f) / 500), 1, 18); int length = 5 + Mathf.clamp((int)(Mathf.pow(power, 0.98f) / 500), 1, 18);
@ -123,7 +127,7 @@ public class Damage{
Fx.bigShockwave.at(x, y); Fx.bigShockwave.at(x, y);
} }
float shake = Math.min(explosiveness / 4f + 3f, 9f); float shake = Math.min(explosiveness / 4f + baseShake, 9f);
Effect.shake(shake, shake, x, y); Effect.shake(shake, shake, x, y);
explosionFx.at(x, y, radius / 8f); explosionFx.at(x, y, radius / 8f);
} }
@ -549,7 +553,12 @@ public class Damage{
//this needs to be compensated //this needs to be compensated
if(in != null && in.team != team && in.block.size > 1 && in.health > damage){ 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 //deal the damage of an entire side, to be equivalent with maximum 'standard' damage
in.damage(team, damage * Math.min((in.block.size), baseRadius * 0.4f)); float d = damage * Math.min((in.block.size), baseRadius * 0.4f);
if(source != null){
in.damage(source, team, d);
}else{
in.damage(team, d);
}
//no need to continue with the explosion //no need to continue with the explosion
return; return;
} }

View file

@ -166,7 +166,7 @@ public class EntityCollisions{
} }
} }
private boolean collide(float x1, float y1, float w1, float h1, float vx1, float vy1, public static boolean collide(float x1, float y1, float w1, float h1, float vx1, float vy1,
float x2, float y2, float w2, float h2, float vx2, float vy2, Vec2 out){ float x2, float y2, float w2, float h2, float vx2, float vy2, Vec2 out){
float px = vx1, py = vy1; float px = vx1, py = vy1;

View file

@ -17,7 +17,7 @@ import static mindustry.Vars.*;
public class Lightning{ public class Lightning{
private static final Rand random = new Rand(); private static final Rand random = new Rand();
private static final Rect rect = new Rect(); private static final Rect rect = new Rect();
private static final Seq<Unitc> entities = new Seq<>(); private static final Seq<Unit> entities = new Seq<>();
private static final IntSet hit = new IntSet(); private static final IntSet hit = new IntSet();
private static final int maxChain = 8; private static final int maxChain = 8;
private static final float hitRange = 30f; private static final float hitRange = 30f;
@ -74,7 +74,7 @@ public class Lightning{
}); });
} }
Unitc furthest = Geometry.findFurthest(x, y, entities); Unit furthest = Geometry.findFurthest(x, y, entities);
if(furthest != null){ if(furthest != null){
hit.add(furthest.id()); hit.add(furthest.id());

View file

@ -97,6 +97,12 @@ public class Puddles{
} }
} }
public static boolean hasLiquid(Tile tile, Liquid liquid){
if(tile == null) return false;
var p = get(tile);
return p != null && p.liquid == liquid && p.amount >= 0.5f;
}
public static void remove(Tile tile){ public static void remove(Tile tile){
if(tile == null) return; if(tile == null) return;
@ -126,7 +132,7 @@ public class Puddles{
if(Mathf.chance(0.8f * amount)){ if(Mathf.chance(0.8f * amount)){
Fx.steam.at(x, y); Fx.steam.at(x, y);
} }
return -0.4f * amount; return -0.7f * amount;
} }
return dest.react(liquid, amount, tile, x, y); return dest.react(liquid, amount, tile, x, y);
} }

View file

@ -4,7 +4,9 @@ package mindustry.entities;
public class TargetPriority{ public class TargetPriority{
public static final float public static final float
//nobody cares about walls //nobody cares about walls
wall = -2f, wall = -3f,
//anything that has underBullets gets this priority (it's probably still more important than a wall)
under = -2f,
//transport infrastructure isn't as important as factories //transport infrastructure isn't as important as factories
transport = -1f, transport = -1f,
//most blocks //most blocks

View file

@ -1,6 +1,7 @@
package mindustry.entities; package mindustry.entities;
import arc.math.*; import arc.math.*;
import mindustry.content.*;
import mindustry.entities.Units.*; import mindustry.entities.Units.*;
import mindustry.gen.*; import mindustry.gen.*;
@ -11,4 +12,9 @@ public class UnitSorts{
farthest = (u, x, y) -> -u.dst2(x, y), farthest = (u, x, y) -> -u.dst2(x, y),
strongest = (u, x, y) -> -u.maxHealth + Mathf.dst2(u.x, u.y, x, y) / 6400f, strongest = (u, x, y) -> -u.maxHealth + Mathf.dst2(u.x, u.y, x, y) / 6400f,
weakest = (u, x, y) -> u.maxHealth + Mathf.dst2(u.x, u.y, x, y) / 6400f; weakest = (u, x, y) -> u.maxHealth + Mathf.dst2(u.x, u.y, x, y) / 6400f;
public static BuildingPriorityf
buildingDefault = b -> b.block.priority,
buildingWater = b -> b.block.priority + (b.liquids != null && b.liquids.get(Liquids.water) > 5f ? 10f : 0f);
} }

View file

@ -95,7 +95,7 @@ public class Units{
public static int getCap(Team team){ public static int getCap(Team team){
//wave team has no cap //wave team has no cap
if((team == state.rules.waveTeam && !state.rules.pvp) || (state.isCampaign() && team == state.rules.waveTeam) || state.rules.disableUnitCap){ if((team == state.rules.waveTeam && !state.rules.pvp) || (state.isCampaign() && team == state.rules.waveTeam) || state.rules.disableUnitCap || team.ignoreUnitCap){
return Integer.MAX_VALUE; return Integer.MAX_VALUE;
} }
return Math.max(0, state.rules.unitCapVariable ? state.rules.unitCap + team.data().unitCap : state.rules.unitCap); return Math.max(0, state.rules.unitCapVariable ? state.rules.unitCap + team.data().unitCap : state.rules.unitCap);
@ -197,18 +197,8 @@ public class Units{
/** Returns the nearest 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<Building> pred){ public static Building findEnemyTile(Team team, float x, float y, float range, Boolf<Building> pred){
return findEnemyTile(team, x, y, range, false, pred);
}
/** Returns the nearest enemy tile in a range. */
public static Building findEnemyTile(Team team, float x, float y, float range, boolean checkUnder, Boolf<Building> pred){
if(team == Team.derelict) return null; if(team == Team.derelict) return null;
if(checkUnder){
Building target = indexer.findEnemyTile(team, x, y, range, build -> !build.block.underBullets && pred.get(build));
if(target != null) return target;
}
return indexer.findEnemyTile(team, x, y, range, pred); return indexer.findEnemyTile(team, x, y, range, pred);
} }
@ -258,7 +248,7 @@ public class Units{
if(unit != null){ if(unit != null){
return unit; return unit;
}else{ }else{
return findEnemyTile(team, x, y, range, true, tilePred); return findEnemyTile(team, x, y, range, tilePred);
} }
} }
@ -270,7 +260,7 @@ public class Units{
if(unit != null){ if(unit != null){
return unit; return unit;
}else{ }else{
return findEnemyTile(team, x, y, range, true, tilePred); return findEnemyTile(team, x, y, range, tilePred);
} }
} }
@ -409,7 +399,7 @@ public class Units{
/** @return whether any units exist in this rectangle */ /** @return whether any units exist in this rectangle */
public static boolean any(float x, float y, float width, float height, Boolf<Unit> filter){ public static boolean any(float x, float y, float width, float height, Boolf<Unit> filter){
return count(x, y, width, height, filter) > 0; return Groups.unit.intersect(x, y, width, height, filter);
} }
/** Iterates over all units in a rectangle. */ /** Iterates over all units in a rectangle. */
@ -494,4 +484,8 @@ public class Units{
public interface Sortf{ public interface Sortf{
float cost(Unit unit, float x, float y); float cost(Unit unit, float x, float y);
} }
public interface BuildingPriorityf{
float priority(Building build);
}
} }

View file

@ -4,6 +4,7 @@ import arc.*;
import arc.scene.ui.layout.*; import arc.scene.ui.layout.*;
import mindustry.gen.*; import mindustry.gen.*;
import mindustry.type.*; import mindustry.type.*;
import mindustry.ui.*;
public abstract class Ability implements Cloneable{ public abstract class Ability implements Cloneable{
protected static final float descriptionWidth = 350f; protected static final float descriptionWidth = 350f;
@ -13,11 +14,26 @@ public abstract class Ability implements Cloneable{
public float data; public float data;
public void update(Unit unit){} public void update(Unit unit){}
public void draw(Unit unit){} public void draw(Unit unit){}
public void death(Unit unit){} public void death(Unit unit){}
public void created(Unit unit){} public void created(Unit unit){}
public void init(UnitType type){} public void init(UnitType type){}
public void displayBars(Unit unit, Table bars){} public void displayBars(Unit unit, Table bars){}
public void display(Table t){
t.table(Styles.grayPanel, a -> {
a.add("[accent]" + localized()).padBottom(4).center().top().expandX();
a.row();
a.left().top().defaults().left();
addStats(a);
}).pad(5).margin(10).growX().top().uniformX();
}
public void addStats(Table t){ public void addStats(Table t){
if(Core.bundle.has(getBundle() + ".description")){ if(Core.bundle.has(getBundle() + ".description")){
t.add(Core.bundle.get(getBundle() + ".description")).wrap().width(descriptionWidth); t.add(Core.bundle.get(getBundle() + ".description")).wrap().width(descriptionWidth);

View file

@ -13,8 +13,8 @@ import static mindustry.Vars.*;
public class LiquidRegenAbility extends Ability{ public class LiquidRegenAbility extends Ability{
public Liquid liquid; public Liquid liquid;
public float slurpSpeed = 9f; public float slurpSpeed = 5f;
public float regenPerSlurp = 2.9f; public float regenPerSlurp = 6f;
public float slurpEffectChance = 0.4f; public float slurpEffectChance = 0.4f;
public Effect slurpEffect = Fx.heal; public Effect slurpEffect = Fx.heal;
@ -31,7 +31,7 @@ public class LiquidRegenAbility extends Ability{
//TODO timer? //TODO timer?
//TODO effects? //TODO effects?
if(unit.damaged()){ if(unit.damaged() && !unit.isFlying()){
boolean healed = false; boolean healed = false;
int tx = unit.tileX(), ty = unit.tileY(); int tx = unit.tileX(), ty = unit.tileY();
int rad = Math.max((int)(unit.hitSize / tilesize * 0.6f), 1); int rad = Math.max((int)(unit.hitSize / tilesize * 0.6f), 1);

View file

@ -1,6 +1,7 @@
package mindustry.entities.abilities; package mindustry.entities.abilities;
import arc.graphics.*; import arc.graphics.*;
import arc.math.*;
import arc.util.*; import arc.util.*;
import mindustry.*; import mindustry.*;
import mindustry.content.*; import mindustry.content.*;
@ -9,8 +10,9 @@ import mindustry.gen.*;
public class MoveEffectAbility extends Ability{ public class MoveEffectAbility extends Ability{
public float minVelocity = 0.08f; public float minVelocity = 0.08f;
public float interval = 3f; public float interval = 3f, chance = 0f;
public float x, y, rotation; public int amount = 1;
public float x, y, rotation, rangeX, rangeY, rangeLengthMin, rangeLengthMax;
public boolean rotateEffect = false; public boolean rotateEffect = false;
public float effectParam = 3f; public float effectParam = 3f;
public boolean teamColor = false; public boolean teamColor = false;
@ -38,10 +40,17 @@ public class MoveEffectAbility extends Ability{
if(Vars.headless) return; if(Vars.headless) return;
counter += Time.delta; counter += Time.delta;
if(unit.vel.len2() >= minVelocity * minVelocity && (counter >= interval) && !unit.inFogTo(Vars.player.team())){ if(unit.vel.len2() >= minVelocity * minVelocity && (counter >= interval || (chance > 0 && Mathf.chanceDelta(chance))) && !unit.inFogTo(Vars.player.team())){
Tmp.v1.trns(unit.rotation - 90f, x, y); if(rangeLengthMax > 0){
Tmp.v1.trns(unit.rotation - 90f, x, y).add(Tmp.v2.rnd(Mathf.random(rangeLengthMin, rangeLengthMax)));
}else{
Tmp.v1.trns(unit.rotation - 90f, x + Mathf.range(rangeX), y + Mathf.range(rangeY));
}
counter %= interval; counter %= interval;
effect.at(Tmp.v1.x + unit.x, Tmp.v1.y + unit.y, (rotateEffect ? unit.rotation : effectParam) + rotation, teamColor ? unit.team.color : color, parentizeEffects ? unit : null); for(int i = 0; i < amount; i++){
effect.at(Tmp.v1.x + unit.x, Tmp.v1.y + unit.y, (rotateEffect ? unit.rotation : effectParam) + rotation, teamColor ? unit.team.color : color, parentizeEffects ? unit : null);
}
} }
} }
} }

View file

@ -30,16 +30,24 @@ public class BulletType extends Content implements Cloneable{
/** Lifetime in ticks. */ /** Lifetime in ticks. */
public float lifetime = 40f; public float lifetime = 40f;
/** Min/max multipliers for lifetime applied to this bullet when spawned. */
public float lifeScaleRandMin = 1f, lifeScaleRandMax = 1f;
/** Speed in units/tick. */ /** Speed in units/tick. */
public float speed = 1f; public float speed = 1f;
/** Min/max multipliers for velocity applied to this bullet when spawned. */
public float velocityScaleRandMin = 1f, velocityScaleRandMax = 1f;
/** Direct damage dealt on hit. */ /** Direct damage dealt on hit. */
public float damage = 1f; public float damage = 1f;
/** Hitbox size. */ /** Hitbox size. */
public float hitSize = 4; public float hitSize = 4;
/** Clipping hitbox. */ /** Clipping hitbox. */
public float drawSize = 40f; public float drawSize = 40f;
/** Angle offset applied to bullet when spawned each time. */
public float angleOffset = 0f, randomAngleOffset = 0f;
/** Drag as fraction of velocity. */ /** Drag as fraction of velocity. */
public float drag = 0f; public float drag = 0f;
/** Acceleration per frame. */
public float accel = 0f;
/** Whether to pierce units. */ /** Whether to pierce units. */
public boolean pierce; public boolean pierce;
/** Whether to pierce buildings. */ /** Whether to pierce buildings. */
@ -155,6 +163,8 @@ public class BulletType extends Content implements Cloneable{
public float healAmount = 0f; public float healAmount = 0f;
/** Whether to make fire on impact */ /** Whether to make fire on impact */
public boolean makeFire = false; public boolean makeFire = false;
/** Whether this bullet will always hit blocks under it. */
public boolean hitUnder = false;
/** Whether to create hit effects on despawn. Forced to true if this bullet has any special effects like splash damage. */ /** Whether to create hit effects on despawn. Forced to true if this bullet has any special effects like splash damage. */
public boolean despawnHit = false; public boolean despawnHit = false;
/** If true, this bullet will create bullets when it hits anything, not just when it despawns. */ /** If true, this bullet will create bullets when it hits anything, not just when it despawns. */
@ -163,6 +173,10 @@ public class BulletType extends Content implements Cloneable{
public boolean fragOnAbsorb = true; public boolean fragOnAbsorb = true;
/** If true, unit armor is ignored in damage calculations. */ /** If true, unit armor is ignored in damage calculations. */
public boolean pierceArmor = false; public boolean pierceArmor = false;
/** If true, the bullet will "stick" to enemies and get deactivated on collision. */
public boolean sticky = false;
/** Extra time added to bullet when it sticks to something. */
public float stickyExtraLifetime = 0f;
/** Whether status and despawnHit should automatically be set. */ /** Whether status and despawnHit should automatically be set. */
public boolean setDefaults = true; public boolean setDefaults = true;
/** Amount of shaking produced when this bullet hits something or despawns. */ /** Amount of shaking produced when this bullet hits something or despawns. */
@ -195,7 +209,7 @@ public class BulletType extends Content implements Cloneable{
public float bulletInterval = 20f; public float bulletInterval = 20f;
/** Number of bullet spawned per interval. */ /** Number of bullet spawned per interval. */
public int intervalBullets = 1; public int intervalBullets = 1;
/** Random spread of interval bullets. */ /** Random angle added to interval bullets. */
public float intervalRandomSpread = 360f; public float intervalRandomSpread = 360f;
/** Angle spread between individual interval bullets. */ /** Angle spread between individual interval bullets. */
public float intervalSpread = 0f; public float intervalSpread = 0f;
@ -204,6 +218,9 @@ public class BulletType extends Content implements Cloneable{
/** Use a negative value to disable interval bullet delay. */ /** Use a negative value to disable interval bullet delay. */
public float intervalDelay = -1f; public float intervalDelay = -1f;
/** If true, this bullet is rendered underwater. Highly experimental! */
public boolean underwater = false;
/** Color used for hit/despawn effects. */ /** Color used for hit/despawn effects. */
public Color hitColor = Color.white; public Color hitColor = Color.white;
/** Color used for block heal effects. */ /** Color used for block heal effects. */
@ -212,6 +229,8 @@ public class BulletType extends Content implements Cloneable{
public Effect healEffect = Fx.healBlockFull; public Effect healEffect = Fx.healBlockFull;
/** Bullets spawned when this bullet is created. Rarely necessary, used for visuals. */ /** Bullets spawned when this bullet is created. Rarely necessary, used for visuals. */
public Seq<BulletType> spawnBullets = new Seq<>(); public Seq<BulletType> spawnBullets = new Seq<>();
/** Random angle spread of spawn bullets. */
public float spawnBulletRandomSpread = 0f;
/** Unit spawned _instead of_ this bullet. Useful for missiles. */ /** Unit spawned _instead of_ this bullet. Useful for missiles. */
public @Nullable UnitType spawnUnit; public @Nullable UnitType spawnUnit;
/** Unit spawned when this bullet hits something or despawns due to it hitting the end of its lifetime. */ /** Unit spawned when this bullet hits something or despawns due to it hitting the end of its lifetime. */
@ -233,8 +252,12 @@ public class BulletType extends Content implements Cloneable{
public float trailChance = -0.0001f; public float trailChance = -0.0001f;
/** Uniform interval in which trail effect is spawned. */ /** Uniform interval in which trail effect is spawned. */
public float trailInterval = 0f; public float trailInterval = 0f;
/** Min velocity required for trail effect to spawn. */
public float trailMinVelocity = 0f;
/** Trail effect that is spawned. */ /** Trail effect that is spawned. */
public Effect trailEffect = Fx.missileTrail; public Effect trailEffect = Fx.missileTrail;
/** Random offset of trail effect. */
public float trailSpread = 0f;
/** Rotation/size parameter that is passed to trail. Usually, this controls size. */ /** Rotation/size parameter that is passed to trail. Usually, this controls size. */
public float trailParam = 2f; public float trailParam = 2f;
/** Whether the parameter passed to the trail is the bullet rotation, instead of a flat value. */ /** Whether the parameter passed to the trail is the bullet rotation, instead of a flat value. */
@ -247,6 +270,14 @@ public class BulletType extends Content implements Cloneable{
public float trailWidth = 2f; public float trailWidth = 2f;
/** If trailSinMag > 0, these values are applied as a sine curve to trail width. */ /** If trailSinMag > 0, these values are applied as a sine curve to trail width. */
public float trailSinMag = 0f, trailSinScl = 3f; public float trailSinMag = 0f, trailSinScl = 3f;
/** If true, the bullet will attempt to circle around its shooting entity. */
public boolean circleShooter = false;
/** Radius that the bullet attempts to circle at. */
public float circleShooterRadius = 13f;
/** Smooth extra radius value for circling. */
public float circleShooterRadiusSmooth = 10f;
/** Multiplier of speed that is used to adjust velocity when circling. */
public float circleShooterRotateSpeed = 0.3f;
/** Use a negative value to disable splash damage. */ /** Use a negative value to disable splash damage. */
public float splashDamageRadius = -1f; public float splashDamageRadius = -1f;
@ -299,6 +330,8 @@ public class BulletType extends Content implements Cloneable{
public float weaveMag = 0f; public float weaveMag = 0f;
/** If true, the bullet weave will randomly switch directions on spawn. */ /** If true, the bullet weave will randomly switch directions on spawn. */
public boolean weaveRandom = true; public boolean weaveRandom = true;
/** Rotation speed of the bullet velocity as it travels. */
public float rotateSpeed = 0f;
/** Number of individual puddles created. */ /** Number of individual puddles created. */
public int puddles; public int puddles;
@ -624,7 +657,7 @@ public class BulletType extends Content implements Cloneable{
if(spawnBullets.size > 0){ if(spawnBullets.size > 0){
for(var bullet : spawnBullets){ for(var bullet : spawnBullets){
bullet.create(b, b.x, b.y, b.rotation()); bullet.create(b, b.x, b.y, b.rotation() + Mathf.range(spawnBulletRandomSpread));
} }
} }
} }
@ -678,18 +711,40 @@ public class BulletType extends Content implements Cloneable{
if(weaveMag != 0){ if(weaveMag != 0){
b.vel.rotateRadExact((float)Math.sin((b.time + Math.PI * weaveScale/2f) / weaveScale) * weaveMag * (weaveRandom ? (Mathf.randomSeed(b.id, 0, 1) == 1 ? -1 : 1) : 1f) * Time.delta * Mathf.degRad); b.vel.rotateRadExact((float)Math.sin((b.time + Math.PI * weaveScale/2f) / weaveScale) * weaveMag * (weaveRandom ? (Mathf.randomSeed(b.id, 0, 1) == 1 ? -1 : 1) : 1f) * Time.delta * Mathf.degRad);
} }
if(rotateSpeed != 0){
b.vel.rotate(rotateSpeed * Time.delta);
}
if(circleShooter && b.owner instanceof Healthc h && h.isValid()){
Tmp.v1.set(h).sub(b);
Tmp.v1.rotate(90f * Mathf.lerp(0f, 1f, 1f - Mathf.clamp((Tmp.v1.len() - circleShooterRadius) / circleShooterRadiusSmooth)));
b.vel.add(Tmp.v1.limit(speed * circleShooterRotateSpeed * Time.delta)).limit(speed);
}
} }
public void updateTrailEffects(Bullet b){ public void updateTrailEffects(Bullet b){
if(trailChance > 0){ boolean canSpawn = trailMinVelocity <= 0f || b.vel.len2() >= trailMinVelocity * trailMinVelocity;
if(trailChance > 0 && canSpawn){
if(Mathf.chanceDelta(trailChance)){ if(Mathf.chanceDelta(trailChance)){
trailEffect.at(b.x, b.y, trailRotation ? b.rotation() : trailParam, trailColor); if(trailSpread > 0){
Tmp.v1.rnd(Mathf.random(trailSpread));
}else{
Tmp.v1.setZero();
}
trailEffect.at(b.x + Tmp.v1.x, b.y + Tmp.v1.y, trailRotation ? b.rotation() : trailParam, trailColor);
} }
} }
if(trailInterval > 0f){ if(trailInterval > 0f && canSpawn){
if(b.timer(0, trailInterval)){ if(b.timer(0, trailInterval)){
trailEffect.at(b.x, b.y, trailRotation ? b.rotation() : trailParam, trailColor); if(trailSpread > 0){
Tmp.v1.rnd(Mathf.random(trailSpread));
}else{
Tmp.v1.setZero();
}
trailEffect.at(b.x + Tmp.v1.x, b.y + Tmp.v1.y, trailRotation ? b.rotation() : trailParam, trailColor);
} }
} }
} }
@ -800,6 +855,8 @@ public class BulletType extends Content implements Cloneable{
@Nullable Entityc owner, @Nullable Entityc shooter, Team team, float x, float y, float angle, float damage, float velocityScl, @Nullable Entityc owner, @Nullable Entityc shooter, Team team, float x, float y, float angle, float damage, float velocityScl,
float lifetimeScl, Object data, @Nullable Mover mover, float aimX, float aimY, @Nullable Teamc target float lifetimeScl, Object data, @Nullable Mover mover, float aimX, float aimY, @Nullable Teamc target
){ ){
angle += angleOffset + Mathf.range(randomAngleOffset);
if(!Mathf.chance(createChance)) return null; if(!Mathf.chance(createChance)) return null;
if(ignoreSpawnAngle) angle = 0; if(ignoreSpawnAngle) angle = 0;
if(spawnUnit != null){ if(spawnUnit != null){
@ -846,13 +903,13 @@ public class BulletType extends Content implements Cloneable{
bullet.aimX = aimX; bullet.aimX = aimX;
bullet.aimY = aimY; bullet.aimY = aimY;
bullet.initVel(angle, speed * velocityScl); bullet.initVel(angle, speed * velocityScl * (velocityScaleRandMin != 1f || velocityScaleRandMax != 1f ? Mathf.random(velocityScaleRandMin, velocityScaleRandMax) : 1f));
if(backMove){ if(backMove){
bullet.set(x - bullet.vel.x * Time.delta, y - bullet.vel.y * Time.delta); bullet.set(x - bullet.vel.x * Time.delta, y - bullet.vel.y * Time.delta);
}else{ }else{
bullet.set(x, y); bullet.set(x, y);
} }
bullet.lifetime = lifetime * lifetimeScl; bullet.lifetime = lifetime * lifetimeScl * (lifeScaleRandMin != 1f || lifeScaleRandMax != 1f ? Mathf.random(lifeScaleRandMin, lifeScaleRandMax) : 1f);
bullet.data = data; bullet.data = data;
bullet.drag = drag; bullet.drag = drag;
bullet.hitSize = hitSize; bullet.hitSize = hitSize;

View file

@ -0,0 +1,52 @@
package mindustry.entities.bullet;
import arc.util.*;
import mindustry.entities.*;
import mindustry.gen.*;
public class InterceptorBulletType extends BasicBulletType{
public InterceptorBulletType(float speed, float damage){
super(speed, damage);
}
public InterceptorBulletType(){
}
public InterceptorBulletType(float speed, float damage, String bulletSprite){
super(speed, damage, bulletSprite);
}
@Override
public void update(Bullet b){
super.update(b);
if(b.data instanceof Bullet other){
if(other.isAdded()){
//check for an overlap between the two bullet trajectories; it is the responsibility of the creator to make sure the bullet is a valid target
if(EntityCollisions.collide(
b.x, b.y,
b.hitSize, b.hitSize,
b.deltaX, b.deltaY,
other.x, other.y,
other.hitSize, other.hitSize,
other.deltaX, other.deltaY, Tmp.v1)){
b.set(Tmp.v1);
hit(b, b.x, b.y);
b.remove();
if(other.damage > damage){
other.damage -= b.damage;
}else{
other.remove();
}
}
}else{
b.data = null;
}
}
}
}

View file

@ -0,0 +1,61 @@
package mindustry.entities.bullet;
import arc.util.*;
import mindustry.entities.*;
import mindustry.game.*;
import mindustry.gen.*;
/** A fake bullet type that spawns multiple sub-bullets when "fired". */
public class MultiBulletType extends BulletType{
public BulletType[] bullets = {};
/** Amount of times the bullet array is repeated. */
public int repeat = 1;
public MultiBulletType(BulletType... bullets){
this.bullets = bullets;
}
public MultiBulletType(int repeat, BulletType... bullets){
this.repeat = repeat;
this.bullets = bullets;
}
public MultiBulletType(){
}
@Override
public float estimateDPS(){
float sum = 0f;
for(var b : bullets){
sum += b.estimateDPS();
}
return sum;
}
@Override
protected float calculateRange(){
float max = 0f;
for(var b : bullets){
max = Math.max(max, b.calculateRange());
}
return max;
}
@Override
public @Nullable Bullet create(
@Nullable Entityc owner, @Nullable Entityc shooter, Team team, float x, float y, float angle, float damage, float velocityScl,
float lifetimeScl, Object data, @Nullable Mover mover, float aimX, float aimY, @Nullable Teamc target
){
angle += angleOffset;
Bullet last = null;
for(int i = 0; i < repeat; i++){
for(var bullet : bullets){
last = bullet.create(owner, shooter, team, x, y, angle, damage, velocityScl, lifetimeScl, data, mover, aimX, aimY, target);
}
}
return last;
}
}

View file

@ -3,6 +3,7 @@ package mindustry.entities.bullet;
import arc.*; import arc.*;
import arc.graphics.*; import arc.graphics.*;
import arc.graphics.g2d.*; import arc.graphics.g2d.*;
import arc.math.*;
import arc.math.geom.*; import arc.math.geom.*;
import arc.util.*; import arc.util.*;
import mindustry.content.*; import mindustry.content.*;
@ -11,7 +12,7 @@ import mindustry.gen.*;
import mindustry.graphics.*; import mindustry.graphics.*;
public class SapBulletType extends BulletType{ public class SapBulletType extends BulletType{
public float length = 100f; public float length = 100f, lengthRand = 0f;
public float sapStrength = 0.5f; public float sapStrength = 0.5f;
public Color color = Color.white.cpy(); public Color color = Color.white.cpy();
public float width = 0.4f; public float width = 0.4f;
@ -72,7 +73,9 @@ public class SapBulletType extends BulletType{
public void init(Bullet b){ public void init(Bullet b){
super.init(b); super.init(b);
Healthc target = Damage.linecast(b, b.x, b.y, b.rotation(), length); float len = Mathf.random(length, length + lengthRand);
Healthc target = Damage.linecast(b, b.x, b.y, b.rotation(), len);
b.data = target; b.data = target;
if(target != null){ if(target != null){
@ -92,7 +95,7 @@ public class SapBulletType extends BulletType{
hit(b, tile.x, tile.y); hit(b, tile.x, tile.y);
} }
}else{ }else{
b.data = new Vec2().trns(b.rotation(), length).add(b.x, b.y); b.data = new Vec2().trns(b.rotation(), len).add(b.x, b.y);
} }
} }
} }

View file

@ -63,6 +63,11 @@ abstract class BlockUnitComp implements Unitc{
return tile != null && tile.isValid(); return tile != null && tile.isValid();
} }
@Replace
public boolean isAdded(){
return tile != null && tile.isValid();
}
@Replace @Replace
public void team(Team team){ public void team(Team team){
if(tile != null && this.team != team){ if(tile != null && this.team != team){

View file

@ -1,58 +0,0 @@
package mindustry.entities.comp;
import arc.math.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.type.*;
import static mindustry.Vars.*;
@Component
abstract class BoundedComp implements Velc, Posc, Healthc, Flyingc{
static final float warpDst = 30f;
@Import UnitType type;
@Import float x, y;
@Import Team team;
@Override
public void update(){
if(!type.bounded) return;
float bot = 0f, left = 0f, top = world.unitHeight(), right = world.unitWidth();
//TODO hidden map rules only apply to player teams? should they?
if(state.rules.limitMapArea && !team.isAI()){
bot = state.rules.limitY * tilesize;
left = state.rules.limitX * tilesize;
top = state.rules.limitHeight * tilesize + bot;
right = state.rules.limitWidth * tilesize + left;
}
if(!net.client() || isLocal()){
float dx = 0f, dy = 0f;
//repel unit out of bounds
if(x < left) dx += (-(x - left)/warpDst);
if(y < bot) dy += (-(y - bot)/warpDst);
if(x > right) dx -= (x - right)/warpDst;
if(y > top) dy -= (y - top)/warpDst;
velAddNet(dx * Time.delta, dy * Time.delta);
}
//clamp position if not flying
if(isGrounded()){
x = Mathf.clamp(x, left, right - tilesize);
y = Mathf.clamp(y, bot, top - tilesize);
}
//kill when out of bounds
if(x < -finalWorldBounds + left || y < -finalWorldBounds + bot || x >= right + finalWorldBounds || y >= top + finalWorldBounds){
kill();
}
}
}

View file

@ -136,7 +136,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{
} }
if(!(tile.build instanceof ConstructBuild cb)){ if(!(tile.build instanceof ConstructBuild cb)){
if(!current.initialized && !current.breaking && Build.validPlaceIgnoreUnits(current.block, team, current.x, current.y, current.rotation, true)){ if(!current.initialized && !current.breaking && Build.validPlaceIgnoreUnits(current.block, team, current.x, current.y, current.rotation, true, true)){
if(Build.checkNoUnitOverlap(current.block, current.x, current.y)){ if(Build.checkNoUnitOverlap(current.block, current.x, current.y)){
boolean hasAll = infinite || current.isRotation(team) || boolean hasAll = infinite || current.isRotation(team) ||
//derelict repair //derelict repair

View file

@ -16,7 +16,6 @@ import arc.util.*;
import arc.util.io.*; import arc.util.io.*;
import mindustry.*; import mindustry.*;
import mindustry.annotations.Annotations.*; import mindustry.annotations.Annotations.*;
import mindustry.audio.*;
import mindustry.content.*; import mindustry.content.*;
import mindustry.core.*; import mindustry.core.*;
import mindustry.ctype.*; import mindustry.ctype.*;
@ -47,7 +46,7 @@ import java.util.*;
import static mindustry.Vars.*; import static mindustry.Vars.*;
@EntityDef(value = {Buildingc.class}, isFinal = false, genio = false, serialize = false) @EntityDef(value = {Buildingc.class}, isFinal = false, genio = false, serialize = false)
@Component(base = true) @Component(base = true, genInterface = false)
abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, QuadTreeObject, Displayable, Sized, Senseable, Controllable, Settable{ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, QuadTreeObject, Displayable, Sized, Senseable, Controllable, Settable{
//region vars and initialization //region vars and initialization
static final float timeToSleep = 60f * 1, recentDamageTime = 60f * 5f; static final float timeToSleep = 60f * 1, recentDamageTime = 60f * 5f;
@ -63,7 +62,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
transient Tile tile; transient Tile tile;
transient Block block; transient Block block;
transient Seq<Building> proximity = new Seq<>(6); transient Seq<Building> proximity = new Seq<>(true, 6, Building.class);
transient int cdump; transient int cdump;
transient int rotation; transient int rotation;
transient float payloadRotation; transient float payloadRotation;
@ -99,8 +98,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
private transient float timeScale = 1f, timeScaleDuration; private transient float timeScale = 1f, timeScaleDuration;
private transient float dumpAccum; private transient float dumpAccum;
private transient @Nullable SoundLoop sound;
private transient boolean sleeping; private transient boolean sleeping;
private transient float sleepTime; private transient float sleepTime;
private transient boolean initialized; private transient boolean initialized;
@ -126,6 +123,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
add(); add();
} }
checkAllowUpdate();
created(); created();
return self(); return self();
@ -136,10 +134,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
this.block = block; this.block = block;
this.team = team; this.team = team;
if(block.loopSound != Sounds.none){
sound = new SoundLoop(block.loopSound, block.loopSoundVolume);
}
health = block.health; health = block.health;
maxHealth(block.health); maxHealth(block.health);
timer(new Interval(block.timers)); timer(new Interval(block.timers));
@ -342,13 +336,14 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
} }
public @Nullable Tile findClosestEdge(Position to, Boolf<Tile> solid){ public @Nullable Tile findClosestEdge(Position to, Boolf<Tile> solid){
if(to == null) return null;
Tile best = null; Tile best = null;
float mindst = 0f; float mindst = 0f;
for(var point : Edges.getEdges(block.size)){ for(var point : Edges.getEdges(block.size)){
Tile other = Vars.world.tile(tile.x + point.x, tile.y + point.y); Tile other = Vars.world.tile(tile.x + point.x, tile.y + point.y);
if(other != null && !solid.get(other) && (best == null || to.dst2(other) < mindst)){ if(other != null && !solid.get(other) && (best == null || to.dst2(other) < mindst)){
best = other; best = other;
mindst = other.dst2(other); mindst = other.dst2(to);
} }
} }
return best; return best;
@ -473,6 +468,15 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return lastDamageTime + recentDamageTime >= Time.time; return lastDamageTime + recentDamageTime >= Time.time;
} }
public void eachEdge(Cons<Tile> cons){
for(var edge : block.getEdges()){
Tile other = world.tile(tile.x + edge.x, tile.y + edge.y);
if(other != null){
cons.get(other);
}
}
}
public Building nearby(int dx, int dy){ public Building nearby(int dx, int dy){
return world.build(tile.x + dx, tile.y + dy); return world.build(tile.x + dx, tile.y + dy);
} }
@ -809,6 +813,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return false; return false;
} }
public boolean canBeReplaced(Block other){
return other.canReplace(block);
}
public void handleItem(Building source, Item item){ public void handleItem(Building source, Item item){
items.add(item, 1); items.add(item, 1);
} }
@ -1066,7 +1074,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
} }
public void incrementDump(int prox){ public void incrementDump(int prox){
cdump = ((cdump + 1) % prox); //this is possible if transferring an item changed a block
if(prox != 0){
cdump = ((cdump + 1) % prox);
}
} }
/** Used for dumping items. */ /** Used for dumping items. */
@ -1092,6 +1103,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
} }
/** Called after this building is created in the world. May be called multiple times, or when adjacent buildings change. */ /** Called after this building is created in the world. May be called multiple times, or when adjacent buildings change. */
//TODO ??? this is just onProximityUpdate ?
public void onProximityAdded(){ public void onProximityAdded(){
if(power != null){ if(power != null){
updatePowerGraph(); updatePowerGraph();
@ -1156,16 +1168,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return getProgressIncrease(1f) / edelta(); return getProgressIncrease(1f) / edelta();
} }
/** @return whether this block should play its active sound.*/
public boolean shouldActiveSound(){
return false;
}
/** @return volume cale of active sound. */
public float activeSoundVolume(){
return 1f;
}
/** @return whether this block should play its idle sound.*/ /** @return whether this block should play its idle sound.*/
public boolean shouldAmbientSound(){ public boolean shouldAmbientSound(){
return shouldConsume(); return shouldConsume();
@ -1315,14 +1317,23 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
public void onRemoved(){ public void onRemoved(){
} }
/** Called every frame a unit is on this */ /** Called every frame a unit is on this. Hovering/flying/steppy units do not apply. */
public void unitOn(Unit unit){ public void unitOn(Unit unit){
} }
/** Called every frame a unit is on this. Applies to any unit. */
public void unitOnAny(Unit unit){
}
/** Called when a unit that spawned at this tile is removed. */ /** Called when a unit that spawned at this tile is removed. */
public void unitRemoved(Unit unit){ public void unitRemoved(Unit unit){
} }
/** Called when a puddle is on this building. Only called at an interval (40 ticks). */
public void puddleOn(Puddle puddle){
}
/** Called when arbitrary configuration is applied to a tile. */ /** Called when arbitrary configuration is applied to a tile. */
public void configured(@Nullable Unit builder, @Nullable Object value){ public void configured(@Nullable Unit builder, @Nullable Object value){
//null is of type void.class; anonymous classes use their superclass. //null is of type void.class; anonymous classes use their superclass.
@ -1372,6 +1383,19 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return block.itemCapacity; return block.itemCapacity;
} }
public void splashLiquid(Liquid liquid, float amount){
float splash = Mathf.clamp(amount / 4f, 0f, 10f);
for(int i = 0; i < Mathf.clamp(amount / 5, 0, 30); i++){
Time.run(i / 2f, () -> {
Tile other = world.tileWorld(x + Mathf.range(block.size * tilesize / 2), y + Mathf.range(block.size * tilesize / 2));
if(other != null){
Puddles.deposit(other, liquid, splash);
}
});
}
}
/** Called when a block begins (not finishes!) deconstruction. The building is still present at this point. */ /** Called when a block begins (not finishes!) deconstruction. The building is still present at this point. */
public void onDeconstructed(@Nullable Unit builder){ public void onDeconstructed(@Nullable Unit builder){
//deposit non-incinerable liquid on ground //deposit non-incinerable liquid on ground
@ -1383,9 +1407,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
/** Called when the block is destroyed. The tile is still intact at this stage. */ /** Called when the block is destroyed. The tile is still intact at this stage. */
public void onDestroyed(){ public void onDestroyed(){
if(sound != null){
sound.stop();
}
float explosiveness = block.baseExplosiveness; float explosiveness = block.baseExplosiveness;
float flammability = 0f; float flammability = 0f;
@ -1411,22 +1432,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
if(block.hasLiquids && state.rules.damageExplosions){ if(block.hasLiquids && state.rules.damageExplosions){
liquids.each((liquid, amount) -> { liquids.each(this::splashLiquid);
float splash = Mathf.clamp(amount / 4f, 0f, 10f);
for(int i = 0; i < Mathf.clamp(amount / 5, 0, 30); i++){
Time.run(i / 2f, () -> {
Tile other = world.tileWorld(x + Mathf.range(block.size * tilesize / 2), y + Mathf.range(block.size * tilesize / 2));
if(other != null){
Puddles.deposit(other, liquid, splash);
}
});
}
});
} }
//cap explosiveness so fluid tanks/vaults don't instakill units //cap explosiveness so fluid tanks/vaults don't instakill units
Damage.dynamicExplosion(x, y, flammability, explosiveness * 3.5f, power, tilesize * block.size / 2f, state.rules.damageExplosions, block.destroyEffect); Damage.dynamicExplosion(x, y, flammability, explosiveness * 3.5f, power, tilesize * block.size / 2f, state.rules.damageExplosions, block.destroyEffect, block.baseShake);
if(block.createRubble && !floor().solid && !floor().isLiquid){ if(block.createRubble && !floor().solid && !floor().isLiquid){
Effect.rubble(x, y, block.size); Effect.rubble(x, y, block.size);
@ -1657,8 +1667,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
damage = Damage.applyArmor(damage, block.armor); damage = Damage.applyArmor(damage, block.armor);
} }
damage(other.team, damage); damage(other, other.team, damage);
Events.fire(bulletDamageEvent.set(self(), other));
if(health <= 0 && !wasDead){ if(health <= 0 && !wasDead){
Events.fire(new BuildingBulletDestroyEvent(self(), other)); Events.fire(new BuildingBulletDestroyEvent(self(), other));
@ -1681,6 +1690,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
/** Changes this building's team in a safe manner. */ /** Changes this building's team in a safe manner. */
public void changeTeam(Team next){ public void changeTeam(Team next){
if(this.team == next) return; if(this.team == next) return;
if(block.forceTeam != null) team = block.forceTeam;
Team last = this.team; Team last = this.team;
boolean was = isValid(); boolean was = isValid();
@ -1693,10 +1703,12 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
indexer.addIndex(tile); indexer.addIndex(tile);
Events.fire(teamChangeEvent.set(last, self())); Events.fire(teamChangeEvent.set(last, self()));
} }
checkAllowUpdate();
} }
public boolean canPickup(){ public boolean canPickup(){
return true; return block.canPickup;
} }
/** Called right before this building is picked up. */ /** Called right before this building is picked up. */
@ -1764,6 +1776,8 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
} }
} }
public void onNearbyBuildAdded(Building other){}
public void consume(){ public void consume(){
for(Consume cons : block.consumers){ for(Consume cons : block.consumers){
cons.trigger(self()); cons.trigger(self());
@ -2094,22 +2108,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
return true; return true;
} }
@Override
public void remove(){
stopSound();
}
public void stopSound(){
if(sound != null){
sound.stop();
}
}
@Override @Override
public void killed(){ public void killed(){
dead = true; dead = true;
Events.fire(new BlockDestroyEvent(tile)); Events.fire(new BlockDestroyEvent(tile));
block.destroySound.at(tile); block.destroySound.at(tile, Mathf.random(block.destroyPitchMin, block.destroyPitchMax));
onDestroyed(); onDestroyed();
if(tile != emptyTile){ if(tile != emptyTile){
tile.remove(); tile.remove();
@ -2118,48 +2121,44 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc,
afterDestroyed(); afterDestroyed();
} }
public void checkAllowUpdate(){
if(!allowUpdate()){
enabled = false;
}
}
@Final @Final
@Replace @Replace
@Override @Override
public void update(){ public void update(){
//TODO should just avoid updating buildings instead
if(state.isEditor()) return;
//TODO refactor to timestamp-based system? //TODO refactor to timestamp-based system?
if((timeScaleDuration -= Time.delta) <= 0f || !block.canOverdrive){ if((timeScaleDuration -= Time.delta) <= 0f || !block.canOverdrive){
timeScale = 1f; timeScale = 1f;
} }
if(!allowUpdate()){ //TODO separate multithreaded system for sound? AudioSource, etc
enabled = false; if(!headless && block.ambientSound != Sounds.none && shouldAmbientSound()){
} control.sound.loop(block.ambientSound, self(), block.ambientSoundVolume * ambientVolume());
if(!headless && !wasVisible && state.rules.fog && !inFogTo(player.team())){
visibleFlags |= (1L << player.team().id);
wasVisible = true;
renderer.blocks.updateShadow(self());
renderer.minimap.update(tile);
}
//TODO separate system for sound? AudioSource, etc
if(!headless){
if(sound != null){
sound.update(x, y, shouldActiveSound(), activeSoundVolume());
}
if(block.ambientSound != Sounds.none && shouldAmbientSound()){
control.sound.loop(block.ambientSound, self(), block.ambientSoundVolume * ambientVolume());
}
} }
updateConsumption(); updateConsumption();
//TODO just handle per-block instead
if(enabled || !block.noUpdateDisabled){ if(enabled || !block.noUpdateDisabled){
updateTile(); updateTile();
} }
} }
/** When a block is newly revealed outside of camera view range, it is updated on the minimap. */
public void updateFogVisibility(){
if(!wasVisible && !inFogTo(player.team())){
visibleFlags |= (1L << player.team().id);
wasVisible = true;
renderer.blocks.updateShadow(self());
renderer.minimap.update(tile);
}
}
@Override @Override
public void hitbox(Rect out){ public void hitbox(Rect out){
out.setCentered(x, y, block.size * tilesize, block.size * tilesize); out.setCentered(x, y, block.size * tilesize, block.size * tilesize);

View file

@ -39,6 +39,7 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
//setting this variable to true prevents lifetime from decreasing for a frame. //setting this variable to true prevents lifetime from decreasing for a frame.
transient boolean keepAlive; transient boolean keepAlive;
/** Unlike the owner, the shooter is the original entity that created this bullet. For a second-stage missile, the shooter would be the turret, but the owner would be the last missile stage.*/
transient Entityc shooter; transient Entityc shooter;
transient @Nullable Tile aimTile; transient @Nullable Tile aimTile;
transient float aimX, aimY; transient float aimX, aimY;
@ -48,6 +49,9 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
transient @Nullable Trail trail; transient @Nullable Trail trail;
transient int frags; transient int frags;
transient Posc stickyTarget;
transient float stickyX, stickyY, stickyRotation, stickyRotationOffset;
@Override @Override
public void getCollisions(Cons<QuadTree> consumer){ public void getCollisions(Cons<QuadTree> consumer){
Seq<TeamData> data = state.teams.present; Seq<TeamData> data = state.teams.present;
@ -106,24 +110,43 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
@Override @Override
public boolean collides(Hitboxc other){ public boolean collides(Hitboxc other){
return type.collides && (other instanceof Teamc t && t.team() != team) return type.collides && (other instanceof Teamc t && t.team() != team)
&& !(other instanceof Flyingc f && !f.checkTarget(type.collidesAir, type.collidesGround)) && !(other instanceof Unit f && !f.checkTarget(type.collidesAir, type.collidesGround))
&& !(type.pierce && hasCollided(other.id())); //prevent multiple collisions && !(type.pierce && hasCollided(other.id())) && stickyTarget == null; //prevent multiple collisions
} }
@MethodPriority(100) @MethodPriority(100)
@Override @Override
public void collision(Hitboxc other, float x, float y){ public void collision(Hitboxc other, float x, float y){
type.hit(self(), x, y); if(type.sticky){
if(stickyTarget == null){
//must be last. //tunnel into the target a bit for better visuals
if(!type.pierce){ this.x = x + vel.x;
hit = true; this.y = y + vel.y;
remove(); stickTo(other);
}
}else{ }else{
collided.add(other.id()); type.hit(self(), x, y);
}
type.hitEntity(self(), other, other instanceof Healthc h ? h.health() : 0f); //must be last.
if(!type.pierce){
hit = true;
remove();
}else{
collided.add(other.id());
}
type.hitEntity(self(), other, other instanceof Healthc h ? h.health() : 0f);
}
}
public void stickTo(Posc other){
lifetime += type.stickyExtraLifetime;
//sticky bullets don't actually hit anything.
stickyX = this.x - other.x();
stickyY = this.y - other.y();
stickyTarget = other;
stickyRotationOffset = rotation;
stickyRotation = (other instanceof Rotc rot ? rot.rotation() : 0f);
} }
@Override @Override
@ -132,9 +155,21 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
mover.move(self()); mover.move(self());
} }
if(type.accel != 0){
vel.setLength(vel.len() + type.accel * Time.delta);
}
type.update(self()); type.update(self());
if(type.collidesTiles && type.collides && type.collidesGround){ if(stickyTarget != null){
//only stick to things that still exist in the world
if(stickyTarget instanceof Healthc h && h.isValid()){
float rotate = (stickyTarget instanceof Rotc rot ? rot.rotation() - stickyRotation : 0f);
set(Tmp.v1.set(stickyX, stickyY).rotate(rotate).add(stickyTarget));
this.rotation = rotate + stickyRotationOffset;
vel.setAngle(this.rotation);
}
}else if(type.collidesTiles && type.collides && type.collidesGround){
tileRaycast(World.toTile(lastX), World.toTile(lastY), tileX(), tileY()); tileRaycast(World.toTile(lastX), World.toTile(lastY), tileX(), tileY());
} }
@ -165,6 +200,8 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
(!build.block.underBullets || (!build.block.underBullets ||
//direct hit on correct tile //direct hit on correct tile
(aimTile != null && aimTile.build == build) || (aimTile != null && aimTile.build == build) ||
//bullet type allows hitting under bullets
type.hitUnder ||
//same team has no 'under build' mechanics //same team has no 'under build' mechanics
(build.team == team) || (build.team == team) ||
//a piercing bullet overshot the aim tile, it's fine to hit things now //a piercing bullet overshot the aim tile, it's fine to hit things now
@ -201,31 +238,46 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
&& build.collide(self()) && type.testCollision(self(), build) && build.collide(self()) && type.testCollision(self(), build)
&& !build.dead() && (type.collidesTeam || build.team != team) && !(type.pierceBuilding && hasCollided(build.id))){ && !build.dead() && (type.collidesTeam || build.team != team) && !(type.pierceBuilding && hasCollided(build.id))){
boolean remove = false; if(type.sticky){
float health = build.health; if(build.team != team){
//stick to edge of block
Vec2 hit = Geometry.raycastRect(lastX, lastY, x, y, Tmp.r1.setCentered(x * tilesize, y * tilesize, tilesize, tilesize));
if(hit != null){
this.x = hit.x;
this.y = hit.y;
}
if(build.team != team){ stickTo(build);
remove = build.collision(self());
}
if(remove || type.collidesTeam){ return;
if(Mathf.dst2(lastX, lastY, x * tilesize, y * tilesize) < Mathf.dst2(lastX, lastY, this.x, this.y)){ }
this.x = x * tilesize; }else{
this.y = y * tilesize; boolean remove = false;
float health = build.health;
if(build.team != team){
remove = build.collision(self());
} }
if(!type.pierceBuilding){ if(remove || type.collidesTeam){
hit = true; if(Mathf.dst2(lastX, lastY, x * tilesize, y * tilesize) < Mathf.dst2(lastX, lastY, this.x, this.y)){
remove(); this.x = x * tilesize;
}else{ this.y = y * tilesize;
collided.add(build.id); }
if(!type.pierceBuilding){
hit = true;
remove();
}else{
collided.add(build.id);
}
} }
type.hitTile(self(), build, x * tilesize, y * tilesize, health, true);
//stop raycasting when building is hit
if(type.pierceBuilding) return;
} }
type.hitTile(self(), build, x * tilesize, y * tilesize, health, true);
//stop raycasting when building is hit
if(type.pierceBuilding) return;
} }
if(x == x2 && y == y2) break; if(x == x2 && y == y2) break;
@ -247,7 +299,11 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw
public void draw(){ public void draw(){
Draw.z(type.layer); Draw.z(type.layer);
type.draw(self()); if(type.underwater){
Drawf.underwater(() -> type.draw(self()));
}else{
type.draw(self());
}
type.drawLight(self()); type.drawLight(self());
Draw.reset(); Draw.reset();

View file

@ -1,10 +1,8 @@
package mindustry.entities.comp; package mindustry.entities.comp;
import arc.math.*; import arc.math.*;
import arc.math.geom.*;
import arc.util.*; import arc.util.*;
import mindustry.*; import mindustry.*;
import mindustry.ai.*;
import mindustry.annotations.Annotations.*; import mindustry.annotations.Annotations.*;
import mindustry.content.*; import mindustry.content.*;
import mindustry.entities.*; import mindustry.entities.*;
@ -22,7 +20,6 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{
@Import float x, y, speedMultiplier, rotation, hitSize; @Import float x, y, speedMultiplier, rotation, hitSize;
@Import UnitType type; @Import UnitType type;
@Import Team team; @Import Team team;
@Import Vec2 vel;
transient Floor lastDeepFloor; transient Floor lastDeepFloor;
transient float lastCrawlSlowdown = 1f; transient float lastCrawlSlowdown = 1f;
@ -31,13 +28,7 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{
@Replace @Replace
@Override @Override
public SolidPred solidity(){ public SolidPred solidity(){
return EntityCollisions::legsSolid; return ignoreSolids() ? null : EntityCollisions::legsSolid;
}
@Override
@Replace
public int pathType(){
return Pathfinder.costLegs;
} }
@Override @Override
@ -110,6 +101,6 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{
} }
segmentRot = Angles.clampRange(segmentRot, rotation, type.segmentMaxRot); segmentRot = Angles.clampRange(segmentRot, rotation, type.segmentMaxRot);
crawlTime += vel.len() * Time.delta; crawlTime += deltaLen();
} }
} }

View file

@ -6,13 +6,13 @@ import mindustry.entities.EntityCollisions.*;
import mindustry.gen.*; import mindustry.gen.*;
@Component @Component
abstract class ElevationMoveComp implements Velc, Posc, Flyingc, Hitboxc{ abstract class ElevationMoveComp implements Velc, Posc, Hitboxc, Unitc{
@Import float x, y; @Import float x, y;
@Replace @Replace
@Override @Override
public SolidPred solidity(){ public SolidPred solidity(){
return isFlying() ? null : EntityCollisions::solid; return isFlying() || ignoreSolids() ? null : EntityCollisions::solid;
} }
} }

View file

@ -59,12 +59,16 @@ abstract class EntityComp{
} }
void beforeWrite(){
}
void afterRead(){ void afterRead(){
} }
/** Called after *all* entities are read. */ //called after all entities have been read (useful for ID resolution)
void afterAllRead(){ void afterReadAll(){
} }
} }

View file

@ -1,123 +0,0 @@
package mindustry.entities.comp;
import arc.*;
import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*;
@Component
abstract class FlyingComp implements Posc, Velc, Healthc, Hitboxc{
private static final Vec2 tmp1 = new Vec2(), tmp2 = new Vec2();
@Import float x, y, speedMultiplier, hitSize;
@Import Vec2 vel;
@Import UnitType type;
@SyncLocal float elevation;
private transient boolean wasFlying;
transient boolean hovering;
transient float drownTime;
transient float splashTimer;
transient @Nullable Floor lastDrownFloor;
boolean checkTarget(boolean targetAir, boolean targetGround){
return (isGrounded() && targetGround) || (isFlying() && targetAir);
}
boolean isGrounded(){
return elevation < 0.001f;
}
boolean isFlying(){
return elevation >= 0.09f;
}
boolean canDrown(){
return isGrounded() && !hovering;
}
@Nullable Floor drownFloor(){
return canDrown() ? floorOn() : null;
}
boolean emitWalkSound(){
return true;
}
void landed(){
}
void wobble(){
x += Mathf.sin(Time.time + (id() % 10) * 12, 25f, 0.05f) * Time.delta * elevation;
y += Mathf.cos(Time.time + (id() % 10) * 12, 25f, 0.05f) * Time.delta * elevation;
}
void moveAt(Vec2 vector, float acceleration){
Vec2 t = tmp1.set(vector); //target vector
tmp2.set(t).sub(vel).limit(acceleration * vector.len() * Time.delta); //delta vector
vel.add(tmp2);
}
float floorSpeedMultiplier(){
Floor on = isFlying() || hovering ? Blocks.air.asFloor() : floorOn();
return on.speedMultiplier * speedMultiplier;
}
@Override
public void update(){
Floor floor = floorOn();
if(isFlying() != wasFlying){
if(wasFlying){
if(tileOn() != null){
Fx.unitLand.at(x, y, floorOn().isLiquid ? 1f : 0.5f, tileOn().floor().mapColor);
}
}
wasFlying = isFlying();
}
if(!hovering && isGrounded()){
if((splashTimer += Mathf.dst(deltaX(), deltaY())) >= (7f + hitSize()/8f)){
floor.walkEffect.at(x, y, hitSize() / 8f, floor.mapColor);
splashTimer = 0f;
if(emitWalkSound()){
floor.walkSound.at(x, y, Mathf.random(floor.walkSoundPitchMin, floor.walkSoundPitchMax), floor.walkSoundVolume);
}
}
}
updateDrowning();
}
public void updateDrowning(){
Floor floor = drownFloor();
if(floor != null && floor.isLiquid && floor.drownTime > 0){
lastDrownFloor = floor;
drownTime += Time.delta / floor.drownTime / type.drownTimeMultiplier;
if(Mathf.chanceDelta(0.05f)){
floor.drownUpdateEffect.at(x, y, hitSize, floor.mapColor);
}
if(drownTime >= 0.999f && !net.client()){
kill();
Events.fire(new UnitDrownEvent(self()));
}
}else{
drownTime -= Time.delta / 50f;
}
drownTime = Mathf.clamp(drownTime);
}
}

View file

@ -4,7 +4,6 @@ import arc.math.*;
import arc.math.geom.*; import arc.math.geom.*;
import arc.util.*; import arc.util.*;
import mindustry.*; import mindustry.*;
import mindustry.ai.*;
import mindustry.annotations.Annotations.*; import mindustry.annotations.Annotations.*;
import mindustry.content.*; import mindustry.content.*;
import mindustry.entities.*; import mindustry.entities.*;
@ -19,7 +18,7 @@ import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*; import static mindustry.Vars.*;
@Component @Component
abstract class LegsComp implements Posc, Rotc, Hitboxc, Flyingc, Unitc{ abstract class LegsComp implements Posc, Rotc, Hitboxc, Unitc{
private static final Vec2 straightVec = new Vec2(); private static final Vec2 straightVec = new Vec2();
@Import float x, y, rotation, speedMultiplier; @Import float x, y, rotation, speedMultiplier;
@ -37,13 +36,7 @@ abstract class LegsComp implements Posc, Rotc, Hitboxc, Flyingc, Unitc{
@Replace @Replace
@Override @Override
public SolidPred solidity(){ public SolidPred solidity(){
return type.allowLegStep ? EntityCollisions::legsSolid : EntityCollisions::solid; return ignoreSolids() ? null : type.allowLegStep ? EntityCollisions::legsSolid : EntityCollisions::solid;
}
@Override
@Replace
public int pathType(){
return type.allowLegStep ? Pathfinder.costLegs : Pathfinder.costGround;
} }
@Override @Override
@ -111,6 +104,7 @@ abstract class LegsComp implements Posc, Rotc, Hitboxc, Flyingc, Unitc{
legs[i] = l; legs[i] = l;
} }
totalLength = Mathf.random(100f);
} }
@Override @Override

View file

@ -12,7 +12,7 @@ import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*; import static mindustry.Vars.*;
@Component @Component
abstract class MechComp implements Posc, Flyingc, Hitboxc, Unitc, Mechc, ElevationMovec{ abstract class MechComp implements Posc, Hitboxc, Unitc, Mechc, ElevationMovec{
@Import float x, y, hitSize; @Import float x, y, hitSize;
@Import UnitType type; @Import UnitType type;
@ -68,7 +68,7 @@ abstract class MechComp implements Posc, Flyingc, Hitboxc, Unitc, Mechc, Elevati
} }
} }
} }
return canDrown() ? floorOn() : null; return floorOn();
} }
public float walkExtend(boolean scaled){ public float walkExtend(boolean scaled){

View file

@ -10,7 +10,7 @@ import mindustry.gen.*;
* Will bounce off of other objects that are at similar elevations. * Will bounce off of other objects that are at similar elevations.
* Has mass.*/ * Has mass.*/
@Component @Component
abstract class PhysicsComp implements Velc, Hitboxc, Flyingc{ abstract class PhysicsComp implements Velc, Hitboxc{
@Import float hitSize, x, y; @Import float hitSize, x, y;
@Import Vec2 vel; @Import Vec2 vel;

View file

@ -23,7 +23,7 @@ abstract class PuddleComp implements Posc, Puddlec, Drawc, Syncc{
private static Puddle paramPuddle; private static Puddle paramPuddle;
private static Cons<Unit> unitCons = unit -> { private static Cons<Unit> unitCons = unit -> {
if(unit.isGrounded() && !unit.hovering){ if(unit.isGrounded() && !unit.type.hovering){
unit.hitbox(rect2); unit.hitbox(rect2);
if(rect.overlaps(rect2)){ if(rect.overlaps(rect2)){
unit.apply(paramPuddle.liquid.effect, 60 * 2); unit.apply(paramPuddle.liquid.effect, 60 * 2);
@ -104,6 +104,10 @@ abstract class PuddleComp implements Posc, Puddlec, Drawc, Syncc{
} }
updateTime = 40f; updateTime = 40f;
if(tile.build != null){
tile.build.puddleOn(self());
}
} }
if(!headless && liquid.particleEffect != Fx.none){ if(!headless && liquid.particleEffect != Fx.none){

View file

@ -0,0 +1,158 @@
package mindustry.entities.comp;
import arc.math.*;
import arc.math.geom.*;
import arc.util.*;
import mindustry.ai.types.*;
import mindustry.annotations.Annotations.*;
import mindustry.async.*;
import mindustry.gen.*;
import mindustry.type.*;
@Component
abstract class SegmentComp implements Posc, Rotc, Hitboxc, Unitc, Segmentc{
@Import float x, y, rotation;
@Import UnitType type;
@Import Vec2 vel;
transient @Nullable Segmentc parentSegment, childSegment, headSegment;
transient int segmentIndex;
int parentId;
public boolean isHead(){
return parentSegment == null;
}
public void addChild(Unit other){
if(other == self()) return;
if(childSegment != null){
childSegment.parentSegment(null);
}
if(other instanceof Segmentc seg){
if(seg.parentSegment() != null){
seg.parentSegment().childSegment(null);
}
childSegment = seg;
seg.parentSegment(this);
}
}
@Override
@Replace
public boolean ignoreSolids(){
return isFlying() || parentSegment != null;
}
//TODO make it phase through things.
@Override
public void update(){
if(childSegment != null && !childSegment.isValid()){
childSegment = null;
}
if(parentSegment != null && !parentSegment.isValid()){
parentSegment = null;
}
if(parentSegment == null){
segmentIndex = 0;
if(childSegment != null){
headSegment = this;
childSegment.updateSegment(this, this, 1);
}
}
}
@Replace
@Override
public boolean playerControllable(){
return type.playerControllable && isHead();
}
@Override
@Replace
public boolean shouldUpdateController(){
return isHead();
}
@Override
@Replace
public boolean moving(){
if(isHead()){
return !vel.isZero(0.01f);
}else{
return deltaLen() / Time.delta >= 0.01f;
}
}
@Override
@Replace
public int collisionLayer(){
if(parentSegment != null) return -1;
return type.allowLegStep && type.legPhysicsLayer ? PhysicsProcess.layerLegs : isGrounded() ? PhysicsProcess.layerGround : PhysicsProcess.layerFlying;
}
@Replace
@Override
public boolean isCommandable(){
return parentSegment == null && controller() instanceof CommandAI;
}
@Override
public void afterSync(){
checkParent();
}
@Override
public void afterReadAll(){
checkParent();
}
@Override
public void beforeWrite(){
parentId = parentSegment == null ? -1 : parentSegment.id();
}
public void checkParent(){
if(parentId != -1){
var parent = Groups.unit.getByID(parentId);
if(parent instanceof Segmentc seg){
parentSegment = seg;
seg.childSegment(this);
return;
}
parentId = -1;
}
//TODO should this unassign the parent's child too?
parentSegment = null;
}
public void updateSegment(Segmentc head, Segmentc parent, int index){
rotation = Angles.clampRange(rotation, parent.rotation(), type.segmentRotationRange);
segmentIndex = index;
headSegment = head;
float headDelta = head.deltaLen();
//TODO should depend on the head's speed.
if(headDelta > 0.001f){
rotation = Mathf.slerpDelta(rotation, parent.rotation(), type.baseRotateSpeed * Mathf.clamp(headDelta / type().speed / Time.delta));
}
Vec2 moveVec = Tmp.v1.trns(rotation + 180f, type.segmentSpacing).add(parent).sub(x, y);
float prefSpeed = type.speed * Time.delta * 9999f;
move(moveVec.limit(prefSpeed)); //TODO other segments are left behind
if(childSegment != null){
childSegment.updateSegment(head, this, index + 1);
}
}
}

View file

@ -15,7 +15,7 @@ import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*; import static mindustry.Vars.*;
@Component @Component
abstract class StatusComp implements Posc, Flyingc{ abstract class StatusComp implements Posc{
private Seq<StatusEntry> statuses = new Seq<>(4); private Seq<StatusEntry> statuses = new Seq<>(4);
private transient Bits applied = new Bits(content.getBy(ContentType.status).size); private transient Bits applied = new Bits(content.getBy(ContentType.status).size);
@ -28,12 +28,12 @@ abstract class StatusComp implements Posc, Flyingc{
@Import float maxHealth; @Import float maxHealth;
/** Apply a status effect for 1 tick (for permanent effects) **/ /** Apply a status effect for 1 tick (for permanent effects) **/
void apply(StatusEffect effect){ public void apply(StatusEffect effect){
apply(effect, 1); apply(effect, 1);
} }
/** Adds a status effect to this unit. */ /** Adds a status effect to this unit. */
void apply(StatusEffect effect, float duration){ public void apply(StatusEffect effect, float duration){
if(effect == StatusEffects.none || effect == null || isImmune(effect)) return; //don't apply empty or immune effects if(effect == StatusEffects.none || effect == null || isImmune(effect)) return; //don't apply empty or immune effects
//unlock status effects regardless of whether they were applied to friendly units //unlock status effects regardless of whether they were applied to friendly units
@ -67,18 +67,18 @@ abstract class StatusComp implements Posc, Flyingc{
} }
} }
float getDuration(StatusEffect effect){ public float getDuration(StatusEffect effect){
var entry = statuses.find(e -> e.effect == effect); var entry = statuses.find(e -> e.effect == effect);
return entry == null ? 0 : entry.time; return entry == null ? 0 : entry.time;
} }
void clearStatuses(){ public void clearStatuses(){
statuses.each(e -> e.effect.onRemoved(self())); statuses.each(e -> e.effect.onRemoved(self()));
statuses.clear(); statuses.clear();
} }
/** Removes a status effect. */ /** Removes a status effect. */
void unapply(StatusEffect effect){ public void unapply(StatusEffect effect){
statuses.remove(e -> { statuses.remove(e -> {
if(e.effect == effect){ if(e.effect == effect){
e.effect.onRemoved(self()); e.effect.onRemoved(self());
@ -89,13 +89,15 @@ abstract class StatusComp implements Posc, Flyingc{
}); });
} }
boolean isBoss(){ public boolean isBoss(){
return hasEffect(StatusEffects.boss); return hasEffect(StatusEffects.boss);
} }
abstract boolean isImmune(StatusEffect effect); public boolean isImmune(StatusEffect effect){
return type.immunities.contains(effect);
}
Color statusColor(){ public Color statusColor(){
if(statuses.size == 0){ if(statuses.size == 0){
return Tmp.c1.set(Color.white); return Tmp.c1.set(Color.white);
} }
@ -168,6 +170,8 @@ abstract class StatusComp implements Posc, Flyingc{
applyDynamicStatus().armorOverride = armor; applyDynamicStatus().armorOverride = armor;
} }
public abstract boolean isGrounded();
@Override @Override
public void update(){ public void update(){
Floor floor = floorOn(); Floor floor = floorOn();
@ -237,7 +241,7 @@ abstract class StatusComp implements Posc, Flyingc{
} }
} }
boolean hasEffect(StatusEffect effect){ public boolean hasEffect(StatusEffect effect){
return applied.get(effect.id); return applied.get(effect.id);
} }
} }

View file

@ -17,9 +17,9 @@ import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*; import static mindustry.Vars.*;
@Component @Component
abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec{ abstract class TankComp implements Posc, Hitboxc, Unitc, ElevationMovec{
@Import float x, y, hitSize, rotation, speedMultiplier; @Import float x, y, hitSize, rotation, speedMultiplier;
@Import boolean hovering, disarmed; @Import boolean disarmed;
@Import UnitType type; @Import UnitType type;
@Import Team team; @Import Team team;
@ -27,6 +27,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
transient float treadTime; transient float treadTime;
transient boolean walked; transient boolean walked;
transient Floor lastDeepFloor;
@Override @Override
public void update(){ public void update(){
@ -51,6 +52,9 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
} }
} }
lastDeepFloor = null;
boolean anyNonDeep = false;
//calculate overlapping tiles so it slows down when going "over" walls //calculate overlapping tiles so it slows down when going "over" walls
int r = Math.max((int)(hitSize * 0.6f / tilesize), 0); int r = Math.max((int)(hitSize * 0.6f / tilesize), 0);
@ -62,6 +66,12 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
solids ++; solids ++;
} }
if(t.floor().isDeep()){
lastDeepFloor = t.floor();
}else{
anyNonDeep = true;
}
//TODO should this apply to the player team(s)? currently PvE due to balancing //TODO should this apply to the player team(s)? currently PvE due to balancing
if(type.crushDamage > 0 && !disarmed && (walked || deltaLen() >= 0.01f) && t != null if(type.crushDamage > 0 && !disarmed && (walked || deltaLen() >= 0.01f) && t != null
//damage radius is 1 tile smaller to prevent it from just touching walls as it passes //damage radius is 1 tile smaller to prevent it from just touching walls as it passes
@ -76,6 +86,10 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
} }
} }
if(anyNonDeep){
lastDeepFloor = null;
}
lastSlowdown = Mathf.lerp(1f, type.crawlSlowdown, Mathf.clamp((float)solids / total / type.crawlSlowdownFrac)); lastSlowdown = Mathf.lerp(1f, type.crawlSlowdown, Mathf.clamp((float)solids / total / type.crawlSlowdownFrac));
//trigger animation only when walking manually //trigger animation only when walking manually
@ -89,7 +103,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
@Override @Override
@Replace @Replace
public float floorSpeedMultiplier(){ public float floorSpeedMultiplier(){
Floor on = isFlying() || hovering ? Blocks.air.asFloor() : floorOn(); Floor on = isFlying() || type.hovering ? Blocks.air.asFloor() : floorOn();
//TODO take into account extra blocks //TODO take into account extra blocks
return on.speedMultiplier * speedMultiplier * lastSlowdown; return on.speedMultiplier * speedMultiplier * lastSlowdown;
} }
@ -97,17 +111,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec
@Replace @Replace
@Override @Override
public @Nullable Floor drownFloor(){ public @Nullable Floor drownFloor(){
//tanks can only drown when all the nearby floors are deep return canDrown() ? lastDeepFloor : null;
//TODO implement properly
if(hitSize >= 12 && canDrown()){
for(Point2 p : Geometry.d8){
Floor f = world.floorWorld(x + p.x * tilesize, y + p.y * tilesize);
if(!f.isDeep()){
return null;
}
}
}
return canDrown() ? floorOn() : null;
} }
@Override @Override

View file

@ -0,0 +1,39 @@
package mindustry.entities.comp;
import mindustry.annotations.Annotations.*;
import mindustry.async.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.type.*;
@Component
abstract class UnderwaterMoveComp implements WaterMovec{
@Import UnitType type;
@MethodPriority(10f)
@Replace
public void draw(){
//TODO draw status effects?
Drawf.underwater(() -> {
type.draw(self());
});
}
@Override
public int collisionLayer(){
return PhysicsProcess.layerUnderwater;
}
@Override
public boolean hittable(){
return false && type.hittable(self());
}
@Override
public boolean targetable(Team targeter){
return false && type.targetable(self(), targeter);
}
}

View file

@ -7,7 +7,6 @@ import arc.math.*;
import arc.math.geom.*; import arc.math.geom.*;
import arc.scene.ui.layout.*; import arc.scene.ui.layout.*;
import arc.util.*; import arc.util.*;
import mindustry.ai.*;
import mindustry.ai.types.*; import mindustry.ai.types.*;
import mindustry.annotations.Annotations.*; import mindustry.annotations.Annotations.*;
import mindustry.async.*; import mindustry.async.*;
@ -34,10 +33,12 @@ import static mindustry.Vars.*;
import static mindustry.logic.GlobalVars.*; import static mindustry.logic.GlobalVars.*;
@Component(base = true) @Component(base = true)
abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, Itemsc, Rotc, Unitc, Weaponsc, Drawc, Boundedc, Syncc, Shieldc, Displayable, Ranged, Minerc, Builderc, Senseable, Settable{ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, Itemsc, Rotc, Unitc, Weaponsc, Drawc, Syncc, Shieldc, Displayable, Ranged, Minerc, Builderc, Senseable, Settable{
private static final Vec2 tmp1 = new Vec2(), tmp2 = new Vec2();
static final float warpDst = 30f;
@Import boolean hovering, dead, disarmed; @Import boolean dead, disarmed;
@Import float x, y, rotation, elevation, maxHealth, drag, armor, hitSize, health, shield, ammo, dragMultiplier, armorOverride, speedMultiplier; @Import float x, y, rotation, maxHealth, drag, armor, hitSize, health, shield, ammo, dragMultiplier, armorOverride, speedMultiplier;
@Import Team team; @Import Team team;
@Import int id; @Import int id;
@Import @Nullable Tile mineTile; @Import @Nullable Tile mineTile;
@ -62,6 +63,48 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
private transient boolean wasPlayer; private transient boolean wasPlayer;
private transient boolean wasHealed; private transient boolean wasHealed;
@SyncLocal float elevation;
private transient boolean wasFlying;
transient float drownTime;
transient float splashTimer;
transient @Nullable Floor lastDrownFloor;
public boolean checkTarget(boolean targetAir, boolean targetGround){
return (isGrounded() && targetGround) || (isFlying() && targetAir);
}
public boolean isGrounded(){
return elevation < 0.001f;
}
public boolean isFlying(){
return elevation >= 0.09f;
}
public boolean canDrown(){
return isGrounded() && type.canDrown;
}
public @Nullable Floor drownFloor(){
return floorOn();
}
public void wobble(){
x += Mathf.sin(Time.time + (id % 10) * 12, 25f, 0.05f) * Time.delta * elevation;
y += Mathf.cos(Time.time + (id % 10) * 12, 25f, 0.05f) * Time.delta * elevation;
}
public void moveAt(Vec2 vector, float acceleration){
Vec2 t = tmp1.set(vector); //target vector
tmp2.set(t).sub(vel).limit(acceleration * vector.len() * Time.delta); //delta vector
vel.add(tmp2);
}
public float floorSpeedMultiplier(){
Floor on = isFlying() || type.hovering ? Blocks.air.asFloor() : floorOn();
return on.speedMultiplier * speedMultiplier;
}
/** Called when this unit was unloaded from a factory or spawn point. */ /** Called when this unit was unloaded from a factory or spawn point. */
public void unloaded(){ public void unloaded(){
@ -352,12 +395,6 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
} }
} }
@Override
@Replace
public boolean canDrown(){
return isGrounded() && !hovering && type.canDrown;
}
@Override @Override
@Replace @Replace
public boolean canShoot(){ public boolean canShoot(){
@ -420,11 +457,6 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
return type.allowLegStep && type.legPhysicsLayer ? PhysicsProcess.layerLegs : isGrounded() ? PhysicsProcess.layerGround : PhysicsProcess.layerFlying; return type.allowLegStep && type.legPhysicsLayer ? PhysicsProcess.layerLegs : isGrounded() ? PhysicsProcess.layerGround : PhysicsProcess.layerFlying;
} }
/** @return pathfinder path type for calculating costs. This is used for wave AI only. (TODO: remove) */
public int pathType(){
return Pathfinder.costGround;
}
public void lookAt(float angle){ public void lookAt(float angle){
rotation = Angles.moveToward(rotation, angle, type.rotateSpeed * Time.delta * speedMultiplier()); rotation = Angles.moveToward(rotation, angle, type.rotateSpeed * Time.delta * speedMultiplier());
} }
@ -445,8 +477,8 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
return controller instanceof CommandAI; return controller instanceof CommandAI;
} }
public boolean canTarget(Unit other){ public boolean canTarget(Teamc other){
return other != null && other.checkTarget(type.targetAir, type.targetGround); return other != null && (other instanceof Unit u ? u.checkTarget(type.targetAir, type.targetGround) : (other instanceof Building b && type.targetGround));
} }
public CommandAI command(){ public CommandAI command(){
@ -475,9 +507,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
this.drag = type.drag; this.drag = type.drag;
this.armor = type.armor; this.armor = type.armor;
this.hitSize = type.hitSize; this.hitSize = type.hitSize;
this.hovering = type.hovering;
if(controller == null) controller(type.createController(self()));
if(mounts().length != type.weapons.size) setupWeapons(type); if(mounts().length != type.weapons.size) setupWeapons(type);
if(abilities.length != type.abilities.size){ if(abilities.length != type.abilities.size){
abilities = new Ability[type.abilities.size]; abilities = new Ability[type.abilities.size];
@ -485,6 +515,11 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
abilities[i] = type.abilities.get(i).copy(); abilities[i] = type.abilities.get(i).copy();
} }
} }
if(controller == null) controller(type.createController(self()));
}
public boolean playerControllable(){
return type.playerControllable;
} }
public boolean targetable(Team targeter){ public boolean targetable(Team targeter){
@ -504,7 +539,8 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
@Override @Override
public void afterRead(){ public void afterRead(){
afterSync(); setType(this.type);
controller.unit(self());
//reset controller state //reset controller state
if(!(controller instanceof AIController ai && ai.keepState())){ if(!(controller instanceof AIController ai && ai.keepState())){
controller(type.createController(self())); controller(type.createController(self()));
@ -512,7 +548,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
} }
@Override @Override
public void afterAllRead(){ public void afterReadAll(){
controller.afterRead(self()); controller.afterRead(self());
} }
@ -555,11 +591,98 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
} }
} }
public void updateDrowning(){
Floor floor = drownFloor();
if(floor != null && floor.isLiquid && floor.drownTime > 0 && canDrown()){
lastDrownFloor = floor;
drownTime += Time.delta / floor.drownTime / type.drownTimeMultiplier;
if(Mathf.chanceDelta(0.05f)){
floor.drownUpdateEffect.at(x, y, hitSize, floor.mapColor);
}
if(drownTime >= 0.999f && !net.client()){
kill();
Events.fire(new UnitDrownEvent(self()));
}
}else{
drownTime -= Time.delta / 50f;
}
drownTime = Mathf.clamp(drownTime);
}
@Override @Override
public void update(){ public void update(){
type.update(self()); type.update(self());
//update bounds
if(type.bounded){
float bot = 0f, left = 0f, top = world.unitHeight(), right = world.unitWidth();
//TODO hidden map rules only apply to player teams? should they?
if(state.rules.limitMapArea && !team.isAI()){
bot = state.rules.limitY * tilesize;
left = state.rules.limitX * tilesize;
top = state.rules.limitHeight * tilesize + bot;
right = state.rules.limitWidth * tilesize + left;
}
if(!net.client() || isLocal()){
float dx = 0f, dy = 0f;
//repel unit out of bounds
if(x < left) dx += (-(x - left)/warpDst);
if(y < bot) dy += (-(y - bot)/warpDst);
if(x > right) dx -= (x - right)/warpDst;
if(y > top) dy -= (y - top)/warpDst;
velAddNet(dx * Time.delta, dy * Time.delta);
}
//clamp position if not flying
if(isGrounded()){
x = Mathf.clamp(x, left, right - tilesize);
y = Mathf.clamp(y, bot, top - tilesize);
}
//kill when out of bounds
if(x < -finalWorldBounds + left || y < -finalWorldBounds + bot || x >= right + finalWorldBounds || y >= top + finalWorldBounds){
kill();
}
}
//update drown/flying state
Floor floor = floorOn();
Tile tile = tileOn();
if(isFlying() != wasFlying){
if(wasFlying){
if(tile != null){
Fx.unitLand.at(x, y, floor.isLiquid ? 1f : 0.5f, tile.floor().mapColor);
}
}
wasFlying = isFlying();
}
if(!type.hovering && isGrounded() && type.emitWalkEffect){
if((splashTimer += Mathf.dst(deltaX(), deltaY())) >= (7f + hitSize()/8f)){
floor.walkEffect.at(x, y, hitSize() / 8f, floor.mapColor);
splashTimer = 0f;
if(type.emitWalkSound){
floor.walkSound.at(x, y, Mathf.random(floor.walkSoundPitchMin, floor.walkSoundPitchMax), floor.walkSoundVolume);
}
}
}
updateDrowning();
if(wasHealed && healTime <= -1f){ if(wasHealed && healTime <= -1f){
healTime = 1f; healTime = 1f;
} }
@ -647,8 +770,9 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
} }
} }
Tile tile = tileOn(); if(tile != null && tile.build != null){
Floor floor = floorOn(); tile.build.unitOnAny(self());
}
if(tile != null && isGrounded() && !type.hovering){ if(tile != null && isGrounded() && !type.hovering){
//unit block update //unit block update
@ -673,7 +797,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
} }
//AI only updates on the server //AI only updates on the server
if(!net.client() && !dead){ if(!net.client() && !dead && shouldUpdateController()){
controller.updateUnit(); controller.updateUnit();
} }
@ -688,6 +812,10 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
} }
} }
public boolean shouldUpdateController(){
return true;
}
/** @return a preview UI icon for this unit. */ /** @return a preview UI icon for this unit. */
public TextureRegion icon(){ public TextureRegion icon(){
return type.uiIcon; return type.uiIcon;
@ -770,11 +898,6 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I
type.display(self(), table); type.display(self(), table);
} }
@Override
public boolean isImmune(StatusEffect effect){
return type.immunities.contains(effect);
}
@Override @Override
public void draw(){ public void draw(){
type.draw(self()); type.draw(self());

View file

@ -39,6 +39,10 @@ abstract class VelComp implements Posc{
return null; return null;
} }
boolean ignoreSolids(){
return false;
}
/** @return whether this entity can move through a location*/ /** @return whether this entity can move through a location*/
boolean canPass(int tileX, int tileY){ boolean canPass(int tileX, int tileY){
SolidPred s = solidity(); SolidPred s = solidity();

View file

@ -0,0 +1,38 @@
package mindustry.entities.comp;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.entities.EntityCollisions.*;
import mindustry.gen.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.environment.*;
@Component
abstract class WaterCrawlComp implements Posc, Velc, Hitboxc, Unitc, Crawlc{
@Import float x, y, rotation, speedMultiplier;
@Import UnitType type;
@Replace
public SolidPred solidity(){
return isFlying() || ignoreSolids() ? null : EntityCollisions::waterSolid;
}
@Replace
public boolean onSolid(){
return EntityCollisions.waterSolid(tileX(), tileY());
}
@Replace
public float floorSpeedMultiplier(){
Floor on = isFlying() ? Blocks.air.asFloor() : floorOn();
return (on.shallow ? 1f : 1.3f) * speedMultiplier;
}
public boolean onLiquid(){
Tile tile = tileOn();
return tile != null && tile.floor().isLiquid;
}
}

View file

@ -4,7 +4,6 @@ import arc.graphics.*;
import arc.graphics.g2d.*; import arc.graphics.g2d.*;
import arc.math.*; import arc.math.*;
import arc.util.*; import arc.util.*;
import mindustry.ai.*;
import mindustry.annotations.Annotations.*; import mindustry.annotations.Annotations.*;
import mindustry.content.*; import mindustry.content.*;
import mindustry.entities.*; import mindustry.entities.*;
@ -18,7 +17,7 @@ import mindustry.world.blocks.environment.*;
import static mindustry.Vars.*; import static mindustry.Vars.*;
@Component @Component
abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Flyingc, Unitc{ abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Unitc{
@Import float x, y, rotation, speedMultiplier; @Import float x, y, rotation, speedMultiplier;
@Import UnitType type; @Import UnitType type;
@ -38,19 +37,6 @@ abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Flyingc, Unitc{
} }
} }
@Override
@Replace
public int pathType(){
return Pathfinder.costNaval;
}
//don't want obnoxious splashing
@Override
@Replace
public boolean emitWalkSound(){
return false;
}
@Override @Override
public void add(){ public void add(){
tleft.clear(); tleft.clear();
@ -59,6 +45,7 @@ abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Flyingc, Unitc{
@Override @Override
public void draw(){ public void draw(){
//TODO: move to UnitType
float z = Draw.z(); float z = Draw.z();
Draw.z(Layer.debris); Draw.z(Layer.debris);
@ -76,7 +63,7 @@ abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Flyingc, Unitc{
@Replace @Replace
@Override @Override
public SolidPred solidity(){ public SolidPred solidity(){
return isFlying() ? null : EntityCollisions::waterSolid; return isFlying() || ignoreSolids() ? null : EntityCollisions::waterSolid;
} }
@Replace @Replace

View file

@ -23,6 +23,8 @@ public class RegionPart extends DrawPart{
public boolean mirror = false; public boolean mirror = false;
/** If true, an outline is drawn under the part. */ /** If true, an outline is drawn under the part. */
public boolean outline = true; public boolean outline = true;
/** If true, this part has an outline created 'in-place'. Currently vanilla only, do not use this! */
public boolean replaceOutline = false;
/** If true, the base + outline regions are drawn. Set to false for heat-only regions. */ /** If true, the base + outline regions are drawn. Set to false for heat-only regions. */
public boolean drawRegion = true; public boolean drawRegion = true;
/** If true, the heat region produces light. */ /** If true, the heat region produces light. */
@ -38,7 +40,8 @@ public class RegionPart extends DrawPart{
public Blending blending = Blending.normal; public Blending blending = Blending.normal;
public float layer = -1, layerOffset = 0f, heatLayerOffset = 1f, turretHeatLayer = Layer.turretHeat; public float layer = -1, layerOffset = 0f, heatLayerOffset = 1f, turretHeatLayer = Layer.turretHeat;
public float outlineLayerOffset = -0.001f; public float outlineLayerOffset = -0.001f;
public float x, y, xScl = 1f, yScl = 1f, rotation; //note that origin DOES NOT AFFECT child parts
public float x, y, xScl = 1f, yScl = 1f, rotation, originX, originY;
public float moveX, moveY, growX, growY, moveRot; public float moveX, moveY, growX, growY, moveRot;
public float heatLightOpacity = 0.3f; public float heatLightOpacity = 0.3f;
public @Nullable Color color, colorTo, mixColor, mixColorTo; public @Nullable Color color, colorTo, mixColor, mixColorTo;
@ -99,16 +102,21 @@ public class RegionPart extends DrawPart{
float sign = (i == 0 ? 1 : -1) * params.sideMultiplier; float sign = (i == 0 ? 1 : -1) * params.sideMultiplier;
Tmp.v1.set((x + mx) * sign, y + my).rotateRadExact((params.rotation - 90) * Mathf.degRad); Tmp.v1.set((x + mx) * sign, y + my).rotateRadExact((params.rotation - 90) * Mathf.degRad);
Draw.xscl *= sign;
if(originX != 0f || originY != 0f){
//correct for offset caused by origin shift
Tmp.v1.sub(Tmp.v2.set(-originX * Draw.xscl, -originY * Draw.yscl).rotate(params.rotation - 90f).add(originX * Draw.xscl, originY * Draw.yscl));
}
float float
rx = params.x + Tmp.v1.x, rx = params.x + Tmp.v1.x,
ry = params.y + Tmp.v1.y, ry = params.y + Tmp.v1.y,
rot = mr * sign + params.rotation - 90; rot = mr * sign + params.rotation - 90;
Draw.xscl *= sign;
if(outline && drawRegion){ if(outline && drawRegion){
Draw.z(prevZ + outlineLayerOffset); Draw.z(prevZ + outlineLayerOffset);
Draw.rect(outlines[Math.min(i, regions.length - 1)], rx, ry, rot); rect(outlines[Math.min(i, regions.length - 1)], rx, ry, rot);
Draw.z(prevZ); Draw.z(prevZ);
} }
@ -126,7 +134,7 @@ public class RegionPart extends DrawPart{
} }
Draw.blend(blending); Draw.blend(blending);
Draw.rect(region, rx, ry, rot); rect(region, rx, ry, rot);
Draw.blend(); Draw.blend();
if(color != null) Draw.color(); if(color != null) Draw.color();
} }
@ -134,7 +142,7 @@ public class RegionPart extends DrawPart{
if(heat.found()){ if(heat.found()){
float hprog = heatProgress.getClamp(params, clampProgress); float hprog = heatProgress.getClamp(params, clampProgress);
heatColor.write(Tmp.c1).a(hprog * heatColor.a); heatColor.write(Tmp.c1).a(hprog * heatColor.a);
Drawf.additive(heat, Tmp.c1, rx, ry, rot, turretShading ? turretHeatLayer : Draw.z() + heatLayerOffset); Drawf.additive(heat, Tmp.c1, 1f, rx, ry, rot, turretShading ? turretHeatLayer : Draw.z() + heatLayerOffset, originX, originY);
if(heatLight) Drawf.light(rx, ry, light.found() ? light : heat, rot, Tmp.c1, heatLightOpacity * hprog); if(heatLight) Drawf.light(rx, ry, light.found() ? light : heat, rot, Tmp.c1, heatLightOpacity * hprog);
} }
@ -167,6 +175,11 @@ public class RegionPart extends DrawPart{
Draw.scl(preXscl, preYscl); Draw.scl(preXscl, preYscl);
} }
void rect(TextureRegion region, float x, float y, float rotation){
float w = region.width * region.scl() * Draw.xscl, h = region.height * region.scl() * Draw.yscl;
Draw.rect(region, x, y, w, h, w / 2f + originX * Draw.xscl, h / 2f + originY * Draw.yscl, rotation);
}
@Override @Override
public void load(String name){ public void load(String name){
String realName = this.name == null ? name + suffix : this.name; String realName = this.name == null ? name + suffix : this.name;

View file

@ -6,6 +6,20 @@ import arc.util.*;
public class ShootHelix extends ShootPattern{ public class ShootHelix extends ShootPattern{
public float scl = 2f, mag = 1.5f, offset = Mathf.PI * 1.25f; public float scl = 2f, mag = 1.5f, offset = Mathf.PI * 1.25f;
public ShootHelix(float scl, float mag){
this.scl = scl;
this.mag = mag;
}
public ShootHelix(float scl, float mag, float offset){
this.scl = scl;
this.mag = mag;
this.offset = offset;
}
public ShootHelix(){
}
@Override @Override
public void shoot(int totalShots, BulletHandler handler, @Nullable Runnable barrelIncrementer){ public void shoot(int totalShots, BulletHandler handler, @Nullable Runnable barrelIncrementer){
for(int i = 0; i < shots; i++){ for(int i = 0; i < shots; i++){

View file

@ -14,6 +14,10 @@ public class ShootSpread extends ShootPattern{
public ShootSpread(){ public ShootSpread(){
} }
public static ShootSpread circle(int points){
return new ShootSpread(points, 360f / points);
}
@Override @Override
public void shoot(int totalShots, BulletHandler handler, @Nullable Runnable barrelIncrementer){ public void shoot(int totalShots, BulletHandler handler, @Nullable Runnable barrelIncrementer){
for(int i = 0; i < shots; i++){ for(int i = 0; i < shots; i++){

View file

@ -21,11 +21,12 @@ public class AIController implements UnitController{
protected Unit unit; protected Unit unit;
protected Interval timer = new Interval(4); protected Interval timer = new Interval(4);
protected AIController fallback; protected @Nullable AIController fallback;
protected float noTargetTime; protected float noTargetTime;
/** main target that is being faced */ /** main target that is being faced */
protected Teamc target; protected @Nullable Teamc target;
protected @Nullable Teamc bomberTarget;
{ {
resetTimers(); resetTimers();
@ -124,15 +125,27 @@ public class AIController implements UnitController{
} }
public void pathfind(int pathTarget){ public void pathfind(int pathTarget){
int costType = unit.pathType(); pathfind(pathTarget, true);
}
public void pathfind(int pathTarget, boolean stopAtTargetTile){
int costType = unit.type.flowfieldPathType;
Tile tile = unit.tileOn(); Tile tile = unit.tileOn();
if(tile == null) return; if(tile == null) return;
Tile targetTile = pathfinder.getTargetTile(tile, pathfinder.getField(unit.team, costType, pathTarget)); Tile targetTile = pathfinder.getField(unit.team, costType, pathTarget).getNextTile(tile);
if(tile == targetTile || !unit.canPass(targetTile.x, targetTile.y)) return; if((tile == targetTile && stopAtTargetTile) || !unit.canPass(targetTile.x, targetTile.y)) return;
unit.movePref(vec.trns(unit.angleTo(targetTile.worldx(), targetTile.worldy()), prefSpeed())); unit.movePref(alterPathfind(vec.set(targetTile.worldx(), targetTile.worldy()).sub(tile.worldx(), tile.worldy()).setLength(prefSpeed())));
}
public Vec2 alterPathfind(Vec2 vec){
return vec;
}
public void targetInvalidated(){
//TODO: try this for normal units, reset the target timer
} }
public void updateWeapons(){ public void updateWeapons(){
@ -146,6 +159,9 @@ public class AIController implements UnitController{
noTargetTime += Time.delta; noTargetTime += Time.delta;
if(invalid(target)){ if(invalid(target)){
if(target != null && !target.isAdded()){
targetInvalidated();
}
target = null; target = null;
}else{ }else{
noTargetTime = 0f; noTargetTime = 0f;
@ -185,6 +201,13 @@ public class AIController implements UnitController{
if(mount.target != null){ if(mount.target != null){
shoot = mount.target.within(mountX, mountY, wrange + (mount.target instanceof Sized s ? s.hitSize()/2f : 0f)) && shouldShoot(); shoot = mount.target.within(mountX, mountY, wrange + (mount.target instanceof Sized s ? s.hitSize()/2f : 0f)) && shouldShoot();
if(unit.type.autoDropBombs && !shoot){
if(bomberTarget == null || !bomberTarget.isAdded() || !bomberTarget.within(unit, unit.hitSize/2f + ((Sized)bomberTarget).hitSize()/2f)){
bomberTarget = Units.closestTarget(unit.team, unit.x, unit.y, unit.hitSize, u -> !u.isFlying(), t -> true);
}
shoot = bomberTarget != null;
}
Vec2 to = Predict.intercept(unit, mount.target, weapon.bullet.speed); Vec2 to = Predict.intercept(unit, mount.target, weapon.bullet.speed);
mount.aimX = to.x; mount.aimX = to.x;
mount.aimY = to.y; mount.aimY = to.y;

View file

@ -31,8 +31,4 @@ public interface UnitController{
default void afterRead(Unit unit){ default void afterRead(Unit unit){
} }
default boolean isBeingControlled(Unit player){
return false;
}
} }

View file

@ -9,6 +9,7 @@ import mindustry.net.*;
import mindustry.net.Packets.*; import mindustry.net.Packets.*;
import mindustry.type.*; import mindustry.type.*;
import mindustry.world.*; import mindustry.world.*;
import mindustry.world.blocks.environment.*;
import mindustry.world.blocks.storage.CoreBlock.*; import mindustry.world.blocks.storage.CoreBlock.*;
public class EventType{ public class EventType{
@ -82,6 +83,8 @@ public class EventType{
public static class BlockInfoEvent{} public static class BlockInfoEvent{}
/** Called *after* all content has been initialized. */ /** Called *after* all content has been initialized. */
public static class ContentInitEvent{} public static class ContentInitEvent{}
/** Called *after* all content has been added to the atlas, but before its pixmaps are disposed. */
public static class AtlasPackEvent{}
/** Called *after* all mod content has been loaded, but before it has been initialized. */ /** Called *after* all mod content has been loaded, but before it has been initialized. */
public static class ModContentLoadEvent{} public static class ModContentLoadEvent{}
/** Called when the client game is first loaded. */ /** Called when the client game is first loaded. */
@ -395,6 +398,22 @@ public class EventType{
} }
} }
/**
* Called when a tile changes its floor. Do not cache or use with a timer.
* Do not modify any tiles inside listener code.
* */
public static class TileFloorChangeEvent{
public Tile tile;
public Floor previous, floor;
public TileFloorChangeEvent set(Tile tile, Floor previous, Floor floor){
this.tile = tile;
this.previous = previous;
this.floor = floor;
return this;
}
}
/** /**
* Called after a building's team changes. * Called after a building's team changes.
* Event object is reused, do not nest! * Event object is reused, do not nest!

View file

@ -36,6 +36,7 @@ public final class FogControl implements CustomChunk{
private boolean justLoaded = false; private boolean justLoaded = false;
private boolean loadedStatic = false; private boolean loadedStatic = false;
private int lastEntityUpdateIndex = 0;
public FogControl(){ public FogControl(){
Events.on(ResetEvent.class, e -> { Events.on(ResetEvent.class, e -> {
@ -131,6 +132,7 @@ public final class FogControl implements CustomChunk{
} }
void stop(){ void stop(){
lastEntityUpdateIndex = 0;
fog = null; fog = null;
//I don't care whether the fog thread crashes here, it's about to die anyway //I don't care whether the fog thread crashes here, it's about to die anyway
staticEvents.clear(); staticEvents.clear();
@ -214,6 +216,31 @@ public final class FogControl implements CustomChunk{
//clear to prepare for queuing fog radius from units and buildings //clear to prepare for queuing fog radius from units and buildings
dynamicEventQueue.clear(); dynamicEventQueue.clear();
//update fog visibility manually
if(state.rules.fog && !headless && Groups.build.size() > 0){
int size = Groups.build.size();
int chunkSize = 5; //fraction of entity list to iterate each frame
int chunks = Math.min(chunkSize, size);
int iterated = Math.max(1, size / chunks);
int steps = 0;
int i = lastEntityUpdateIndex % size;
while(steps < iterated){
Groups.build.index(i).updateFogVisibility();
steps ++;
i ++;
if(i >= size){
i = 0;
}
}
lastEntityUpdateIndex = i;
}
for(var team : state.teams.present){ for(var team : state.teams.present){
//AI teams do not have fog //AI teams do not have fog
if(!team.team.isOnlyAI()){ if(!team.team.isOnlyAI()){

View file

@ -277,6 +277,11 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
String className = getClass().getSimpleName().replace("Objective", ""); String className = getClass().getSimpleName().replace("Objective", "");
return Core.bundle == null ? className : Core.bundle.get("objective." + className.toLowerCase() + ".name", className); return Core.bundle == null ? className : Core.bundle.get("objective." + className.toLowerCase() + ".name", className);
} }
/** Validate fields after reading to make sure none of them are null. */
public void validate(){
}
} }
/** Research a specific piece of content in the tech tree. */ /** Research a specific piece of content in the tech tree. */
@ -298,6 +303,11 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
public String text(){ public String text(){
return Core.bundle.format("objective.research", content.emoji(), content.localizedName); return Core.bundle.format("objective.research", content.emoji(), content.localizedName);
} }
@Override
public void validate(){
if(content == null) content = Items.copper;
}
} }
/** Produce a specific piece of content in the tech tree (essentially research with different text). */ /** Produce a specific piece of content in the tech tree (essentially research with different text). */
@ -319,6 +329,11 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
public String text(){ public String text(){
return Core.bundle.format("objective.produce", content.emoji(), content.localizedName); return Core.bundle.format("objective.produce", content.emoji(), content.localizedName);
} }
@Override
public void validate(){
if(content == null) content = Items.copper;
}
} }
/** Have a certain amount of item in your core. */ /** Have a certain amount of item in your core. */
@ -342,6 +357,11 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
public String text(){ public String text(){
return Core.bundle.format("objective.item", state.rules.defaultTeam.items().get(item), amount, item.emoji(), item.localizedName); return Core.bundle.format("objective.item", state.rules.defaultTeam.items().get(item), amount, item.emoji(), item.localizedName);
} }
@Override
public void validate(){
if(item == null) item = Items.copper;
}
} }
/** Get a certain item in your core (through a block, not manually.) */ /** Get a certain item in your core (through a block, not manually.) */
@ -365,6 +385,11 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
public String text(){ public String text(){
return Core.bundle.format("objective.coreitem", state.stats.coreItemCount.get(item), amount, item.emoji(), item.localizedName); return Core.bundle.format("objective.coreitem", state.stats.coreItemCount.get(item), amount, item.emoji(), item.localizedName);
} }
@Override
public void validate(){
if(item == null) item = Items.copper;
}
} }
/** Build a certain amount of a block. */ /** Build a certain amount of a block. */
@ -388,6 +413,11 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
public String text(){ public String text(){
return Core.bundle.format("objective.build", count - state.stats.placedBlockCount.get(block, 0), block.emoji(), block.localizedName); return Core.bundle.format("objective.build", count - state.stats.placedBlockCount.get(block, 0), block.emoji(), block.localizedName);
} }
@Override
public void validate(){
if(block == null) block = Blocks.conveyor;
}
} }
/** Produce a certain amount of a unit. */ /** Produce a certain amount of a unit. */
@ -411,6 +441,11 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
public String text(){ public String text(){
return Core.bundle.format("objective.buildunit", count - state.rules.defaultTeam.data().countType(unit), unit.emoji(), unit.localizedName); return Core.bundle.format("objective.buildunit", count - state.rules.defaultTeam.data().countType(unit), unit.emoji(), unit.localizedName);
} }
@Override
public void validate(){
if(unit == null) unit = UnitTypes.dagger;
}
} }
/** Produce a certain amount of units. */ /** Produce a certain amount of units. */
@ -524,6 +559,11 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
public String text(){ public String text(){
return Core.bundle.format("objective.destroyblock", block.emoji(), block.localizedName); return Core.bundle.format("objective.destroyblock", block.emoji(), block.localizedName);
} }
@Override
public void validate(){
if(block == null) block = Blocks.router;
}
} }
public static class DestroyBlocksObjective extends MapObjective{ public static class DestroyBlocksObjective extends MapObjective{
@ -559,6 +599,11 @@ public class MapObjectives implements Iterable<MapObjective>, Eachable<MapObject
public String text(){ public String text(){
return Core.bundle.format("objective.destroyblocks", progress(), positions.length, block.emoji(), block.localizedName); return Core.bundle.format("objective.destroyblocks", progress(), positions.length, block.emoji(), block.localizedName);
} }
@Override
public void validate(){
if(block == null) block = Blocks.router;
}
} }
/** Command any unit to do anything. Always compete in headless mode. */ /** Command any unit to do anything. Always compete in headless mode. */

View file

@ -1,11 +1,11 @@
package mindustry.game; package mindustry.game;
import arc.func.*;
import arc.struct.*; import arc.struct.*;
import arc.util.*; import arc.util.*;
import arc.util.serialization.*; import arc.util.serialization.*;
import arc.util.serialization.Json.*; import arc.util.serialization.Json.*;
import mindustry.content.*; import mindustry.content.*;
import mindustry.ctype.*;
import mindustry.gen.*; import mindustry.gen.*;
import mindustry.io.versions.*; import mindustry.io.versions.*;
import mindustry.type.*; import mindustry.type.*;
@ -48,6 +48,8 @@ public class SpawnGroup implements JsonSerializable, Cloneable{
public @Nullable StatusEffect effect; public @Nullable StatusEffect effect;
/** Items this unit spawns with. Null to disable. */ /** Items this unit spawns with. Null to disable. */
public @Nullable ItemStack items; public @Nullable ItemStack items;
/** Team that units spawned use. Null for default wave team. */
public @Nullable Team team;
public SpawnGroup(UnitType type){ public SpawnGroup(UnitType type){
this.type = type; this.type = type;
@ -75,12 +77,9 @@ public class SpawnGroup implements JsonSerializable, Cloneable{
return Math.max(shields + shieldScaling*(wave - begin), 0); return Math.max(shields + shieldScaling*(wave - begin), 0);
} }
/** /** Creates a unit, and assigns correct values based on this group's data. */
* Creates a unit, and assigns correct values based on this group's data. public Unit createUnit(Team team, float x, float y, float rotation, int wave, Cons<Unit> cons){
* This method does not add() the unit. Unit unit = type.spawn(team, x, y, rotation, cons);
*/
public Unit createUnit(Team team, int wave){
Unit unit = type.create(team);
if(effect != null){ if(effect != null){
unit.apply(effect, 999999f); unit.apply(effect, 999999f);
@ -104,6 +103,11 @@ public class SpawnGroup implements JsonSerializable, Cloneable{
return unit; return unit;
} }
/** Creates a unit, and assigns correct values based on this group's data. */
public Unit createUnit(Team team, int wave){
return createUnit(team, 0f, 0f, 0f, wave, u -> {});
}
@Override @Override
public void write(Json json){ public void write(Json json){
if(type == null) type = UnitTypes.dagger; if(type == null) type = UnitTypes.dagger;
@ -120,7 +124,7 @@ public class SpawnGroup implements JsonSerializable, Cloneable{
if(spawn != -1) json.writeValue("spawn", spawn); if(spawn != -1) json.writeValue("spawn", spawn);
if(payloads != null && payloads.any()) json.writeValue("payloads", payloads.map(u -> u.name).toArray(String.class)); if(payloads != null && payloads.any()) json.writeValue("payloads", payloads.map(u -> u.name).toArray(String.class));
if(items != null && items.amount > 0) json.writeValue("items", items); if(items != null && items.amount > 0) json.writeValue("items", items);
if(team != null) json.writeValue("team", team.id);
} }
@Override @Override
@ -140,6 +144,7 @@ public class SpawnGroup implements JsonSerializable, Cloneable{
spawn = data.getInt("spawn", -1); spawn = data.getInt("spawn", -1);
if(data.has("payloads")) payloads = Seq.with(json.readValue(String[].class, data.get("payloads"))).map(content::unit).removeAll(t -> t == null); if(data.has("payloads")) payloads = Seq.with(json.readValue(String[].class, data.get("payloads"))).map(content::unit).removeAll(t -> t == null);
if(data.has("items")) items = json.readValue(ItemStack.class, data.get("items")); if(data.has("items")) items = json.readValue(ItemStack.class, data.get("items"));
if(data.has("team")) team = Team.get(data.getInt("team"));
//old boss effect ID //old boss effect ID

View file

@ -19,6 +19,7 @@ public class Team implements Comparable<Team>, Senseable{
public final Color color = new Color(); public final Color color = new Color();
public final Color[] palette = {new Color(), new Color(), new Color()}; public final Color[] palette = {new Color(), new Color(), new Color()};
public final int[] palettei = new int[3]; public final int[] palettei = new int[3];
public boolean ignoreUnitCap = false;
public String emoji = ""; public String emoji = "";
public boolean hasPalette; public boolean hasPalette;
public String name; public String name;
@ -50,6 +51,8 @@ public class Team implements Comparable<Team>, Senseable{
new Team(i, "team#" + i, Color.HSVtoRGB(360f * Mathf.random(), 100f * Mathf.random(0.4f, 1f), 100f * Mathf.random(0.6f, 1f), 1f)); new Team(i, "team#" + i, Color.HSVtoRGB(360f * Mathf.random(), 100f * Mathf.random(0.4f, 1f), 100f * Mathf.random(0.6f, 1f), 1f));
} }
Mathf.rand.setSeed(new Rand().nextLong()); Mathf.rand.setSeed(new Rand().nextLong());
neoplastic.ignoreUnitCap = true;
} }
public static Team get(int id){ public static Team get(int id){
@ -95,10 +98,16 @@ public class Team implements Comparable<Team>, Senseable{
return data().core(); return data().core();
} }
/** @return whether this team has any buildings on this map; in waves mode, this is always true for the enemy team. */
public boolean active(){ public boolean active(){
return state.teams.isActive(this); return state.teams.isActive(this);
} }
/** @return whether this team has any active cores. Not the same as active()! */
public boolean isAlive(){
return data().isAlive();
}
/** @return whether this team is supposed to be AI-controlled. */ /** @return whether this team is supposed to be AI-controlled. */
public boolean isAI(){ public boolean isAI(){
return (state.rules.waves || state.rules.attackMode) && this != state.rules.defaultTeam && !state.rules.pvp; return (state.rules.waves || state.rules.attackMode) && this != state.rules.defaultTeam && !state.rules.pvp;
@ -114,12 +123,6 @@ public class Team implements Comparable<Team>, Senseable{
return isAI() && !rules().rtsAi; return isAI() && !rules().rtsAi;
} }
/** @deprecated There is absolutely no reason to use this. */
@Deprecated
public boolean isEnemy(Team other){
return this != other;
}
public Seq<CoreBuild> cores(){ public Seq<CoreBuild> cores(){
return state.teams.cores(this); return state.teams.cores(this);
} }
@ -161,6 +164,7 @@ public class Team implements Comparable<Team>, Senseable{
@Override @Override
public double sense(LAccess sensor){ public double sense(LAccess sensor){
if(sensor == LAccess.id) return id; if(sensor == LAccess.id) return id;
return 0; if(sensor == LAccess.color) return color.toDoubleBits();
return Double.NaN;
} }
} }

View file

@ -126,6 +126,15 @@ public class Teams{
return active; return active;
} }
public void updateActive(Team team){
TeamData data = get(team);
//register in active list if needed
if(data.active() && !active.contains(data)){
active.add(data);
updateEnemies();
}
}
public void registerCore(CoreBuild core){ public void registerCore(CoreBuild core){
TeamData data = get(core.team); TeamData data = get(core.team);
//add core if not present //add core if not present
@ -407,13 +416,18 @@ public class Teams{
} }
public boolean active(){ public boolean active(){
return (team == state.rules.waveTeam && state.rules.waves) || cores.size > 0; return (team == state.rules.waveTeam && state.rules.waves) || cores.size > 0 || buildings.size > 0 || (team == Team.neoplastic && units.size > 0);
} }
public boolean hasCore(){ public boolean hasCore(){
return cores.size > 0; return cores.size > 0;
} }
/** @return whether this team has any cores (standard team), or any hearts (neoplasm). */
public boolean isAlive(){
return hasCore();
}
public boolean noCores(){ public boolean noCores(){
return cores.isEmpty(); return cores.isEmpty();
} }

View file

@ -49,6 +49,7 @@ public class BlockRenderer{
private IntSet procLinks = new IntSet(), procLights = new IntSet(); private IntSet procLinks = new IntSet(), procLights = new IntSet();
private BlockQuadtree blockTree = new BlockQuadtree(new Rect(0, 0, 1, 1)); private BlockQuadtree blockTree = new BlockQuadtree(new Rect(0, 0, 1, 1));
private BlockLightQuadtree blockLightTree = new BlockLightQuadtree(new Rect(0, 0, 1, 1));
private FloorQuadtree floorTree = new FloorQuadtree(new Rect(0, 0, 1, 1)); private FloorQuadtree floorTree = new FloorQuadtree(new Rect(0, 0, 1, 1));
public BlockRenderer(){ public BlockRenderer(){
@ -64,7 +65,9 @@ public class BlockRenderer{
Events.on(WorldLoadEvent.class, event -> { Events.on(WorldLoadEvent.class, event -> {
blockTree = new BlockQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight())); blockTree = new BlockQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight()));
blockLightTree = new BlockLightQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight()));
floorTree = new FloorQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight())); floorTree = new FloorQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight()));
shadowEvents.clear(); shadowEvents.clear();
updateFloors.clear(); updateFloors.clear();
lastCamY = lastCamX = -99; //invalidate camera position so blocks get updated lastCamY = lastCamX = -99; //invalidate camera position so blocks get updated
@ -93,7 +96,7 @@ public class BlockRenderer{
tile.build.wasVisible = true; tile.build.wasVisible = true;
} }
if(tile.block().hasShadow && (tile.build == null || tile.build.wasVisible)){ if(tile.block().displayShadow(tile) && (tile.build == null || tile.build.wasVisible)){
Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1); Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1);
} }
} }
@ -116,7 +119,10 @@ public class BlockRenderer{
Events.on(TilePreChangeEvent.class, event -> { Events.on(TilePreChangeEvent.class, event -> {
if(blockTree == null || floorTree == null) return; if(blockTree == null || floorTree == null) return;
if(indexBlock(event.tile)) blockTree.remove(event.tile); if(indexBlock(event.tile)){
blockTree.remove(event.tile);
blockLightTree.remove(event.tile);
}
if(indexFloor(event.tile)) floorTree.remove(event.tile); if(indexFloor(event.tile)) floorTree.remove(event.tile);
}); });
@ -209,7 +215,10 @@ public class BlockRenderer{
} }
void recordIndex(Tile tile){ void recordIndex(Tile tile){
if(indexBlock(tile)) blockTree.insert(tile); if(indexBlock(tile)){
blockTree.insert(tile);
blockLightTree.insert(tile);
}
if(indexFloor(tile)) floorTree.insert(tile); if(indexFloor(tile)) floorTree.insert(tile);
} }
@ -295,7 +304,7 @@ public class BlockRenderer{
for(Tile tile : shadowEvents){ for(Tile tile : shadowEvents){
if(tile == null) continue; if(tile == null) continue;
//draw white/shadow color depending on blend //draw white/shadow color depending on blend
Draw.color((!tile.block().hasShadow || (state.rules.fog && tile.build != null && !tile.build.wasVisible)) ? Color.white : blendShadowColor); Draw.color((!tile.block().displayShadow(tile) || (state.rules.fog && tile.build != null && !tile.build.wasVisible)) ? Color.white : blendShadowColor);
Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1); Fill.rect(tile.x + 0.5f, tile.y + 0.5f, 1, 1);
} }
@ -354,16 +363,17 @@ public class BlockRenderer{
//draw floor lights //draw floor lights
floorTree.intersect(bounds, lightview::add); floorTree.intersect(bounds, lightview::add);
blockLightTree.intersect(bounds, tile -> {
if(tile.block().emitLight && (tile.build == null || procLights.add(tile.build.pos()))){
lightview.add(tile);
}
});
blockTree.intersect(bounds, tile -> { blockTree.intersect(bounds, tile -> {
if(tile.build == null || procLinks.add(tile.build.id)){ if(tile.build == null || procLinks.add(tile.build.id)){
tileview.add(tile); 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){ if(tile.build != null && tile.build.power != null && tile.build.power.links.size > 0){
for(Building other : tile.build.getPowerConnections(outArray2)){ for(Building other : tile.build.getPowerConnections(outArray2)){
if(other.block instanceof PowerNode && procLinks.add(other.id)){ //TODO need a generic way to render connections! if(other.block instanceof PowerNode && procLinks.add(other.id)){ //TODO need a generic way to render connections!
@ -519,6 +529,24 @@ public class BlockRenderer{
} }
} }
static class BlockLightQuadtree extends QuadTree<Tile>{
public BlockLightQuadtree(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.lightClipSize, block.lightClipSize);
}
@Override
protected QuadTree<Tile> newChild(Rect rect){
return new BlockLightQuadtree(rect);
}
}
static class FloorQuadtree extends QuadTree<Tile>{ static class FloorQuadtree extends QuadTree<Tile>{
public FloorQuadtree(Rect bounds){ public FloorQuadtree(Rect bounds){
@ -528,7 +556,7 @@ public class BlockRenderer{
@Override @Override
public void hitbox(Tile tile){ public void hitbox(Tile tile){
var floor = tile.floor(); var floor = tile.floor();
tmp.setCentered(tile.worldx(), tile.worldy(), floor.clipSize, floor.clipSize); tmp.setCentered(tile.worldx(), tile.worldy(), floor.lightClipSize, floor.lightClipSize);
} }
@Override @Override

View file

@ -17,6 +17,7 @@ public class CacheLayer{
public static CacheLayer[] all = {}; public static CacheLayer[] all = {};
public int id; public int id;
public boolean liquid;
/** Registers cache layers that will render before the 'normal' layer. */ /** Registers cache layers that will render before the 'normal' layer. */
public static void add(CacheLayer... layers){ public static void add(CacheLayer... layers){
@ -66,7 +67,7 @@ public class CacheLayer{
slag = new ShaderLayer(Shaders.slag), slag = new ShaderLayer(Shaders.slag),
arkycite = new ShaderLayer(Shaders.arkycite), arkycite = new ShaderLayer(Shaders.arkycite),
cryofluid = new ShaderLayer(Shaders.cryofluid), cryofluid = new ShaderLayer(Shaders.cryofluid),
space = new ShaderLayer(Shaders.space), space = new ShaderLayer(Shaders.space, false),
normal = new CacheLayer(), normal = new CacheLayer(),
walls = new CacheLayer() walls = new CacheLayer()
); );
@ -86,7 +87,11 @@ public class CacheLayer{
public @Nullable Shader shader; public @Nullable Shader shader;
public ShaderLayer(Shader shader){ public ShaderLayer(Shader shader){
//shader will be null on headless backend, but that's ok this(shader, true);
}
public ShaderLayer(@Nullable Shader shader, boolean liquid){
this.liquid = liquid;
this.shader = shader; this.shader = shader;
} }
@ -94,7 +99,6 @@ public class CacheLayer{
public void begin(){ public void begin(){
if(!Core.settings.getBool("animatedwater")) return; if(!Core.settings.getBool("animatedwater")) return;
renderer.blocks.floor.endc();
renderer.effectBuffer.begin(); renderer.effectBuffer.begin();
Core.graphics.clear(Color.clear); Core.graphics.clear(Color.clear);
renderer.blocks.floor.beginc(); renderer.blocks.floor.beginc();
@ -104,11 +108,8 @@ public class CacheLayer{
public void end(){ public void end(){
if(!Core.settings.getBool("animatedwater")) return; if(!Core.settings.getBool("animatedwater")) return;
renderer.blocks.floor.endc();
renderer.effectBuffer.end(); renderer.effectBuffer.end();
renderer.effectBuffer.blit(shader); renderer.effectBuffer.blit(shader);
renderer.blocks.floor.beginc(); renderer.blocks.floor.beginc();
} }
} }

View file

@ -26,6 +26,10 @@ public class Drawf{
} }
} }
public static void underwater(Runnable run){
renderer.blocks.floor.drawUnderwater(run);
}
//TODO offset unused //TODO offset unused
public static void flame(float x, float y, int divisions, float rotation, float length, float width, float pan){ public static void flame(float x, float y, int divisions, float rotation, float length, float width, float pan){
float len1 = length * pan, len2 = length * (1f - pan); float len1 = length * pan, len2 = length * (1f - pan);
@ -130,6 +134,17 @@ public class Drawf{
additive(region, color, 1f, x, y, rotation, layer); additive(region, color, 1f, x, y, rotation, layer);
} }
public static void additive(TextureRegion region, Color color, float alpha, float x, float y, float width, float height, float layer){
float pz = Draw.z();
Draw.z(layer);
Draw.color(color, alpha * color.a);
Draw.blend(Blending.additive);
Draw.rect(region, x, y, width, height, 0f);
Draw.blend();
Draw.color();
Draw.z(pz);
}
public static void additive(TextureRegion region, Color color, float alpha, float x, float y, float rotation, float layer){ public static void additive(TextureRegion region, Color color, float alpha, float x, float y, float rotation, float layer){
float pz = Draw.z(); float pz = Draw.z();
Draw.z(layer); Draw.z(layer);
@ -141,6 +156,17 @@ public class Drawf{
Draw.z(pz); Draw.z(pz);
} }
public static void additive(TextureRegion region, Color color, float alpha, float x, float y, float rotation, float layer, float originX, float originY){
float pz = Draw.z(), w = region.width * region.scl() * Draw.xscl, h = region.height * region.scl() * Draw.yscl;
Draw.z(layer);
Draw.color(color, alpha * color.a);
Draw.blend(Blending.additive);
Draw.rect(region, x, y, w, h, w / 2f + originX * region.scl() * Draw.xscl, h / 2f + originY * region.scl() * Draw.yscl, rotation);
Draw.blend();
Draw.color();
Draw.z(pz);
}
public static void limitLine(Position start, Position dest, float len1, float len2, Color color){ public static void limitLine(Position start, Position dest, float len1, float len2, Color color){
if(start.within(dest, len1 + len2)){ if(start.within(dest, len1 + len2)){
return; return;
@ -267,7 +293,7 @@ public class Drawf{
} }
public static void selected(Building tile, Color color){ public static void selected(Building tile, Color color){
selected(tile.tile(), color); selected(tile.tile, color);
} }
public static void selected(Tile tile, Color color){ public static void selected(Tile tile, Color color){

View file

@ -42,21 +42,29 @@ public class FloorRenderer{
private static final boolean dynamic = false; private static final boolean dynamic = false;
private float[] vertices = new float[maxSprites * vertexSize * 4]; private float[] vertices = new float[maxSprites * vertexSize * 4];
private short[] indices = new short[maxSprites * 6];
private int vidx; private int vidx;
private FloorRenderBatch batch = new FloorRenderBatch(); private FloorRenderBatch batch = new FloorRenderBatch();
private Shader shader; private Shader shader;
private Texture texture; private Texture texture;
private TextureRegion error; private TextureRegion error;
private Mesh[][][] cache; private IndexData indexData;
private ChunkMesh[][][] cache;
private IntSet drawnLayerSet = new IntSet(); private IntSet drawnLayerSet = new IntSet();
private IntSet recacheSet = new IntSet(); private IntSet recacheSet = new IntSet();
private IntSeq drawnLayers = new IntSeq(); private IntSeq drawnLayers = new IntSeq();
private ObjectSet<CacheLayer> used = new ObjectSet<>(); private ObjectSet<CacheLayer> used = new ObjectSet<>();
private Seq<Runnable> underwaterDraw = new Seq<>(Runnable.class);
//alpha value of pixels cannot exceed the alpha of the surface they're being drawn on
private Blending underwaterBlend = new Blending(
Gl.srcAlpha, Gl.oneMinusSrcAlpha,
Gl.dstAlpha, Gl.oneMinusSrcAlpha
);
public FloorRenderer(){ public FloorRenderer(){
short j = 0; short j = 0;
short[] indices = new short[maxSprites * 6];
for(int i = 0; i < indices.length; i += 6, j += 4){ for(int i = 0; i < indices.length; i += 6, j += 4){
indices[i] = j; indices[i] = j;
indices[i + 1] = (short)(j + 1); indices[i + 1] = (short)(j + 1);
@ -66,6 +74,14 @@ public class FloorRenderer{
indices[i + 5] = j; indices[i + 5] = j;
} }
indexData = new IndexBufferObject(true, indices.length){
@Override
public void dispose(){
//there is never a need to dispose this index buffer
}
};
indexData.set(indices, 0, indices.length);
shader = new Shader( shader = new Shader(
""" """
attribute vec4 a_position; attribute vec4 a_position;
@ -121,6 +137,8 @@ public class FloorRenderer{
drawnLayers.clear(); drawnLayers.clear();
drawnLayerSet.clear(); drawnLayerSet.clear();
Rect bounds = camera.bounds(Tmp.r3);
//preliminary layer check //preliminary layer check
for(int x = minx; x <= maxx; x++){ for(int x = minx; x <= maxx; x++){
for(int y = miny; y <= maxy; y++){ for(int y = miny; y <= maxy; y++){
@ -131,11 +149,11 @@ public class FloorRenderer{
cacheChunk(x, y); cacheChunk(x, y);
} }
Mesh[] chunk = cache[x][y]; ChunkMesh[] chunk = cache[x][y];
//loop through all layers, and add layer index if it exists //loop through all layers, and add layer index if it exists
for(int i = 0; i < layers; i++){ for(int i = 0; i < layers; i++){
if(chunk[i] != null && i != CacheLayer.walls.id){ if(chunk[i] != null && i != CacheLayer.walls.id && chunk[i].bounds.overlaps(bounds)){
drawnLayerSet.add(i); drawnLayerSet.add(i);
} }
} }
@ -149,16 +167,13 @@ public class FloorRenderer{
drawnLayers.sort(); drawnLayers.sort();
Draw.flush();
beginDraw(); beginDraw();
for(int i = 0; i < drawnLayers.size; i++){ for(int i = 0; i < drawnLayers.size; i++){
CacheLayer layer = CacheLayer.all[drawnLayers.get(i)]; drawLayer(CacheLayer.all[drawnLayers.get(i)]);
drawLayer(layer);
} }
endDraw(); underwaterDraw.clear();
} }
public void beginc(){ public void beginc(){
@ -167,29 +182,6 @@ public class FloorRenderer{
//only ever use the base environment texture //only ever use the base environment texture
texture.bind(0); texture.bind(0);
//enable all mesh attributes; TODO remove once the attribute cache bug is fixed
if(Core.gl30 == null){
for(VertexAttribute attribute : attributes){
int loc = shader.getAttributeLocation(attribute.alias);
if(loc != -1) Gl.enableVertexAttribArray(loc);
}
}
}
public void endc(){
//disable all mesh attributes; TODO remove once the attribute cache bug is fixed
if(Core.gl30 == null){
for(VertexAttribute attribute : attributes){
int loc = shader.getAttributeLocation(attribute.alias);
if(loc != -1) Gl.disableVertexAttribArray(loc);
}
}
//unbind last buffer
Gl.bindBuffer(Gl.arrayBuffer, 0);
Gl.bindBuffer(Gl.elementArrayBuffer, 0);
} }
public void checkChanges(){ public void checkChanges(){
@ -205,6 +197,10 @@ public class FloorRenderer{
} }
} }
public void drawUnderwater(Runnable run){
underwaterDraw.add(run);
}
public void beginDraw(){ public void beginDraw(){
if(cache == null){ if(cache == null){
return; return;
@ -217,14 +213,6 @@ public class FloorRenderer{
Gl.enable(Gl.blend); Gl.enable(Gl.blend);
} }
public void endDraw(){
if(cache == null){
return;
}
endc();
}
public void drawLayer(CacheLayer layer){ public void drawLayer(CacheLayer layer){
if(cache == null){ if(cache == null){
return; return;
@ -240,6 +228,8 @@ public class FloorRenderer{
layer.begin(); layer.begin();
Rect bounds = camera.bounds(Tmp.r3);
for(int x = minx; x <= maxx; x++){ for(int x = minx; x <= maxx; x++){
for(int y = miny; y <= maxy; y++){ for(int y = miny; y <= maxy; y++){
@ -249,33 +239,29 @@ public class FloorRenderer{
var mesh = cache[x][y][layer.id]; var mesh = cache[x][y][layer.id];
//this *must* be a vertexbufferobject on gles2, so cast it and render it directly if(mesh != null && mesh.bounds.overlaps(bounds)){
if(mesh != null && mesh.vertices instanceof VertexBufferObject vbo && mesh.indices instanceof IndexBufferObject ibo){ mesh.render(shader, Gl.triangles, 0, mesh.getMaxVertices() * 6 / 4);
//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;
Gl.vertexAttribPointer(location, attribute.components, attribute.type, attribute.normalized, vertexSize * 4, aoffset);
}
ibo.bind();
mesh.vertices.render(mesh.indices, Gl.triangles, 0, mesh.getNumIndices());
}else if(mesh != null){
//TODO this should be the default branch!
mesh.bind(shader);
mesh.render(shader, Gl.triangles);
} }
} }
} }
//every underwater object needs to be drawn once per cache layer, which sucks.
if(layer.liquid && underwaterDraw.size > 0){
Draw.blend(underwaterBlend);
var items = underwaterDraw.items;
int len = underwaterDraw.size;
for(int i = 0; i < len; i++){
items[i].run();
}
Draw.flush();
Draw.blend(Blending.normal);
Blending.normal.apply();
beginDraw();
}
layer.end(); layer.end();
} }
@ -298,7 +284,7 @@ public class FloorRenderer{
} }
if(cache[cx][cy].length == 0){ if(cache[cx][cy].length == 0){
cache[cx][cy] = new Mesh[CacheLayer.all.length]; cache[cx][cy] = new ChunkMesh[CacheLayer.all.length];
} }
var meshes = cache[cx][cy]; var meshes = cache[cx][cy];
@ -315,7 +301,7 @@ public class FloorRenderer{
} }
} }
private Mesh cacheChunkLayer(int cx, int cy, CacheLayer layer){ private ChunkMesh cacheChunkLayer(int cx, int cy, CacheLayer layer){
vidx = 0; vidx = 0;
Batch current = Core.batch; Batch current = Core.batch;
@ -345,13 +331,13 @@ public class FloorRenderer{
Core.batch = current; Core.batch = current;
int floats = vidx; int floats = vidx;
//every 4 vertices need 6 indices ChunkMesh mesh = new ChunkMesh(true, floats / vertexSize, 0, attributes,
int vertCount = floats / vertexSize, indCount = vertCount * 6/4; cx * tilesize * chunksize - tilesize/2f, cy * tilesize * chunksize - tilesize/2f,
(cx+1) * tilesize * chunksize + tilesize/2f, (cy+1) * tilesize * chunksize + tilesize/2f);
Mesh mesh = new Mesh(true, vertCount, indCount, attributes);
mesh.setVertices(vertices, 0, vidx); mesh.setVertices(vertices, 0, vidx);
mesh.setAutoBind(false); //all vertices are shared
mesh.setIndices(indices, 0, indCount); mesh.indices = indexData;
return mesh; return mesh;
} }
@ -372,7 +358,7 @@ public class FloorRenderer{
recacheSet.clear(); recacheSet.clear();
int chunksx = Mathf.ceil((float)(world.width()) / chunksize), chunksy = Mathf.ceil((float)(world.height()) / chunksize); 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]; cache = new ChunkMesh[chunksx][chunksy][dynamic ? 0 : CacheLayer.all.length];
texture = Core.atlas.find("grass1").texture; texture = Core.atlas.find("grass1").texture;
error = Core.atlas.find("env-error"); error = Core.atlas.find("env-error");
@ -391,7 +377,28 @@ public class FloorRenderer{
} }
} }
static class ChunkMesh extends Mesh{
Rect bounds = new Rect();
ChunkMesh(boolean isStatic, int maxVertices, int maxIndices, VertexAttribute[] attributes, float minX, float minY, float maxX, float maxY){
super(isStatic, maxVertices, maxIndices, attributes);
bounds.set(minX, minY, maxX - minX, maxY - minY);
}
}
class FloorRenderBatch extends Batch{ class FloorRenderBatch extends Batch{
//TODO: alternate clipping approach, can be more accurate
/*
float minX, minY, maxX, maxY;
void reset(){
minX = Float.POSITIVE_INFINITY;
minY = Float.POSITIVE_INFINITY;
maxX = 0f;
maxY = 0f;
}
*/
@Override @Override
protected void draw(TextureRegion region, float x, float y, float originX, float originY, float width, float height, float rotation){ protected void draw(TextureRegion region, float x, float y, float originX, float originY, float width, float height, float rotation){

View file

@ -32,6 +32,8 @@ public class LightRenderer{
public void add(float x, float y, float radius, Color color, float opacity){ public void add(float x, float y, float radius, Color color, float opacity){
if(!enabled() || radius <= 0f) return; if(!enabled() || radius <= 0f) return;
//TODO: clipping.
float res = Color.toFloatBits(color.r, color.g, color.b, opacity); float res = Color.toFloatBits(color.r, color.g, color.b, opacity);
if(circles.size <= circleIndex) circles.add(new CircleLight()); if(circles.size <= circleIndex) circles.add(new CircleLight());

View file

@ -11,6 +11,7 @@ import arc.struct.*;
import arc.util.*; import arc.util.*;
import arc.util.noise.*; import arc.util.noise.*;
import mindustry.content.*; import mindustry.content.*;
import mindustry.game.*;
import mindustry.type.*; import mindustry.type.*;
import mindustry.world.*; import mindustry.world.*;
@ -239,35 +240,17 @@ public class MenuRenderer implements Disposable{
} }
private void drawFlyers(){ private void drawFlyers(){
Draw.color(0f, 0f, 0f, 0.4f); flyerType.sample.elevation = 1f;
flyerType.sample.team = Team.sharded;
TextureRegion icon = flyerType.fullIcon; flyerType.sample.rotation = flyerRot;
flyerType.sample.heal();
flyers((x, y) -> { flyers((x, y) -> {
Draw.rect(icon, x - 12f, y - 13f, flyerRot - 90); flyerType.sample.set(x, y);
flyerType.drawShadow(flyerType.sample);
flyerType.draw(flyerType.sample);
}); });
float size = Math.max(icon.width, icon.height) * icon.scl() * 1.6f;
flyers((x, y) -> {
Draw.rect("circle-shadow", x, y, size, size);
});
Draw.color();
flyers((x, y) -> {
float engineOffset = flyerType.engineOffset, engineSize = flyerType.engineSize, rotation = flyerRot;
Draw.color(Pal.engine);
Fill.circle(x + Angles.trnsx(rotation + 180, engineOffset), y + Angles.trnsy(rotation + 180, engineOffset),
engineSize + Mathf.absin(Time.time, 2f, engineSize / 4f));
Draw.color(Color.white);
Fill.circle(x + Angles.trnsx(rotation + 180, engineOffset - 1f), y + Angles.trnsy(rotation + 180, engineOffset - 1f),
(engineSize + Mathf.absin(Time.time, 2f, engineSize / 4f)) / 2f);
Draw.color();
Draw.rect(icon, x, y, flyerRot - 90);
});
} }
private void flyers(Floatc2 cons){ private void flyers(Floatc2 cons){

View file

@ -30,8 +30,6 @@ public class MinimapRenderer{
private Rect rect = new Rect(); private Rect rect = new Rect();
private float zoom = 4; private float zoom = 4;
private float lastX, lastY, lastW, lastH, lastScl;
private boolean worldSpace;
private IntSet updates = new IntSet(); private IntSet updates = new IntSet();
private float updateCounter = 0f; private float updateCounter = 0f;
@ -123,13 +121,7 @@ public class MinimapRenderer{
region = new TextureRegion(texture); region = new TextureRegion(texture);
} }
public void drawEntities(float x, float y, float w, float h, float scaling, boolean fullView){ public void drawEntities(float x, float y, float w, float h, boolean fullView){
lastX = x;
lastY = y;
lastW = w;
lastH = h;
lastScl = scaling;
worldSpace = fullView;
if(!fullView){ if(!fullView){
updateUnitArray(); updateUnitArray();
@ -150,12 +142,12 @@ public class MinimapRenderer{
float scaleFactor; float scaleFactor;
var trans = Tmp.m1.idt(); var trans = Tmp.m1.idt();
trans.translate(lastX, lastY); trans.translate(x, y);
if(!worldSpace){ if(!fullView){
trans.scl(Tmp.v1.set(scaleFactor = lastW / rect.width, lastH / rect.height)); trans.scl(Tmp.v1.set(scaleFactor = w / rect.width, h / rect.height));
trans.translate(-rect.x, -rect.y); trans.translate(-rect.x, -rect.y);
}else{ }else{
trans.scl(Tmp.v1.set(scaleFactor = lastW / world.unitWidth(), lastH / world.unitHeight())); trans.scl(Tmp.v1.set(scaleFactor = w / world.unitWidth(), h / world.unitHeight()));
} }
trans.translate(tilesize / 2f, tilesize / 2f); trans.translate(tilesize / 2f, tilesize / 2f);
Draw.trans(trans); Draw.trans(trans);

View file

@ -131,7 +131,7 @@ public class OverlayRenderer{
Building build = (select instanceof BlockUnitc b ? b.tile() : select instanceof Building b ? b : null); Building build = (select instanceof BlockUnitc b ? b.tile() : select instanceof Building b ? b : null);
TextureRegion region = build != null ? build.block.fullIcon : Core.atlas.white(); TextureRegion region = build != null ? build.block.fullIcon : Core.atlas.white();
if(!(select instanceof Unitc)){ if(select instanceof BlockUnitc){
Draw.rect(region, select.getX(), select.getY()); Draw.rect(region, select.getX(), select.getY());
} }

View file

@ -116,6 +116,8 @@ public class Pal{
neoplasm1 = Color.valueOf("f98f4a"), neoplasm1 = Color.valueOf("f98f4a"),
neoplasmMid = Color.valueOf("e05438"), neoplasmMid = Color.valueOf("e05438"),
neoplasm2 = Color.valueOf("9e172c"), neoplasm2 = Color.valueOf("9e172c"),
neoplasmAcid = Color.valueOf("8ead44"),
neoplasmAcidGlow = Color.valueOf("68e43e"),
logicBlocks = Color.valueOf("d4816b"), logicBlocks = Color.valueOf("d4816b"),
logicControl = Color.valueOf("6bb2b2"), logicControl = Color.valueOf("6bb2b2"),

View file

@ -18,8 +18,6 @@ public class PlanetParams{
public Vec3 camUp = new Vec3(0f, 1f, 0f); public Vec3 camUp = new Vec3(0f, 1f, 0f);
/** the unit length direction vector of the camera **/ /** the unit length direction vector of the camera **/
public Vec3 camDir = new Vec3(0, 0, -1); public Vec3 camDir = new Vec3(0, 0, -1);
/** The sun/main planet of the solar system from which everything is rendered. Deprecated use planet.solarSystem instead */
public @Deprecated Planet solarSystem = Planets.sun;
/** Planet being looked at. */ /** Planet being looked at. */
public Planet planet = Planets.serpulo; public Planet planet = Planets.serpulo;

View file

@ -474,7 +474,7 @@ public class DesktopInput extends InputHandler{
cursorType = cursor.build.getCursor(); cursorType = cursor.build.getCursor();
} }
if(canRepairDerelict(cursor)){ if(canRepairDerelict(cursor) && !player.dead() && player.unit().canBuild()){
cursorType = ui.repairCursor; cursorType = ui.repairCursor;
} }

View file

@ -312,6 +312,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
//only assign a group when this is not a queued command //only assign a group when this is not a queued command
if(ai.commandQueue.size == 0 && unitIds.length > 1){ if(ai.commandQueue.size == 0 && unitIds.length > 1){
int layer = unit.collisionLayer(); int layer = unit.collisionLayer();
if(layer == -1) layer = 0;
if(groups[layer] == null){ if(groups[layer] == null){
groups[layer] = new UnitGroup(); groups[layer] = new UnitGroup();
} }
@ -418,7 +421,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
if(player == null || build == null || !build.interactable(player.team()) || !player.within(build, itemTransferRange) || player.dead() || amount <= 0) return; if(player == null || build == null || !build.interactable(player.team()) || !player.within(build, itemTransferRange) || player.dead() || amount <= 0) return;
if(net.server() && (!Units.canInteract(player, build) || if(net.server() && (!Units.canInteract(player, build) ||
!netServer.admins.allowAction(player, ActionType.withdrawItem, build.tile(), action -> { !netServer.admins.allowAction(player, ActionType.withdrawItem, build.tile, action -> {
action.item = item; action.item = item;
action.itemAmount = amount; action.itemAmount = amount;
}))){ }))){
@ -606,7 +609,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
if(build == null) return; if(build == null) return;
if(net.server() && (!Units.canInteract(player, build) || if(net.server() && (!Units.canInteract(player, build) ||
!netServer.admins.allowAction(player, ActionType.rotate, build.tile(), action -> action.rotation = Mathf.mod(build.rotation + Mathf.sign(direction), 4)))){ !netServer.admins.allowAction(player, ActionType.rotate, build.tile, action -> action.rotation = Mathf.mod(build.rotation + Mathf.sign(direction), 4)))){
throw new ValidateException(player, "Player cannot rotate a block."); throw new ValidateException(player, "Player cannot rotate a block.");
} }
@ -693,7 +696,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
if(unit == null){ //just clear the unit (is this used?) if(unit == null){ //just clear the unit (is this used?)
player.clearUnit(); player.clearUnit();
//make sure it's AI controlled, so players can't overwrite each other //make sure it's AI controlled, so players can't overwrite each other
}else if(unit.isAI() && unit.team == player.team() && !unit.dead && unit.type.playerControllable){ }else if(unit.isAI() && unit.team == player.team() && !unit.dead && unit.playerControllable()){
if(net.client() && player.isLocal()){ if(net.client() && player.isLocal()){
player.justSwitchFrom = player.unit(); player.justSwitchFrom = player.unit();
player.justSwitchTo = unit; player.justSwitchTo = unit;
@ -878,7 +881,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
} }
if(controlledType != null && player.dead() && controlledType.playerControllable){ if(controlledType != null && player.dead() && controlledType.playerControllable){
Unit unit = Units.closest(player.team(), player.x, player.y, u -> !u.isPlayer() && u.type == controlledType && !u.dead); Unit unit = Units.closest(player.team(), player.x, player.y, u -> !u.isPlayer() && u.type == controlledType && u.playerControllable() && !u.dead);
if(unit != null){ if(unit != null){
//only trying controlling once a second to prevent packet spam //only trying controlling once a second to prevent packet spam
@ -1853,7 +1856,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
} }
public @Nullable Unit selectedUnit(){ public @Nullable Unit selectedUnit(){
Unit unit = Units.closest(player.team(), Core.input.mouseWorld().x, Core.input.mouseWorld().y, 40f, u -> u.isAI() && u.type.playerControllable); Unit unit = Units.closest(player.team(), Core.input.mouseWorld().x, Core.input.mouseWorld().y, 40f, u -> u.isAI() && u.playerControllable());
if(unit != null){ if(unit != null){
unit.hitbox(Tmp.r1); unit.hitbox(Tmp.r1);
Tmp.r1.grow(6f); Tmp.r1.grow(6f);
@ -2053,7 +2056,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
} }
} }
return ignoreUnits ? Build.validPlaceIgnoreUnits(type, player.team(), x, y, rotation, true) : Build.validPlace(type, player.team(), x, y, rotation); return ignoreUnits ? Build.validPlaceIgnoreUnits(type, player.team(), x, y, rotation, true, true) : Build.validPlace(type, player.team(), x, y, rotation);
} }
public boolean validBreak(int x, int y){ public boolean validBreak(int x, int y){

View file

@ -706,7 +706,7 @@ public class MobileInput extends InputHandler implements GestureListener{
payloadTarget = null; payloadTarget = null;
//control a unit/block detected on first tap of double-tap //control a unit/block detected on first tap of double-tap
if(unitTapped != null && state.rules.possessionAllowed && unitTapped.isAI() && unitTapped.team == player.team() && !unitTapped.dead && unitTapped.type.playerControllable){ if(unitTapped != null && state.rules.possessionAllowed && unitTapped.isAI() && unitTapped.team == player.team() && !unitTapped.dead && unitTapped.playerControllable()){
Call.unitControl(player, unitTapped); Call.unitControl(player, unitTapped);
recentRespawnTimer = 1f; recentRespawnTimer = 1f;
}else if(buildingTapped != null && state.rules.possessionAllowed){ }else if(buildingTapped != null && state.rules.possessionAllowed){

View file

@ -308,6 +308,7 @@ public class JsonIO{
} }
exec.all.add(obj); exec.all.add(obj);
obj.validate();
} }
// Second iteration to map the parents. // Second iteration to map the parents.

View file

@ -382,6 +382,7 @@ public abstract class SaveVersion extends SaveFileReader{
writeChunk(stream, true, out -> { writeChunk(stream, true, out -> {
out.writeByte(entity.classId()); out.writeByte(entity.classId());
out.writeInt(entity.id()); out.writeInt(entity.id());
entity.beforeWrite();
entity.write(Writes.get(out)); entity.write(Writes.get(out));
}); });
} }
@ -436,8 +437,6 @@ public abstract class SaveVersion extends SaveFileReader{
//entityMapping is null in older save versions, so use the default //entityMapping is null in older save versions, so use the default
var mapping = this.entityMapping == null ? EntityMapping.idMap : this.entityMapping; var mapping = this.entityMapping == null ? EntityMapping.idMap : this.entityMapping;
Seq<Entityc> entities = new Seq<>();
int amount = stream.readInt(); int amount = stream.readInt();
for(int j = 0; j < amount; j++){ for(int j = 0; j < amount; j++){
readChunk(stream, true, in -> { readChunk(stream, true, in -> {
@ -450,7 +449,6 @@ public abstract class SaveVersion extends SaveFileReader{
int id = in.readInt(); int id = in.readInt();
Entityc entity = (Entityc)mapping[typeid].get(); Entityc entity = (Entityc)mapping[typeid].get();
entities.add(entity);
EntityGroup.checkNextId(id); EntityGroup.checkNextId(id);
entity.id(id); entity.id(id);
entity.read(Reads.get(in)); entity.read(Reads.get(in));
@ -458,9 +456,7 @@ public abstract class SaveVersion extends SaveFileReader{
}); });
} }
for(var e : entities){ Groups.all.each(Entityc::afterReadAll);
e.afterAllRead();
}
} }
public void readEntityMapping(DataInput stream) throws IOException{ public void readEntityMapping(DataInput stream) throws IOException{

View file

@ -1434,7 +1434,7 @@ public class LExecutor{
if(type.obj() instanceof UnitType type && !type.internal && !type.hidden && t != null && Units.canCreate(t, type)){ if(type.obj() instanceof UnitType type && !type.internal && !type.hidden && t != null && Units.canCreate(t, type)){
//random offset to prevent stacking //random offset to prevent stacking
var unit = type.spawn(t, World.unconv(x.numf()) + Mathf.range(0.01f), World.unconv(y.numf()) + Mathf.range(0.01f)); var unit = type.spawn(t, World.unconv(x.numf()) + Mathf.range(0.01f), World.unconv(y.numf()) + Mathf.range(0.01f));
spawner.spawnEffect(unit, rotation.numf()); spawner.spawnEffect(unit);
result.setobj(unit); result.setobj(unit);
} }
} }
@ -1603,11 +1603,12 @@ public class LExecutor{
}else if(full){ }else if(full){
//disable the rule, covers the whole map //disable the rule, covers the whole map
if(set){ if(set){
int prevX = state.rules.limitX, prevY = state.rules.limitY, prevW = state.rules.limitWidth, prevH = state.rules.limitHeight;
state.rules.limitMapArea = false; state.rules.limitMapArea = false;
if(!headless){ if(!headless){
renderer.updateAllDarkness(); renderer.updateAllDarkness();
} }
world.checkMapArea(); world.checkMapArea(prevX, prevY, prevW, prevH);
return false; return false;
} }
} }
@ -1616,12 +1617,20 @@ public class LExecutor{
} }
if(set){ if(set){
int prevX = state.rules.limitX, prevY = state.rules.limitY, prevW = state.rules.limitWidth, prevH = state.rules.limitHeight;
if(!state.rules.limitMapArea){
//it was never on in the first place, so the old bounds don't apply
prevW = 0;
prevH = 0;
prevX = -1;
prevY = -1;
}
state.rules.limitMapArea = true; state.rules.limitMapArea = true;
state.rules.limitX = x; state.rules.limitX = x;
state.rules.limitY = y; state.rules.limitY = y;
state.rules.limitWidth = w; state.rules.limitWidth = w;
state.rules.limitHeight = h; state.rules.limitHeight = h;
world.checkMapArea(); world.checkMapArea(prevX, prevY, prevW, prevH);
if(!headless){ if(!headless){
renderer.updateAllDarkness(); renderer.updateAllDarkness();
@ -1933,9 +1942,7 @@ public class LExecutor{
for(int i = 0; i < spawned; i++){ for(int i = 0; i < spawned; i++){
Tmp.v1.rnd(spread); Tmp.v1.rnd(spread);
Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1); spawner.spawnUnit(group, spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
unit.set(spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
Vars.spawner.spawnEffect(unit);
} }
} }
} }

View file

@ -136,6 +136,8 @@ public class ContentParser{
put(BulletType.class, (type, data) -> { put(BulletType.class, (type, data) -> {
if(data.isString()){ if(data.isString()){
return field(Bullets.class, data); return field(Bullets.class, data);
}else if(data.isArray()){
return new MultiBulletType(parser.readValue(BulletType[].class, data));
} }
Class<?> bc = resolve(data.getString("type", ""), BasicBulletType.class); Class<?> bc = resolve(data.getString("type", ""), BasicBulletType.class);
data.remove("type"); data.remove("type");

View file

@ -366,7 +366,7 @@ public class Mods implements Loadable{
} }
Log.debug("Time to generate icons: @", Time.elapsed()); Log.debug("Time to generate icons: @", Time.elapsed());
//dispose old atlas data //replace old atlas data
Core.atlas = packer.flush(filter, new TextureAtlas(){ Core.atlas = packer.flush(filter, new TextureAtlas(){
PixmapRegion fake = new PixmapRegion(new Pixmap(1, 1)); PixmapRegion fake = new PixmapRegion(new Pixmap(1, 1));
boolean didWarn = false; boolean didWarn = false;
@ -392,6 +392,8 @@ public class Mods implements Loadable{
Log.debug("Total pages: @", Core.atlas.getTextures().size); Log.debug("Total pages: @", Core.atlas.getTextures().size);
packer.printStats(); packer.printStats();
Events.fire(new AtlasPackEvent());
} }
packer.dispose(); packer.dispose();
@ -1151,7 +1153,7 @@ public class Mods implements Loadable{
!skipModLoading() && !skipModLoading() &&
Core.settings.getBool("mod-" + baseName + "-enabled", true) && Core.settings.getBool("mod-" + baseName + "-enabled", true) &&
Version.isAtLeast(meta.minGameVersion) && Version.isAtLeast(meta.minGameVersion) &&
(meta.getMinMajor() >= 136 || headless) && (meta.getMinMajor() >= minJavaModGameVersion || headless) &&
!skipModCode && !skipModCode &&
initialize initialize
){ ){
@ -1249,7 +1251,7 @@ public class Mods implements Loadable{
/** @return whether this is a java class mod. */ /** @return whether this is a java class mod. */
public boolean isJava(){ public boolean isJava(){
return meta.java || main != null; return meta.java || main != null || meta.main != null;
} }
@Nullable @Nullable
@ -1292,10 +1294,10 @@ public class Mods implements Loadable{
return blacklistedMods.contains(name); return blacklistedMods.contains(name);
} }
/** @return whether this mod is outdated, e.g. not compatible with v7. */ /** @return whether this mod is outdated, i.e. not compatible with v8/v7. */
public boolean isOutdated(){ public boolean isOutdated(){
//must be at least 136 to indicate v7 compat //must be at least 136 to indicate v7 compat
return getMinMajor() < 136; return getMinMajor() < (isJava() ? minJavaModGameVersion : minModGameVersion);
} }
public int getMinMajor(){ public int getMinMajor(){
@ -1397,11 +1399,6 @@ public class Mods implements Loadable{
/** If set, load the mod content in this order by content names */ /** If set, load the mod content in this order by content names */
public String[] contentOrder; public String[] contentOrder;
public String displayName(){
//useless, kept for legacy reasons
return displayName;
}
public String shortDescription(){ public String shortDescription(){
return Strings.truncate(subtitle == null ? (description == null || description.length() > maxModSubtitleLength ? "" : description) : subtitle, maxModSubtitleLength, "..."); return Strings.truncate(subtitle == null ? (description == null || description.length() > maxModSubtitleLength ? "" : description) : subtitle, maxModSubtitleLength, "...");
} }

View file

@ -198,7 +198,7 @@ public class GameService{
if(campaign() && e.unit != null && e.unit.isLocal() && !e.breaking){ if(campaign() && e.unit != null && e.unit.isLocal() && !e.breaking){
SStat.blocksBuilt.add(); SStat.blocksBuilt.add();
if(e.tile.block() == Blocks.router && e.tile.build.proximity().contains(t -> t.block == Blocks.router)){ if(e.tile.block() == Blocks.router && e.tile.build.proximity.contains(t -> t.block == Blocks.router)){
chainRouters.complete(); chainRouters.complete();
} }

View file

@ -135,6 +135,8 @@ public class UnitType extends UnlockableContent implements Senseable{
lightRadius = -1f, lightRadius = -1f,
/** light color opacity*/ /** light color opacity*/
lightOpacity = 0.6f, lightOpacity = 0.6f,
/** scale of soft shadow - its size is calculated based off of region size */
softShadowScl = 1f,
/** fog view radius in tiles. <0 for automatic radius. */ /** fog view radius in tiles. <0 for automatic radius. */
fogRadius = -1f, fogRadius = -1f,
@ -159,6 +161,8 @@ public class UnitType extends UnlockableContent implements Senseable{
faceTarget = true, faceTarget = true,
/** AI flag: if true, this flying unit circles around its target like a bomber */ /** AI flag: if true, this flying unit circles around its target like a bomber */
circleTarget = false, circleTarget = false,
/** AI flag: if true, this unit will drop bombs under itself even when it is not next to its 'real' target. used for carpet bombers */
autoDropBombs = false,
/** if true, this unit can boost into the air if a player/processors controls it*/ /** if true, this unit can boost into the air if a player/processors controls it*/
canBoost = false, canBoost = false,
/** if true, this unit will always boost when using builder AI */ /** if true, this unit will always boost when using builder AI */
@ -199,7 +203,7 @@ public class UnitType extends UnlockableContent implements Senseable{
allowLegStep = false, allowLegStep = false,
/** for legged units, setting this to false forces it to be on the ground physics layer. */ /** for legged units, setting this to false forces it to be on the ground physics layer. */
legPhysicsLayer = true, legPhysicsLayer = true,
/** if true, this unit cannot drown, and will not be affected by the floor under it. */ /** if true, this unit will not be affected by the floor under it. */
hovering = false, hovering = false,
/** if true, this unit can move in any direction regardless of rotation. if false, this unit can only move in the direction it is facing. */ /** if true, this unit can move in any direction regardless of rotation. if false, this unit can only move in the direction it is facing. */
omniMovement = true, omniMovement = true,
@ -219,6 +223,8 @@ public class UnitType extends UnlockableContent implements Senseable{
hidden = false, hidden = false,
/** if true, this unit is for internal use only and does not have a sprite generated. */ /** if true, this unit is for internal use only and does not have a sprite generated. */
internal = false, internal = false,
/** For certain units, generating sprites is still necessary, despite being internal. */
internalGenerateSprites = false,
/** If false, this unit is not pushed away from map edges. */ /** If false, this unit is not pushed away from map edges. */
bounded = true, bounded = true,
/** if true, this unit is detected as naval - do NOT assign this manually! Initialized in init() */ /** if true, this unit is detected as naval - do NOT assign this manually! Initialized in init() */
@ -234,6 +240,8 @@ public class UnitType extends UnlockableContent implements Senseable{
hoverable = true, hoverable = true,
/** if true, this modded unit always has a -outline region generated for its base. Normally, outlines are ignored if there are no top = false weapons. */ /** if true, this modded unit always has a -outline region generated for its base. Normally, outlines are ignored if there are no top = false weapons. */
alwaysCreateOutline = false, alwaysCreateOutline = false,
/** for vanilla content only - if false, skips the full icon generation step. */
generateFullIcon = true,
/** if true, this unit has a square shadow. */ /** if true, this unit has a square shadow. */
squareShape = false, squareShape = false,
/** if true, this unit will draw its building beam towards blocks. */ /** if true, this unit will draw its building beam towards blocks. */
@ -248,6 +256,8 @@ public class UnitType extends UnlockableContent implements Senseable{
drawShields = true, drawShields = true,
/** if false, the unit body is not drawn. */ /** if false, the unit body is not drawn. */
drawBody = true, drawBody = true,
/** if false, the soft shadow is not drawn. */
drawSoftShadow = true,
/** if false, the unit is not drawn on the minimap. */ /** if false, the unit is not drawn on the minimap. */
drawMinimap = true; drawMinimap = true;
@ -300,6 +310,8 @@ public class UnitType extends UnlockableContent implements Senseable{
/** override for engine trail color */ /** override for engine trail color */
public @Nullable Color trailColor; public @Nullable Color trailColor;
/** Cost type ID for flow field/enemy AI pathfinding. */
public int flowfieldPathType = -1;
/** Function used for calculating cost of moving with ControlPathfinder. Does not affect "normal" flow field pathfinding. */ /** Function used for calculating cost of moving with ControlPathfinder. Does not affect "normal" flow field pathfinding. */
public @Nullable PathCost pathCost; public @Nullable PathCost pathCost;
/** ID for path cost, to be used in the control path finder. This is the value that actually matters; do not assign manually. Set in init(). */ /** ID for path cost, to be used in the control path finder. This is the value that actually matters; do not assign manually. Set in init(). */
@ -386,12 +398,18 @@ public class UnitType extends UnlockableContent implements Senseable{
/** how straight the leg outward angles are (0 = circular, 1 = horizontal line) */ /** how straight the leg outward angles are (0 = circular, 1 = horizontal line) */
legStraightness = 0f; legStraightness = 0f;
/** If true, the base (further away) leg region is drawn under instead of over. */
public boolean legBaseUnder = false;
/** If true, legs are locked to the base of the unit instead of being on an implicit rotating "mount". */ /** If true, legs are locked to the base of the unit instead of being on an implicit rotating "mount". */
public boolean lockLegBase = false; public boolean lockLegBase = false;
/** If true, legs always try to move around even when the unit is not moving (leads to more natural behavior) */ /** If true, legs always try to move around even when the unit is not moving (leads to more natural behavior) */
public boolean legContinuousMove; public boolean legContinuousMove;
/** TODO neither of these appear to do much */ /** TODO neither of these appear to do much */
public boolean flipBackLegs = true, flipLegSide = false; public boolean flipBackLegs = true, flipLegSide = false;
/** Whether to emit a splashing noise in water. */
public boolean emitWalkSound = true;
/** Whether to emit a splashing effect in water (fasle implies emitWalkSound false). */
public boolean emitWalkEffect = true;
//MECH UNITS //MECH UNITS
@ -417,6 +435,14 @@ public class UnitType extends UnlockableContent implements Senseable{
/** number of independent segments */ /** number of independent segments */
public int segments = 0; public int segments = 0;
/** TODO wave support - for multi-unit segmented units, this is the number of independent units that are spawned */
public int segmentUnits = 1;
/** unit spawned in segments; if null, the same unit is used */
public @Nullable UnitType segmentUnit;
/** unit spawned at the end; if null, the segment unit is used */
public @Nullable UnitType segmentEndUnit;
/** true - parent segments are on higher layers; false - parent segments are on lower layers than head*/
public boolean segmentLayerOrder = true;
/** magnitude of sine offset between segments */ /** magnitude of sine offset between segments */
public float segmentMag = 2f, public float segmentMag = 2f,
/** scale of sine offset between segments */ /** scale of sine offset between segments */
@ -427,6 +453,10 @@ public class UnitType extends UnlockableContent implements Senseable{
segmentRotSpeed = 1f, segmentRotSpeed = 1f,
/** maximum difference between segment angles */ /** maximum difference between segment angles */
segmentMaxRot = 30f, segmentMaxRot = 30f,
/** spacing between separate unit segments (only used for multi-unit worms) */
segmentSpacing = -1f,
/** rotation between segments is clamped to this range */
segmentRotationRange = 80f,
/** speed multiplier this unit will have when crawlSlowdownFrac is met. */ /** speed multiplier this unit will have when crawlSlowdownFrac is met. */
crawlSlowdown = 0.5f, crawlSlowdown = 0.5f,
/** damage dealt to blocks under this tank/crawler every frame. */ /** damage dealt to blocks under this tank/crawler every frame. */
@ -485,13 +515,49 @@ public class UnitType extends UnlockableContent implements Senseable{
return unit; return unit;
} }
public Unit spawn(Team team, float x, float y){ /** @param cons Callback that gets called with every unit that is spawned. This is used for multi-unit segmented units. */
public Unit spawn(Team team, float x, float y, float rotation, @Nullable Cons<Unit> cons){
float offsetX = 0f, offsetY = 0f;
if(segmentUnits > 1 && sample instanceof Segmentc){
Tmp.v1.trns(rotation, segmentSpacing * segmentUnits / 2f);
offsetX = Tmp.v1.x;
offsetY = Tmp.v1.y;
}
Unit out = create(team); Unit out = create(team);
out.set(x, y); out.rotation = rotation;
out.set(x + offsetX, y + offsetY);
out.add(); out.add();
if(cons != null) cons.get(out);
if(segmentUnits > 1 && out instanceof Segmentc){
Unit last = out;
UnitType segType = segmentUnit == null ? this : segmentUnit;
for(int i = 0; i < segmentUnits; i++){
UnitType type = i == segmentUnits - 1 && segmentEndUnit != null ? segmentEndUnit : segType;
Unit next = type.create(team);
Tmp.v1.trns(rotation, segmentSpacing * (i + 1));
next.set(x - Tmp.v1.x + offsetX, y - Tmp.v1.y + offsetY);
next.rotation = rotation;
next.add();
((Segmentc)last).addChild(next);
if(cons != null) cons.get(next);
last = next;
}
}
return out; return out;
} }
public Unit spawn(Team team, float x, float y, float rotation){
return spawn(team, x, y, rotation, null);
}
public Unit spawn(Team team, float x, float y){
return spawn(team, x, y, 0f);
}
public Unit spawn(float x, float y){ public Unit spawn(float x, float y){
return spawn(state.rules.defaultTeam, x, y); return spawn(state.rules.defaultTeam, x, y);
} }
@ -557,7 +623,7 @@ public class UnitType extends UnlockableContent implements Senseable{
} }
if(payloadCapacity > 0 && unit instanceof Payloadc payload){ if(payloadCapacity > 0 && unit instanceof Payloadc payload){
bars.add(new Bar("stat.payloadcapacity", Pal.items, () -> payload.payloadUsed() / unit.type().payloadCapacity)); bars.add(new Bar("stat.payloadcapacity", Pal.items, () -> payload.payloadUsed() / payloadCapacity));
bars.row(); bars.row();
var count = new float[]{-1}; var count = new float[]{-1};
@ -699,12 +765,13 @@ public class UnitType extends UnlockableContent implements Senseable{
Unit example = constructor.get(); Unit example = constructor.get();
allowLegStep = example instanceof Legsc; allowLegStep = example instanceof Legsc || example instanceof Crawlc;
//water preset //water preset
if(example instanceof WaterMovec){ if(example instanceof WaterMovec || example instanceof WaterCrawlc){
naval = true; naval = true;
canDrown = false; canDrown = false;
emitWalkSound = false;
omniMovement = false; omniMovement = false;
immunities.add(StatusEffects.wet); immunities.add(StatusEffects.wet);
if(shadowElevation < 0f){ if(shadowElevation < 0f){
@ -712,10 +779,19 @@ public class UnitType extends UnlockableContent implements Senseable{
} }
} }
if(flowfieldPathType == -1){
flowfieldPathType =
naval ? Pathfinder.costNaval :
allowLegStep ? Pathfinder.costLegs :
flying ? Pathfinder.costNone :
hovering ? Pathfinder.costHover :
Pathfinder.costGround;
}
if(pathCost == null){ if(pathCost == null){
pathCost = pathCost =
naval ? ControlPathfinder.costNaval : naval ? ControlPathfinder.costNaval :
allowLegStep || example instanceof Crawlc ? ControlPathfinder.costLegs : allowLegStep ? ControlPathfinder.costLegs :
hovering ? ControlPathfinder.costHover : hovering ? ControlPathfinder.costHover :
ControlPathfinder.costGround; ControlPathfinder.costGround;
} }
@ -783,6 +859,10 @@ public class UnitType extends UnlockableContent implements Senseable{
mechStride = 4f + (hitSize -8f)/2.1f; mechStride = 4f + (hitSize -8f)/2.1f;
} }
if(segmentSpacing < 0){
segmentSpacing = hitSize;
}
if(aimDst < 0){ if(aimDst < 0){
aimDst = weapons.contains(w -> !w.rotate) ? hitSize * 2f : hitSize / 2f; aimDst = weapons.contains(w -> !w.rotate) ? hitSize * 2f : hitSize / 2f;
} }
@ -1229,19 +1309,27 @@ public class UnitType extends UnlockableContent implements Senseable{
//region drawing //region drawing
public void draw(Unit unit){ public void draw(Unit unit){
float scl = xscl;
if(unit.inFogTo(Vars.player.team())) return; if(unit.inFogTo(Vars.player.team())) return;
unit.drawBuilding(); if(buildSpeed > 0f){
drawMining(unit); unit.drawBuilding();
}
if(unit.mining()){
drawMining(unit);
}
boolean isPayload = !unit.isAdded(); boolean isPayload = !unit.isAdded();
Mechc mech = unit instanceof Mechc ? (Mechc)unit : null; Mechc mech = unit instanceof Mechc m ? m : null;
float z = isPayload ? Draw.z() : (unit.elevation > 0.5f ? flyingLayer : groundLayer) + Mathf.clamp(hitSize / 4000f, 0, 0.01f); Segmentc seg = unit instanceof Segmentc c ? c : null;
float z =
if(unit.controller().isBeingControlled(player.unit())){ isPayload ? Draw.z() :
drawControl(unit); //dead flying units are assumed to be falling, and to prevent weird clipping issue with the dark "fog", they always draw above it
} unit.elevation > 0.5f || (flying && unit.dead) ? (flyingLayer) :
seg != null ? groundLayer + seg.segmentIndex() / 4000f * Mathf.sign(segmentLayerOrder) + (!segmentLayerOrder ? 0.01f : 0f) :
groundLayer + Mathf.clamp(hitSize / 4000f, 0, 0.01f);
if(!isPayload && (unit.isFlying() || shadowElevation > 0)){ if(!isPayload && (unit.isFlying() || shadowElevation > 0)){
Draw.z(Math.min(Layer.darkness, z - 1f)); Draw.z(Math.min(Layer.darkness, z - 1f));
@ -1276,7 +1364,7 @@ public class UnitType extends UnlockableContent implements Senseable{
drawPayload((Unit & Payloadc)unit); drawPayload((Unit & Payloadc)unit);
} }
drawSoftShadow(unit); if(drawSoftShadow) drawSoftShadow(unit);
Draw.z(z); Draw.z(z);
@ -1294,6 +1382,7 @@ public class UnitType extends UnlockableContent implements Senseable{
Draw.z(z); Draw.z(z);
if(drawBody) drawBody(unit); if(drawBody) drawBody(unit);
if(drawCell && !(unit instanceof Crawlc)) drawCell(unit); if(drawCell && !(unit instanceof Crawlc)) drawCell(unit);
Draw.scl(scl); //TODO this is a hack for neoplasm turrets
drawWeapons(unit); drawWeapons(unit);
if(drawItems) drawItems(unit); if(drawItems) drawItems(unit);
if(!isPayload){ if(!isPayload){
@ -1394,15 +1483,6 @@ public class UnitType extends UnlockableContent implements Senseable{
); );
} }
public void drawControl(Unit unit){
Draw.z(unit.isFlying() ? Layer.flyingUnitLow : Layer.groundUnit - 2);
Draw.color(Pal.accent, Color.white, Mathf.absin(4f, 0.3f));
Lines.poly(unit.x, unit.y, 4, unit.hitSize + 1.5f);
Draw.reset();
}
public void drawShadow(Unit unit){ public void drawShadow(Unit unit){
float e = Mathf.clamp(unit.elevation, shadowElevation, 1f) * shadowElevationScl * (1f - unit.drownTime); float e = Mathf.clamp(unit.elevation, shadowElevation, 1f) * shadowElevationScl * (1f - unit.drownTime);
float x = unit.x + shadowTX * e, y = unit.y + shadowTY * e; float x = unit.x + shadowTX * e, y = unit.y + shadowTY * e;
@ -1428,7 +1508,7 @@ public class UnitType extends UnlockableContent implements Senseable{
public void drawSoftShadow(float x, float y, float rotation, float alpha){ public void drawSoftShadow(float x, float y, float rotation, float alpha){
Draw.color(0, 0, 0, 0.4f * alpha); Draw.color(0, 0, 0, 0.4f * alpha);
float rad = 1.6f; float rad = 1.6f;
float size = Math.max(region.width, region.height) * region.scl(); float size = Math.max(region.width, region.height) * region.scl() * softShadowScl;
Draw.rect(softShadowRegion, x, y, size * rad * Draw.xscl, size * rad * Draw.yscl, rotation - 90); Draw.rect(softShadowRegion, x, y, size * rad * Draw.xscl, size * rad * Draw.yscl, rotation - 90);
Draw.color(); Draw.color();
} }
@ -1528,6 +1608,11 @@ public class UnitType extends UnlockableContent implements Senseable{
public void drawBody(Unit unit){ public void drawBody(Unit unit){
applyColor(unit); applyColor(unit);
if(unit instanceof UnderwaterMovec){
Draw.alpha(1f);
Draw.mixcol(unit.floorOn().mapColor.write(Tmp.c1).mul(0.9f), 1f);
}
Draw.rect(region, unit.x, unit.y, unit.rotation - 90); Draw.rect(region, unit.x, unit.y, unit.rotation - 90);
Draw.reset(); Draw.reset();
@ -1613,11 +1698,19 @@ public class UnitType extends UnlockableContent implements Senseable{
Draw.rect(footRegion, leg.base.x, leg.base.y, position.angleTo(leg.base)); Draw.rect(footRegion, leg.base.x, leg.base.y, position.angleTo(leg.base));
} }
Lines.stroke(legRegion.height * legRegion.scl() * flips); if(legBaseUnder){
Lines.line(legRegion, position.x, position.y, leg.joint.x, leg.joint.y, false); Lines.stroke(legBaseRegion.height * legRegion.scl() * flips);
Lines.line(legBaseRegion, leg.joint.x + Tmp.v1.x, leg.joint.y + Tmp.v1.y, leg.base.x, leg.base.y, false);
Lines.stroke(legBaseRegion.height * legRegion.scl() * flips); Lines.stroke(legRegion.height * legRegion.scl() * flips);
Lines.line(legBaseRegion, leg.joint.x + Tmp.v1.x, leg.joint.y + Tmp.v1.y, leg.base.x, leg.base.y, false); Lines.line(legRegion, position.x, position.y, leg.joint.x, leg.joint.y, false);
}else{
Lines.stroke(legRegion.height * legRegion.scl() * flips);
Lines.line(legRegion, position.x, position.y, leg.joint.x, leg.joint.y, false);
Lines.stroke(legBaseRegion.height * legRegion.scl() * flips);
Lines.line(legBaseRegion, leg.joint.x + Tmp.v1.x, leg.joint.y + Tmp.v1.y, leg.base.x, leg.base.y, false);
}
if(jointRegion.found()){ if(jointRegion.found()){
Draw.rect(jointRegion, leg.joint.x, leg.joint.y); Draw.rect(jointRegion, leg.joint.x, leg.joint.y);
@ -1640,27 +1733,29 @@ public class UnitType extends UnlockableContent implements Senseable{
Draw.reset(); Draw.reset();
} }
//TODO
public void drawCrawl(Crawlc crawl){ public void drawCrawl(Crawlc crawl){
Unit unit = (Unit)crawl; Unit unit = (Unit)crawl;
applyColor(unit); applyColor(unit);
//change to 2 TODO float crawlTime =
crawl instanceof Segmentc seg && seg.headSegment() instanceof Crawlc head ? head.crawlTime() + seg.segmentIndex() * segmentPhase * segments :
crawl.crawlTime();
for(int p = 0; p < 2; p++){ for(int p = 0; p < 2; p++){
TextureRegion[] regions = p == 0 ? segmentOutlineRegions : segmentRegions; TextureRegion[] regions = p == 0 ? segmentOutlineRegions : segmentRegions;
for(int i = 0; i < segments; i++){ for(int i = 0; i < segments; i++){
float trns = Mathf.sin(crawl.crawlTime() + i * segmentPhase, segmentScl, segmentMag); float trns = Mathf.sin(crawlTime + i * segmentPhase, segmentScl, segmentMag);
//at segment 0, rotation = segmentRot, but at the last segment it is rotation //at segment 0, rotation = segmentRot, but at the last segment it is rotation
float rot = Mathf.slerp(crawl.segmentRot(), unit.rotation, i / (float)(segments - 1)); float rot = Mathf.slerp(crawl.segmentRot(), unit.rotation, i / (float)(segments - 1));
float tx = Angles.trnsx(rot, trns), ty = Angles.trnsy(rot, trns); float tx = Angles.trnsx(rot, trns), ty = Angles.trnsy(rot, trns);
//shadow //shadow
Draw.color(0f, 0f, 0f, 0.2f); //Draw.color(0f, 0f, 0f, 0.2f);
//Draw.rect(regions[i], unit.x + tx + 2f, unit.y + ty - 2f, rot - 90); //Draw.rect(regions[i], unit.x + tx + 2f, unit.y + ty - 2f, rot - 90);
applyColor(unit); //applyColor(unit);
//TODO merge outlines? //TODO merge outlines?
Draw.rect(regions[i], unit.x + tx, unit.y + ty, rot - 90); Draw.rect(regions[i], unit.x + tx, unit.y + ty, rot - 90);

View file

@ -46,7 +46,7 @@ public class Weapon implements Cloneable{
public boolean rotate = false; public boolean rotate = false;
/** Whether to show the sprite of the weapon in the database. */ /** Whether to show the sprite of the weapon in the database. */
public boolean showStatSprite = true; public boolean showStatSprite = true;
/** rotation at which this weapon starts at. TODO buggy!*/ /** rotation at which this weapon starts at. */
public float baseRotation = 0f; public float baseRotation = 0f;
/** whether to draw the outline on top. */ /** whether to draw the outline on top. */
public boolean top = true; public boolean top = true;
@ -92,14 +92,16 @@ public class Weapon implements Cloneable{
public float shootX = 0f, shootY = 3f; public float shootX = 0f, shootY = 3f;
/** offsets of weapon position on unit */ /** offsets of weapon position on unit */
public float x = 5f, y = 0f; public float x = 5f, y = 0f;
/** Random spread on the X axis. */ /** Random spread on the X/Y axis. */
public float xRand = 0f; public float xRand = 0f, yRand = 0f;
/** pattern used for bullets */ /** pattern used for bullets */
public ShootPattern shoot = new ShootPattern(); public ShootPattern shoot = new ShootPattern();
/** radius of shadow drawn under the weapon; <0 to disable */ /** radius of shadow drawn under the weapon; <0 to disable */
public float shadow = -1f; public float shadow = -1f;
/** fraction of velocity that is random */ /** fraction of velocity that is random */
public float velocityRnd = 0f; public float velocityRnd = 0f;
/** extra velocity that is added as a fraction */
public float extraVelocity = 0f;
/** The half-radius of the cone in which shooting will start. */ /** The half-radius of the cone in which shooting will start. */
public float shootCone = 5f; public float shootCone = 5f;
/** Cone in which the weapon can rotate relative to its mount. */ /** Cone in which the weapon can rotate relative to its mount. */
@ -234,7 +236,9 @@ public class Weapon implements Cloneable{
} }
} }
Draw.xscl = -Mathf.sign(flipSprite); float prev = Draw.xscl;
Draw.xscl *= -Mathf.sign(flipSprite);
//fix color //fix color
unit.type.applyColor(unit); unit.type.applyColor(unit);
@ -255,7 +259,7 @@ public class Weapon implements Cloneable{
Draw.color(); Draw.color();
} }
Draw.xscl = 1f; Draw.xscl = prev;
if(parts.size > 0){ if(parts.size > 0){
//TODO does it need an outline? //TODO does it need an outline?
@ -479,17 +483,18 @@ public class Weapon implements Cloneable{
mount.charging = false; mount.charging = false;
float float
xSpread = Mathf.range(xRand), xSpread = Mathf.range(xRand),
ySpread = Mathf.range(yRand),
weaponRotation = unit.rotation - 90 + (rotate ? mount.rotation : baseRotation), weaponRotation = unit.rotation - 90 + (rotate ? mount.rotation : baseRotation),
mountX = unit.x + Angles.trnsx(unit.rotation - 90, x, y), mountX = unit.x + Angles.trnsx(unit.rotation - 90, x, y),
mountY = unit.y + Angles.trnsy(unit.rotation - 90, x, y), mountY = unit.y + Angles.trnsy(unit.rotation - 90, x, y),
bulletX = mountX + Angles.trnsx(weaponRotation, this.shootX + xOffset + xSpread, this.shootY + yOffset), bulletX = mountX + Angles.trnsx(weaponRotation, this.shootX + xOffset + xSpread, this.shootY + yOffset + ySpread),
bulletY = mountY + Angles.trnsy(weaponRotation, this.shootX + xOffset + xSpread, this.shootY + yOffset), bulletY = mountY + Angles.trnsy(weaponRotation, this.shootX + xOffset + xSpread, this.shootY + yOffset + ySpread),
shootAngle = bulletRotation(unit, mount, bulletX, bulletY) + angleOffset, shootAngle = bulletRotation(unit, mount, bulletX, bulletY) + angleOffset,
lifeScl = bullet.scaleLife ? Mathf.clamp(Mathf.dst(bulletX, bulletY, mount.aimX, mount.aimY) / bullet.range) : 1f, lifeScl = bullet.scaleLife ? Mathf.clamp(Mathf.dst(bulletX, bulletY, mount.aimX, mount.aimY) / bullet.range) : 1f,
angle = shootAngle + Mathf.range(inaccuracy + bullet.inaccuracy); angle = shootAngle + Mathf.range(inaccuracy + bullet.inaccuracy);
Entityc shooter = unit.controller() instanceof MissileAI ai ? ai.shooter : unit; //Pass the missile's shooter down to its bullets Entityc shooter = unit.controller() instanceof MissileAI ai ? ai.shooter : unit; //Pass the missile's shooter down to its bullets
mount.bullet = bullet.create(unit, shooter, unit.team, bulletX, bulletY, angle, -1f, (1f - velocityRnd) + Mathf.random(velocityRnd), lifeScl, null, mover, mount.aimX, mount.aimY, mount.target); mount.bullet = bullet.create(unit, shooter, unit.team, bulletX, bulletY, angle, -1f, (1f - velocityRnd) + Mathf.random(velocityRnd) + extraVelocity, lifeScl, null, mover, mount.aimX, mount.aimY, mount.target);
handleBullet(unit, mount, mount.bullet); handleBullet(unit, mount, mount.bullet);
if(!continuous){ if(!continuous){

View file

@ -18,6 +18,7 @@ public class MissileUnitType extends UnitType{
logicControllable = false; logicControllable = false;
isEnemy = false; isEnemy = false;
useUnitCap = false; useUnitCap = false;
drawCell = false;
allowedInPayloads = false; allowedInPayloads = false;
controller = u -> new MissileAI(); controller = u -> new MissileAI();
flying = true; flying = true;

View file

@ -0,0 +1,46 @@
package mindustry.type.weapons;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.type.*;
/** Fires a bullet to intercept enemy bullets. The fired bullet MUST be of type InterceptorBulletType. */
public class PointDefenseBulletWeapon extends Weapon{
public float damageTargetWeight = 10f;
public PointDefenseBulletWeapon(String name){
super(name);
}
public PointDefenseBulletWeapon(){
}
{
autoTarget = true;
controllable = false;
rotate = true;
useAmmo = false;
useAttackRange = false;
targetInterval = targetSwitchInterval = 5f;
}
@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.type.collidesAir && !b.type.collidesTiles), b -> b.dst2(x, y) - b.damage * damageTargetWeight);
}
@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 b && b.type != null && b.type.hittable);
}
@Override
protected void handleBullet(Unit unit, WeaponMount mount, Bullet bullet){
super.handleBullet(unit, mount, bullet);
if(mount.target instanceof Bullet b){
bullet.data = b;
}
}
}

Some files were not shown because too many files have changed in this diff Show more