From b7dbe54d7622a99154c00141d8756cc14a1479c0 Mon Sep 17 00:00:00 2001 From: Anuken Date: Fri, 4 Apr 2025 11:47:35 -0400 Subject: [PATCH] Merging changes from private branch --- .gitignore | 1 + CONTRIBUTING.md | 10 +- .../mindustry/annotations/Annotations.java | 2 + .../annotations/entity/EntityProcess.java | 112 +++++--- .../mindustry/annotations/util/Stype.java | 4 + build.gradle | 7 +- core/assets/shaders/arkycite.frag | 6 +- core/assets/shaders/slag.frag | 12 +- core/src/mindustry/ClientLauncher.java | 5 + core/src/mindustry/Vars.java | 11 +- core/src/mindustry/ai/BlockIndexer.java | 186 +++++++++--- core/src/mindustry/ai/ControlPathfinder.java | 15 - core/src/mindustry/ai/Pathfinder.java | 269 ++++++++++++------ core/src/mindustry/ai/RtsAI.java | 13 +- core/src/mindustry/ai/UnitCommand.java | 7 - core/src/mindustry/ai/UnitStance.java | 11 +- core/src/mindustry/ai/WaveSpawner.java | 20 +- core/src/mindustry/ai/types/BuilderAI.java | 5 +- core/src/mindustry/ai/types/CommandAI.java | 4 +- core/src/mindustry/ai/types/DefenderAI.java | 2 +- core/src/mindustry/ai/types/HugAI.java | 1 - core/src/mindustry/ai/types/MinerAI.java | 13 +- core/src/mindustry/async/PhysicsProcess.java | 9 +- core/src/mindustry/core/Logic.java | 11 +- core/src/mindustry/core/NetServer.java | 1 + core/src/mindustry/core/PerfCounter.java | 53 ++++ core/src/mindustry/core/Renderer.java | 4 +- core/src/mindustry/core/World.java | 10 +- core/src/mindustry/editor/EditorTile.java | 4 +- .../mindustry/editor/MapObjectivesDialog.java | 19 +- core/src/mindustry/editor/WaveInfoDialog.java | 7 + core/src/mindustry/entities/Damage.java | 21 +- .../mindustry/entities/EntityCollisions.java | 2 +- core/src/mindustry/entities/Lightning.java | 4 +- core/src/mindustry/entities/Puddles.java | 8 +- .../mindustry/entities/TargetPriority.java | 4 +- core/src/mindustry/entities/UnitSorts.java | 6 + core/src/mindustry/entities/Units.java | 22 +- .../mindustry/entities/abilities/Ability.java | 16 ++ .../abilities/LiquidRegenAbility.java | 6 +- .../entities/abilities/MoveEffectAbility.java | 19 +- .../mindustry/entities/bullet/BulletType.java | 73 ++++- .../bullet/InterceptorBulletType.java | 52 ++++ .../entities/bullet/MultiBulletType.java | 61 ++++ .../entities/bullet/SapBulletType.java | 9 +- .../entities/comp/BlockUnitComp.java | 5 + .../mindustry/entities/comp/BoundedComp.java | 58 ---- .../mindustry/entities/comp/BuilderComp.java | 2 +- .../mindustry/entities/comp/BuildingComp.java | 151 +++++----- .../mindustry/entities/comp/BulletComp.java | 120 +++++--- .../mindustry/entities/comp/CrawlComp.java | 13 +- .../entities/comp/ElevationMoveComp.java | 4 +- .../mindustry/entities/comp/EntityComp.java | 8 +- .../mindustry/entities/comp/FlyingComp.java | 123 -------- .../src/mindustry/entities/comp/LegsComp.java | 12 +- .../src/mindustry/entities/comp/MechComp.java | 4 +- .../mindustry/entities/comp/PhysicsComp.java | 2 +- .../mindustry/entities/comp/PuddleComp.java | 6 +- .../mindustry/entities/comp/SegmentComp.java | 158 ++++++++++ .../mindustry/entities/comp/StatusComp.java | 24 +- .../src/mindustry/entities/comp/TankComp.java | 32 ++- .../entities/comp/UnderwaterMoveComp.java | 39 +++ .../src/mindustry/entities/comp/UnitComp.java | 181 ++++++++++-- core/src/mindustry/entities/comp/VelComp.java | 4 + .../entities/comp/WaterCrawlComp.java | 38 +++ .../entities/comp/WaterMoveComp.java | 19 +- .../mindustry/entities/part/RegionPart.java | 25 +- .../entities/pattern/ShootHelix.java | 14 + .../entities/pattern/ShootSpread.java | 4 + .../entities/units/AIController.java | 35 ++- .../entities/units/UnitController.java | 4 - core/src/mindustry/game/EventType.java | 19 ++ core/src/mindustry/game/FogControl.java | 27 ++ core/src/mindustry/game/MapObjectives.java | 45 +++ core/src/mindustry/game/SpawnGroup.java | 21 +- core/src/mindustry/game/Team.java | 18 +- core/src/mindustry/game/Teams.java | 16 +- .../src/mindustry/graphics/BlockRenderer.java | 48 +++- core/src/mindustry/graphics/CacheLayer.java | 13 +- core/src/mindustry/graphics/Drawf.java | 28 +- .../src/mindustry/graphics/FloorRenderer.java | 149 +++++----- .../src/mindustry/graphics/LightRenderer.java | 2 + core/src/mindustry/graphics/MenuRenderer.java | 33 +-- .../mindustry/graphics/MinimapRenderer.java | 18 +- .../mindustry/graphics/OverlayRenderer.java | 2 +- core/src/mindustry/graphics/Pal.java | 2 + .../mindustry/graphics/g3d/PlanetParams.java | 2 - core/src/mindustry/input/DesktopInput.java | 2 +- core/src/mindustry/input/InputHandler.java | 15 +- core/src/mindustry/input/MobileInput.java | 2 +- core/src/mindustry/io/JsonIO.java | 1 + core/src/mindustry/io/SaveVersion.java | 8 +- core/src/mindustry/logic/LExecutor.java | 19 +- core/src/mindustry/mod/ContentParser.java | 2 + core/src/mindustry/mod/Mods.java | 17 +- core/src/mindustry/service/GameService.java | 2 +- core/src/mindustry/type/UnitType.java | 165 ++++++++--- core/src/mindustry/type/Weapon.java | 21 +- .../mindustry/type/unit/MissileUnitType.java | 1 + .../weapons/PointDefenseBulletWeapon.java | 46 +++ core/src/mindustry/ui/Fonts.java | 78 +++-- core/src/mindustry/ui/Minimap.java | 2 +- .../mindustry/ui/dialogs/DatabaseDialog.java | 2 +- core/src/mindustry/ui/dialogs/ModsDialog.java | 2 +- .../ui/fragments/ConsoleFragment.java | 1 + .../mindustry/ui/fragments/HudFragment.java | 38 --- .../ui/fragments/MinimapFragment.java | 2 +- .../ui/fragments/PlacementFragment.java | 2 +- core/src/mindustry/world/Block.java | 50 +++- core/src/mindustry/world/Build.java | 20 +- core/src/mindustry/world/CachedTile.java | 6 +- core/src/mindustry/world/Edges.java | 2 +- core/src/mindustry/world/Tile.java | 10 +- core/src/mindustry/world/Tiles.java | 1 - .../world/blocks/campaign/LandingPad.java | 1 - .../mindustry/world/blocks/defense/Door.java | 2 +- .../world/blocks/defense/MendProjector.java | 2 + .../blocks/defense/OverdriveProjector.java | 3 - .../world/blocks/defense/RegenProjector.java | 4 +- .../world/blocks/defense/turrets/Turret.java | 29 +- .../blocks/distribution/ArmoredConveyor.java | 2 +- .../world/blocks/distribution/Duct.java | 25 +- .../blocks/distribution/DuctJunction.java | 179 ++++++++++++ .../world/blocks/distribution/DuctRouter.java | 2 +- .../world/blocks/distribution/MassDriver.java | 2 +- .../blocks/distribution/OverflowDuct.java | 2 +- .../world/blocks/distribution/Router.java | 2 +- .../blocks/distribution/StackRouter.java | 2 +- .../world/blocks/environment/Floor.java | 10 +- .../world/blocks/environment/SteamVent.java | 14 + .../world/blocks/heat/HeatProducer.java | 3 + .../world/blocks/logic/MessageBlock.java | 2 +- .../world/blocks/payloads/BuildPayload.java | 2 +- .../world/blocks/payloads/PayloadBlock.java | 2 +- .../world/blocks/payloads/PayloadLoader.java | 2 +- .../blocks/payloads/PayloadMassDriver.java | 6 +- .../world/blocks/payloads/UnitPayload.java | 1 + .../mindustry/world/blocks/power/Battery.java | 4 - .../world/blocks/power/LightBlock.java | 2 +- .../world/blocks/power/NuclearReactor.java | 1 + .../world/blocks/power/PowerNode.java | 8 +- .../world/blocks/power/ThermalGenerator.java | 3 +- .../world/blocks/storage/CoreBlock.java | 17 +- .../world/blocks/units/Reconstructor.java | 11 +- .../world/blocks/units/RepairTurret.java | 2 +- .../world/blocks/units/UnitAssembler.java | 2 +- .../blocks/units/UnitAssemblerModule.java | 8 +- .../world/blocks/units/UnitCargoLoader.java | 5 - .../world/blocks/units/UnitFactory.java | 5 - core/src/mindustry/world/draw/DrawFlame.java | 3 +- core/src/mindustry/world/draw/DrawPlasma.java | 2 + core/src/mindustry/world/meta/BlockFlag.java | 4 +- core/src/mindustry/world/meta/StatValues.java | 10 +- desktop/build.gradle | 1 + .../mindustry/desktop/DesktopLauncher.java | 1 + ios/build.gradle | 2 +- server/build.gradle | 1 + .../src/mindustry/server/ServerControl.java | 9 - .../java/power/ConsumeGeneratorTests.java | 2 +- tools/src/mindustry/tools/Generators.java | 34 ++- tools/src/mindustry/tools/ImagePacker.java | 3 +- 161 files changed, 2484 insertions(+), 1137 deletions(-) create mode 100644 core/src/mindustry/core/PerfCounter.java create mode 100644 core/src/mindustry/entities/bullet/InterceptorBulletType.java create mode 100644 core/src/mindustry/entities/bullet/MultiBulletType.java delete mode 100644 core/src/mindustry/entities/comp/BoundedComp.java delete mode 100644 core/src/mindustry/entities/comp/FlyingComp.java create mode 100644 core/src/mindustry/entities/comp/SegmentComp.java create mode 100644 core/src/mindustry/entities/comp/UnderwaterMoveComp.java create mode 100644 core/src/mindustry/entities/comp/WaterCrawlComp.java create mode 100644 core/src/mindustry/type/weapons/PointDefenseBulletWeapon.java create mode 100644 core/src/mindustry/world/blocks/distribution/DuctJunction.java diff --git a/.gitignore b/.gitignore index dd3f2a0f06..4ebcbb7233 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ ios/libs/ /tools/build/ /tests/build/ /server/build/ +ios/libs/ changelog saves/ /core/assets-raw/fontgen/out/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99086e0da2..02a6643f3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. 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. +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 ### 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. -### 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. 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. ### 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. diff --git a/annotations/src/main/java/mindustry/annotations/Annotations.java b/annotations/src/main/java/mindustry/annotations/Annotations.java index 6aa8d5b9d9..2d6a03dc5c 100644 --- a/annotations/src/main/java/mindustry/annotations/Annotations.java +++ b/annotations/src/main/java/mindustry/annotations/Annotations.java @@ -54,6 +54,8 @@ public class Annotations{ /** 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. */ 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. */ diff --git a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java index a6b1d95d67..bd0e7a4630 100644 --- a/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java +++ b/annotations/src/main/java/mindustry/annotations/entity/EntityProcess.java @@ -19,6 +19,7 @@ import javax.annotation.processing.*; import javax.lang.model.element.*; import javax.lang.model.type.*; import java.lang.annotation.*; +import java.util.*; @SupportedAnnotationTypes({ "mindustry.annotations.Annotations.EntityDef", @@ -97,6 +98,8 @@ public class EntityProcess extends BaseProcessor{ //create component interfaces for(Stype component : allComponents){ + + TypeSpec.Builder inter = TypeSpec.interfaceBuilder(interfaceName(component)) .addModifiers(Modifier.PUBLIC).addAnnotation(EntityInterface.class); @@ -116,45 +119,47 @@ public class EntityProcess extends BaseProcessor{ inter.addSuperinterface(ClassName.get(packageName, interfaceName(type))); } - ObjectSet signatures = new ObjectSet<>(); + if(component.annotation(Component.class).genInterface()){ + ObjectSet signatures = new ObjectSet<>(); - //add utility methods to interface - for(Smethod method : component.methods()){ - //skip private methods, those are for internal use. - if(method.isAny(Modifier.PRIVATE, Modifier.STATIC)) continue; + //add utility methods to interface + for(Smethod method : component.methods()){ + //skip private methods, those are for internal use. + if(method.isAny(Modifier.PRIVATE, Modifier.STATIC)) continue; - //keep track of signatures used to prevent dupes - signatures.add(method.e.toString()); + //keep track of signatures used to prevent dupes + signatures.add(method.e.toString()); - inter.addMethod(MethodSpec.methodBuilder(method.name()) - .addJavadoc(method.doc() == null ? "" : method.doc()) - .addExceptions(method.thrownt()) - .addTypeVariables(method.typeVariables().map(TypeVariableName::get)) - .returns(method.ret().toString().equals("void") ? TypeName.VOID : method.retn()) - .addParameters(method.params().map(v -> ParameterSpec.builder(v.tname(), v.name()) - .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()); + inter.addMethod(MethodSpec.methodBuilder(method.name()) + .addJavadoc(method.doc() == null ? "" : method.doc()) + .addExceptions(method.thrownt()) + .addTypeVariables(method.typeVariables().map(TypeVariableName::get)) + .returns(method.ret().toString().equals("void") ? TypeName.VOID : method.retn()) + .addParameters(method.params().map(v -> ParameterSpec.builder(v.tname(), v.name()) + .build())).addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT).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()); + //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 + 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 for(ObjectMap.Entry> 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 - if(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 + "."); + //there are multiple @Replace implementations, or multiple non-void implementations. + 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)){ + + //remove clutter + entry.value.removeAll(s -> s.is(Modifier.ABSTRACT)); + + Comparator 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()))); diff --git a/annotations/src/main/java/mindustry/annotations/util/Stype.java b/annotations/src/main/java/mindustry/annotations/util/Stype.java index 1b53213733..a70a6276de 100644 --- a/annotations/src/main/java/mindustry/annotations/util/Stype.java +++ b/annotations/src/main/java/mindustry/annotations/util/Stype.java @@ -28,6 +28,10 @@ public class Stype extends Selement{ return interfaces().flatMap(s -> s.allInterfaces().add(s)).distinct(); } + public boolean isInterface(){ + return e.getKind() == ElementKind.INTERFACE; + } + public Seq superclasses(){ return Seq.with(BaseProcessor.typeu.directSupertypes(mirror())).map(Stype::of); } diff --git a/build.gradle b/build.gradle index 90e98a89dc..d67a67aa0a 100644 --- a/build.gradle +++ b/build.gradle @@ -26,8 +26,8 @@ buildscript{ } plugins{ - id "org.jetbrains.kotlin.jvm" version "1.6.0" - id "org.jetbrains.kotlin.kapt" version "1.6.0" + id "org.jetbrains.kotlin.jvm" version "2.1.10" + id "org.jetbrains.kotlin.kapt" version "2.1.10" } allprojects{ @@ -37,7 +37,7 @@ allprojects{ group = 'com.github.Anuken' ext{ - versionNumber = '7' + versionNumber = '8' if(!project.hasProperty("versionModifier")) versionModifier = 'release' if(!project.hasProperty("versionType")) versionType = 'official' appName = 'Mindustry' @@ -366,7 +366,6 @@ project(":core"){ //these are completely unnecessary tasks.kaptGenerateStubsKotlin.onlyIf{ false } tasks.compileKotlin.onlyIf{ false } - tasks.inspectClassesForKotlinIC.onlyIf{ false } } //comp** classes are only used for code generation diff --git a/core/assets/shaders/arkycite.frag b/core/assets/shaders/arkycite.frag index 863b3f6d64..2cee1a7b8e 100644 --- a/core/assets/shaders/arkycite.frag +++ b/core/assets/shaders/arkycite.frag @@ -46,9 +46,5 @@ void main(){ color.rgb = S2; } - if(orig.g > 0.01){ - color = max(S1, color); - } - - gl_FragColor = color; + gl_FragColor = vec4(max(S1, color).rgb, orig.a); } diff --git a/core/assets/shaders/slag.frag b/core/assets/shaders/slag.frag index 2750d32ecd..696dcdf884 100755 --- a/core/assets/shaders/slag.frag +++ b/core/assets/shaders/slag.frag @@ -15,16 +15,22 @@ uniform float u_time; varying vec2 v_texCoords; void main(){ - vec2 c = v_texCoords.xy; - vec2 coords = vec2(c.x * u_resolution.x + u_campos.x, c.y * u_resolution.y + u_campos.y); + vec2 coords = v_texCoords * u_resolution + u_campos; 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; + + //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); if(noise > 0.6){ color.rgb = S2; - }else if (noise > 0.54){ + }else if(noise > 0.54){ color.rgb = S1; } diff --git a/core/src/mindustry/ClientLauncher.java b/core/src/mindustry/ClientLauncher.java index 3e2fbca7df..8990cb0200 100644 --- a/core/src/mindustry/ClientLauncher.java +++ b/core/src/mindustry/ClientLauncher.java @@ -55,6 +55,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform Log.info("[GL] Version: @", graphics.getGLVersion()); Log.info("[GL] Max texture size: @", maxTextureSize); 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()){ Log.info("[GL] Total available VRAM: @mb", NvGpuInfo.getMaxMemoryKB()/1024); } @@ -206,6 +207,8 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform @Override public void update(){ + PerfCounter.update.begin(); + int targetfps = Core.settings.getInt("fpscap", 120); boolean changed = lastTargetFps != targetfps && lastTargetFps != -1; 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)); } } + + PerfCounter.update.end(); } @Override diff --git a/core/src/mindustry/Vars.java b/core/src/mindustry/Vars.java index 8fef2f1616..0043e61df6 100644 --- a/core/src/mindustry/Vars.java +++ b/core/src/mindustry/Vars.java @@ -28,7 +28,6 @@ import mindustry.net.*; import mindustry.service.*; import mindustry.ui.dialogs.*; import mindustry.world.*; -import mindustry.world.blocks.storage.*; import mindustry.world.meta.*; import java.io.*; @@ -47,6 +46,10 @@ public class Vars implements Loadable{ public static boolean loadedLogger = false, loadedFileLogger = false; /** Name of current Steam player. */ 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. */ public static boolean forceBeServers = false; /** 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"; /** URL for discord invite. */ 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/"; /** 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"}; @@ -111,8 +114,6 @@ public class Vars implements Loadable{ public static final float invasionGracePeriod = 20; /** min armor fraction damage; e.g. 0.05 = at least 5% damage */ 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 */ public static final int tilesize = 8; /** size of one tile payload (^2) */ @@ -267,7 +268,7 @@ public class Vars implements Loadable{ public static NetServer netServer; public static NetClient netClient; - public static Player player; + public static @Nullable Player player; @Override public void loadAsync(){ diff --git a/core/src/mindustry/ai/BlockIndexer.java b/core/src/mindustry/ai/BlockIndexer.java index 7f944a3eb0..1206dc71d1 100644 --- a/core/src/mindustry/ai/BlockIndexer.java +++ b/core/src/mindustry/ai/BlockIndexer.java @@ -7,6 +7,8 @@ import arc.math.geom.*; import arc.struct.*; import arc.util.*; import mindustry.content.*; +import mindustry.entities.*; +import mindustry.entities.Units.*; import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.game.Teams.*; @@ -14,6 +16,7 @@ import mindustry.gen.*; import mindustry.logic.*; import mindustry.type.*; import mindustry.world.*; +import mindustry.world.blocks.environment.*; import mindustry.world.meta.*; import static mindustry.Vars.*; @@ -28,11 +31,13 @@ public class BlockIndexer{ private int quadWidth, quadHeight; /** Stores all ore quadrants on the map. Maps ID to qX to qY to a list of tiles with that ore. */ - private IntSeq[][][] ores; + private IntSeq[][][] ores, wallOres; /** Stores all damaged tile entities by team. */ private Seq[] damagedTiles = new Seq[Team.all.length]; + /** All ores present on the map - can be wall or floor. */ + private Seq allPresentOres = new Seq<>(); /** All ores available on this map. */ - private ObjectIntMap allOres = new ObjectIntMap<>(); + private ObjectIntMap allOres = new ObjectIntMap<>(), allWallOres = new ObjectIntMap<>(); /** Stores teams that are present here as tiles. */ private Seq activeTeams = new Seq<>(Team.class); /** Maps teams to a map of flagged tiles by flag. */ @@ -41,6 +46,8 @@ public class BlockIndexer{ private boolean[] blocksPresent; /** Array used for returning and reusing. */ private Seq breturnArray = new Seq<>(Building.class); + /** Maps block flag to a list of floor tiles that have it. */ + private Seq[] floorMap; public BlockIndexer(){ clearFlags(); @@ -53,15 +60,23 @@ public class BlockIndexer{ addIndex(event.tile); }); + Events.on(TileFloorChangeEvent.class, event -> { + removeFloorIndex(event.tile, event.previous); + addFloorIndex(event.tile, event.floor); + }); + Events.on(WorldLoadEvent.class, event -> { damagedTiles = new Seq[Team.all.length]; flagMap = new Seq[Team.all.length][BlockFlag.all.length]; + floorMap = new Seq[BlockFlag.all.length]; activeTeams = new Seq<>(Team.class); clearFlags(); allOres.clear(); + allWallOres.clear(); ores = new IntSeq[content.items().size][][]; + wallOres = new IntSeq[content.items().size][][]; quadWidth = Mathf.ceil(world.width() / (float)quadrantSize); quadHeight = Mathf.ceil(world.height() / (float)quadrantSize); blocksPresent = new boolean[content.blocks().size]; @@ -78,28 +93,67 @@ public class BlockIndexer{ for(Tile tile : world.tiles){ process(tile); - var drop = tile.drop(); + addFloorIndex(tile, tile.floor()); - if(drop != null){ - int qx = (tile.x / quadrantSize); - int qy = (tile.y / quadrantSize); - - //add position of quadrant to list - if(tile.block() == Blocks.air){ - if(ores[drop.id] == null){ - ores[drop.id] = new IntSeq[quadWidth][quadHeight]; - } - if(ores[drop.id][qx][qy] == null){ - ores[drop.id][qx][qy] = new IntSeq(false, 16); - } + Item drop; + int qx = tile.x / quadrantSize, qy = tile.y / quadrantSize; + if(tile.block() == Blocks.air){ + if((drop = tile.drop()) != null){ + //add position of quadrant to list + if(ores[drop.id] == null) ores[drop.id] = new IntSeq[quadWidth][quadHeight]; + if(ores[drop.id][qx][qy] == null) ores[drop.id][qx][qy] = new IntSeq(false, 16); ores[drop.id][qx][qy].add(tile.pos()); 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 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 getFlaggedFloors(BlockFlag flag){ + if(floorMap[flag.ordinal()] == null){ + floorMap[flag.ordinal()] = new Seq<>(false); + } + return floorMap[flag.ordinal()]; + } + public void removeIndex(Tile tile){ var team = tile.team(); if(tile.build != null && tile.isCenter()){ @@ -143,30 +197,37 @@ public class BlockIndexer{ public void addIndex(Tile tile){ process(tile); - var drop = tile.drop(); - if(drop != null && ores != null){ - int qx = tile.x / quadrantSize; - int qy = tile.y / quadrantSize; + Item drop = tile.drop(), wallDrop = tile.wallDrop(); + if(drop == null && wallDrop == null) return; + int qx = tile.x / quadrantSize, qy = tile.y / quadrantSize; + int pos = tile.pos(); - if(ores[drop.id] == null){ - ores[drop.id] = new IntSeq[quadWidth][quadHeight]; - } - if(ores[drop.id][qx][qy] == null){ - ores[drop.id][qx][qy] = new IntSeq(false, 16); - } - - int pos = tile.pos(); - var seq = ores[drop.id][qx][qy]; - - 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); + if(tile.block() == Blocks.air){ + if(drop != null){ //floor + if(ores[drop.id] == null) ores[drop.id] = new IntSeq[quadWidth][quadHeight]; + if(ores[drop.id][qx][qy] == null) ores[drop.id][qx][qy] = new IntSeq(false, 16); + 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(); } - }else if(seq.contains(pos)){ //otherwise, it likely became blocked, remove it - seq.removeValue(pos); - allOres.increment(drop, -1); + } + if(wallDrop != null && wallOres != null && wallOres[wallDrop.id] != null && wallOres[wallDrop.id][qx][qy] != null && wallOres[wallDrop.id][qx][qy].removeValue(pos)){ //wall + 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 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. */ public Seq getDamaged(Team team){ if(damagedTiles[team.id] == null){ @@ -348,7 +414,7 @@ public class BlockIndexer{ breturnArray.size = 0; } - public Building findEnemyTile(Team team, float x, float y, float range, Boolf pred){ + public Building findEnemyTile(Team team, float x, float y, float range, BuildingPriorityf priority, Boolf pred){ Building target = null; float targetDist = 0; @@ -362,10 +428,10 @@ public class BlockIndexer{ //if a block has the same priority, the closer one should be targeted float dist = candidate.dst(x, y) - candidate.hitSize() / 2f; if(target == null || - //if its closer and is at least equal priority - (dist < targetDist && candidate.block.priority >= target.block.priority) || - // block has higher priority (so range doesnt matter) - (candidate.block.priority > target.block.priority)){ + //if it is closer and is at least equal priority + (dist < targetDist && priority.priority(candidate) >= priority.priority(target)) || + // block has higher priority (so range doesn't matter) + priority.priority(candidate) > priority.priority(target)){ target = candidate; targetDist = dist; } @@ -374,6 +440,10 @@ public class BlockIndexer{ return target; } + public Building findEnemyTile(Team team, float x, float y, float range, Boolf pred){ + return findEnemyTile(team, x, y, range, UnitSorts.buildingDefault, pred); + } + public Building findTile(Team team, float x, float y, float range, Boolf pred){ return findTile(team, x, y, range, pred, false); } @@ -432,11 +502,43 @@ public class BlockIndexer{ 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. */ public Tile findClosestOre(Unit unit, Item 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){ var team = tile.team(); //only process entity changes with centered tiles diff --git a/core/src/mindustry/ai/ControlPathfinder.java b/core/src/mindustry/ai/ControlPathfinder.java index ba9e3e6c75..ece85f2792 100644 --- a/core/src/mindustry/ai/ControlPathfinder.java +++ b/core/src/mindustry/ai/ControlPathfinder.java @@ -1082,21 +1082,6 @@ public class ControlPathfinder implements Runnable{ 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){ return getPathPosition(unit, destination, destination, out, noResultFound); } diff --git a/core/src/mindustry/ai/Pathfinder.java b/core/src/mindustry/ai/Pathfinder.java index 615568aed7..e9557655c6 100644 --- a/core/src/mindustry/ai/Pathfinder.java +++ b/core/src/mindustry/ai/Pathfinder.java @@ -5,6 +5,7 @@ import arc.func.*; import arc.math.*; import arc.math.geom.*; import arc.struct.*; +import arc.util.TaskQueue; import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.core.*; @@ -16,11 +17,14 @@ import mindustry.world.blocks.environment.*; import mindustry.world.blocks.storage.*; import mindustry.world.meta.*; +import java.util.*; + import static mindustry.Vars.*; import static mindustry.world.meta.BlockFlag.*; public class Pathfinder implements Runnable{ 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 updateInterval = 1000 / updateFPS; @@ -30,51 +34,66 @@ public class Pathfinder implements Runnable{ static final int impassable = -1; public static final int - fieldCore = 0; + fieldCore = 0, + maxFields = 10; public static final Seq> fieldTypes = Seq.with( - EnemyCoreField::new + EnemyCoreField::new ); public static final int - costGround = 0, - costLegs = 1, - costNaval = 2, - costHover = 3; + costGround = 0, + costLegs = 1, + costNaval = 2, + costNeoplasm = 3, + costNone = 4, + costHover = 5, + + maxCosts = 8; public static final Seq costTypes = Seq.with( - //ground - (team, tile) -> - (PathTile.allDeep(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) + - (PathTile.nearLiquid(tile) ? 6 : 0) + - (PathTile.deep(tile) ? 6000 : 0) + - (PathTile.damages(tile) ? 30 : 0), + //ground + (team, tile) -> + (PathTile.allDeep(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) + + (PathTile.nearLiquid(tile) ? 6 : 0) + + (PathTile.deep(tile) ? 6000 : 0) + + (PathTile.damages(tile) ? 30 : 0), - //legs - (team, tile) -> - PathTile.legSolid(tile) ? impassable : 1 + - (PathTile.deep(tile) ? 6000 : 0) + //leg units can now drown - (PathTile.solid(tile) ? 5 : 0), + //legs + (team, tile) -> + PathTile.legSolid(tile) ? impassable : 1 + + (PathTile.deep(tile) ? 6000 : 0) + //leg units can now drown + (PathTile.solid(tile) ? 5 : 0), - //water - (team, tile) -> - (!PathTile.liquid(tile) ? 6000 : 1) + - PathTile.health(tile) * 5 + - (PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) + - (PathTile.deep(tile) ? 0 : 1) + - (PathTile.damages(tile) ? 35 : 0), + //water + (team, tile) -> + (!PathTile.liquid(tile) ? 6000 : 1) + + PathTile.health(tile) * 5 + + (PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 14 : 0) + + (PathTile.deep(tile) ? 0 : 1) + + (PathTile.damages(tile) ? 35 : 0), - //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) + //neoplasm veins + (team, tile) -> + (PathTile.deep(tile) || (PathTile.team(tile) == 0 && PathTile.solid(tile))) ? impassable : 1 + + (PathTile.health(tile) * 3) + + (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 */ - int[] tiles = new int[0]; + int[] tiles = {}; /** maps team, cost, type to flow field*/ Flowfield[][][] cache; @@ -86,6 +105,8 @@ public class Pathfinder implements Runnable{ @Nullable Thread thread; IntSeq tmpArray = new IntSeq(); + boolean needsRefresh; + public Pathfinder(){ clearCache(); @@ -100,6 +121,7 @@ public class Pathfinder implements Runnable{ mainList = new Seq<>(); clearCache(); + for(int i = 0; i < tiles.length; i++){ Tile tile = world.tiles.geti(i); 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(){ - cache = new Flowfield[256][5][5]; + cache = new Flowfield[256][maxCosts][maxFields]; } /** Packs a tile into its internal representation. */ @@ -185,19 +233,19 @@ public class Pathfinder implements Runnable{ int tid = tile.getTeamID(); return PathTile.get( - 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 - solid, - tile.floor().isLiquid, - tile.legSolid(), - nearLiquid, - nearGround, - nearSolid, - nearLegSolid, - tile.floor().isDeep(), - tile.floor().damageTaken > 0.00001f, - allDeep, - tile.block().teamPassable + 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 + solid, + tile.floor().isLiquid, + tile.legSolid(), + nearLiquid, + nearGround, + nearSolid, + nearLegSolid, + tile.floor().isDeep(), + tile.floor().damageTaken > 0.00001f, + allDeep, + tile.block().teamPassable ); } @@ -223,6 +271,7 @@ public class Pathfinder implements Runnable{ thread = null; } queue.clear(); + needsRefresh = false; } /** 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); + + //queue a refresh sometime in the future + needsRefresh = true; } /** Thread implementation. */ @@ -307,48 +343,55 @@ public class Pathfinder implements Runnable{ /** Gets next tile to travel to. Main thread only. */ 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; //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; } //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(); tmpArray.clear(); path.getPositions(tmpArray); synchronized(path.targets){ - //make sure the position actually changed - if(!(path.targets.size == 1 && tmpArray.size == 1 && path.targets.first() == tmpArray.first())){ - path.updateTargetPositions(); + path.updateTargetPositions(); - //queue an update - queue.post(() -> updateTargets(path)); - } + //queue an update + queue.post(() -> updateTargets(path)); } } //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 apos = tile.array(); + int res = path.resolution; + int ww = path.width; + int apos = tile.x/res + tile.y/res * ww; int value = values[apos]; + var points = diagonals ? Geometry.d8 : Geometry.d4; + Tile current = null; int tl = 0; - for(Point2 point : Geometry.d8){ - int dx = tile.x + point.x, dy = tile.y + point.y; + for(Point2 point : points){ + int dx = tile.x + point.x * res, dy = tile.y + point.y * res; Tile other = world.tile(dx, dy); 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) && - !(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; tl = values[packed]; } @@ -365,13 +408,21 @@ public class Pathfinder implements Runnable{ //increment search, but do not clear the frontier path.search++; + //search overflow; reset everything. + if(path.search >= Short.MAX_VALUE){ + Arrays.fill(path.searches, (short)0); + path.search = 1; + } + synchronized(path.targets){ //add targets for(int i = 0; i < path.targets.size; i++){ int pos = path.targets.get(i); + if(pos >= path.weights.length) continue; + path.weights[pos] = 0; - path.searches[pos] = path.search; + path.searches[pos] = (short)path.search; path.frontier.addFirst(pos); } } @@ -390,7 +441,7 @@ public class Pathfinder implements Runnable{ */ private void registerPath(Flowfield path){ path.lastUpdateTime = Time.millis(); - path.setup(tiles.length); + path.setup(); threadList.add(path); @@ -398,9 +449,7 @@ public class Pathfinder implements Runnable{ Core.app.post(() -> mainList.add(path)); //fill with impassables by default - for(int i = 0; i < tiles.length; i++){ - path.weights[i] = impassable; - } + Arrays.fill(path.weights, impassable); //add targets for(int i = 0; i < path.targets.size; i++){ @@ -416,6 +465,7 @@ public class Pathfinder implements Runnable{ long start = Time.nanos(); int counter = 0; + int w = path.width, h = path.height; while(path.frontier.size > 0){ int tile = path.frontier.removeLast(); @@ -423,7 +473,7 @@ public class Pathfinder implements Runnable{ int cost = path.weights[tile]; //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(); return; } @@ -431,12 +481,12 @@ public class Pathfinder implements Runnable{ if(cost != impassable){ 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 otherCost = path.cost.getCost(path.team.id, tiles[newPos]); + int newPos = dx + dy * w; + int otherCost = path.getCost(tiles, newPos); if((path.weights[newPos] > cost + otherCost || path.searches[newPos] < path.search) && otherCost != impassable){ path.frontier.addFirst(newPos); @@ -523,7 +573,7 @@ public class Pathfinder implements Runnable{ * Concrete subclasses must specify a way to fetch costs and destinations. */ 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; /** Team this path is for. Set before using. */ protected Team team = Team.derelict; @@ -537,12 +587,16 @@ public class Pathfinder implements Runnable{ /** costs of getting to a specific tile */ public int[] weights; /** 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. */ 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 */ - IntQueue frontier = new IntQueue(); + final IntQueue frontier = new IntQueue(); /** all target positions; these positions have a cost of 0, and must be synchronized on! */ final IntSeq targets = new IntSeq(); /** current search ID */ @@ -552,14 +606,44 @@ public class Pathfinder implements Runnable{ /** whether this flow field is ready to be used */ 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.searches = new int[length]; + this.searches = new short[length]; this.completeWeights = new int[length]; this.frontier.ensureCapacity((length) / 4); 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(){ return hasComplete && completeWeights != null; } @@ -569,6 +653,11 @@ public class Pathfinder implements Runnable{ 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){ 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 diff --git a/core/src/mindustry/ai/RtsAI.java b/core/src/mindustry/ai/RtsAI.java index b97e58e464..a3b6a5b6b2 100644 --- a/core/src/mindustry/ai/RtsAI.java +++ b/core/src/mindustry/ai/RtsAI.java @@ -17,7 +17,6 @@ import mindustry.gen.*; import mindustry.graphics.*; import mindustry.logic.*; import mindustry.ui.*; -import mindustry.world.*; import mindustry.world.blocks.defense.turrets.BaseTurret.*; import mindustry.world.blocks.defense.turrets.*; import mindustry.world.blocks.storage.*; @@ -35,7 +34,7 @@ public class RtsAI{ //in order of priority?? static final BlockFlag[] flags = {BlockFlag.generator, BlockFlag.factory, BlockFlag.core, BlockFlag.battery, BlockFlag.drill}; static final ObjectFloatMap 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 TeamData data; @@ -210,12 +209,12 @@ public class RtsAI{ //defendTarget = aggressor; defendPos = new Vec2(aggressor.x, aggressor.y); 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? - Tile closest = defend.findClosestEdge(units.first(), Tile::solid); - if(closest != null){ - defendPos = new Vec2(closest.worldx(), closest.worldy()); - } + // Tile closest = defend.findClosestEdge(units.first(), Tile::solid); + // if(closest != null){ + // defendPos = new Vec2(closest.worldx(), closest.worldy()); + // } }else{ float mindst = Float.MAX_VALUE; Building build = null; diff --git a/core/src/mindustry/ai/UnitCommand.java b/core/src/mindustry/ai/UnitCommand.java index 85eff6be00..5708854109 100644 --- a/core/src/mindustry/ai/UnitCommand.java +++ b/core/src/mindustry/ai/UnitCommand.java @@ -3,7 +3,6 @@ package mindustry.ai; import arc.*; import arc.func.*; import arc.scene.style.*; -import arc.struct.*; import arc.util.*; import mindustry.ai.types.*; 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. */ public class UnitCommand extends MappableContent{ - /** @deprecated now a content type, use the methods in Vars.content instead */ - @Deprecated - public static final Seq all = new Seq<>(); - public static UnitCommand moveCommand, repairCommand, rebuildCommand, assistCommand, mineCommand, boostCommand, enterPayloadCommand, loadUnitsCommand, loadBlocksCommand, unloadPayloadCommand, loopPayloadCommand; /** Name of UI icon (from Icon class). */ @@ -39,8 +34,6 @@ public class UnitCommand extends MappableContent{ this.icon = icon; this.controller = controller == null ? u -> null : controller; - - all.add(this); } public UnitCommand(String name, String icon, Binding keybind, Func controller){ diff --git a/core/src/mindustry/ai/UnitStance.java b/core/src/mindustry/ai/UnitStance.java index 8d4f59bb2e..4fb6ba5c30 100644 --- a/core/src/mindustry/ai/UnitStance.java +++ b/core/src/mindustry/ai/UnitStance.java @@ -2,30 +2,23 @@ package mindustry.ai; import arc.*; import arc.scene.style.*; -import arc.struct.*; import arc.util.*; import mindustry.ctype.*; import mindustry.gen.*; import mindustry.input.*; public class UnitStance extends MappableContent{ - /** @deprecated now a content type, use the methods in Vars.content instead */ - @Deprecated - public static final Seq all = new Seq<>(); - public static UnitStance stop, shoot, holdFire, pursueTarget, patrol, ram; /** Name of UI icon (from Icon class). */ - public final String icon; + public String icon; /** Key to press for this stance. */ - public @Nullable Binding keybind = null; + public @Nullable Binding keybind; public UnitStance(String name, String icon, Binding keybind){ super(name); this.icon = icon; this.keybind = keybind; - - all.add(this); } public String localized(){ diff --git a/core/src/mindustry/ai/WaveSpawner.java b/core/src/mindustry/ai/WaveSpawner.java index 7935418c5f..d3e4ccc651 100644 --- a/core/src/mindustry/ai/WaveSpawner.java +++ b/core/src/mindustry/ai/WaveSpawner.java @@ -82,9 +82,7 @@ public class WaveSpawner{ eachFlyerSpawn(group.spawn, (spawnX, spawnY) -> { for(int i = 0; i < spawnedf; i++){ - Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1); - unit.set(spawnX + Mathf.range(spread), spawnY + Mathf.range(spread)); - spawnEffect(unit); + spawnUnit(group, spawnX + Mathf.range(spread), spawnY + Mathf.range(spread)); } }); }else{ @@ -95,9 +93,7 @@ public class WaveSpawner{ for(int i = 0; i < spawnedf; i++){ Tmp.v1.rnd(spread); - Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1); - unit.set(spawnX + Tmp.v1.x, spawnY + Tmp.v1.y); - spawnEffect(unit); + spawnUnit(group, spawnX + Tmp.v1.x, spawnY + Tmp.v1.y); } }); } @@ -106,6 +102,11 @@ public class WaveSpawner{ 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){ Fx.spawnShockwave.at(x, y, state.rules.dropZoneRadius); 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. */ 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.invincible, 60f); - unit.add(); unit.unloaded(); Events.fire(new UnitSpawnEvent(unit)); diff --git a/core/src/mindustry/ai/types/BuilderAI.java b/core/src/mindustry/ai/types/BuilderAI.java index 7932ad8d9c..5c8d904ea7 100644 --- a/core/src/mindustry/ai/types/BuilderAI.java +++ b/core/src/mindustry/ai/types/BuilderAI.java @@ -118,9 +118,10 @@ public class BuilderAI extends AIController{ Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation))); if(valid){ + float range = Math.min(unit.type.buildRange - 20f, 100f); //move toward the plan - moveTo(req.tile(), unit.type.buildRange - 20f, 20f); - moving = !unit.within(req.tile(), unit.type.buildRange - 10f); + moveTo(req.tile(), range - 10f, 20f); + moving = !unit.within(req.tile(), range); }else{ //discard invalid plan unit.plans.removeFirst(); diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index 48a465ceb9..452ee32bea 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -201,7 +201,7 @@ public class CommandAI extends AIController{ } 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); if(best != null){ targetPos.set(best); @@ -470,7 +470,7 @@ public class CommandAI extends AIController{ @Override public boolean retarget(){ //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(){ diff --git a/core/src/mindustry/ai/types/DefenderAI.java b/core/src/mindustry/ai/types/DefenderAI.java index 01c38e69c6..8ee3889083 100644 --- a/core/src/mindustry/ai/types/DefenderAI.java +++ b/core/src/mindustry/ai/types/DefenderAI.java @@ -28,7 +28,7 @@ public class DefenderAI extends AIController{ public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){ //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); if(result != null) return result; diff --git a/core/src/mindustry/ai/types/HugAI.java b/core/src/mindustry/ai/types/HugAI.java index ff672ff5bc..09b4a22c9f 100644 --- a/core/src/mindustry/ai/types/HugAI.java +++ b/core/src/mindustry/ai/types/HugAI.java @@ -15,7 +15,6 @@ public class HugAI extends AIController{ @Override public void updateMovement(){ - Building core = unit.closestEnemyCore(); if(core != null && unit.within(core, unit.range() / 1.1f + core.block.size * tilesize / 2f)){ diff --git a/core/src/mindustry/ai/types/MinerAI.java b/core/src/mindustry/ai/types/MinerAI.java index eb4307021b..822af9d5d7 100644 --- a/core/src/mindustry/ai/types/MinerAI.java +++ b/core/src/mindustry/ai/types/MinerAI.java @@ -1,6 +1,5 @@ package mindustry.ai.types; -import mindustry.content.*; import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.type.*; @@ -17,7 +16,7 @@ public class MinerAI extends AIController{ public void updateMovement(){ Building core = unit.closestCore(); - if(!(unit.canMine()) || core == null) return; + if(!unit.canMine() || core == null) return; if(!unit.validMine(unit.mineTile)){ unit.mineTile(null); @@ -40,19 +39,17 @@ public class MinerAI extends AIController{ mining = false; }else{ 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){ 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; } - - if(ore.block() != Blocks.air){ - mining = false; - } } } }else{ diff --git a/core/src/mindustry/async/PhysicsProcess.java b/core/src/mindustry/async/PhysicsProcess.java index a38f28eeb2..1a85312e55 100644 --- a/core/src/mindustry/async/PhysicsProcess.java +++ b/core/src/mindustry/async/PhysicsProcess.java @@ -11,10 +11,11 @@ import mindustry.gen.*; public class PhysicsProcess implements AsyncProcess{ public static final int - layers = 3, + layers = 4, layerGround = 0, layerLegs = 1, - layerFlying = 2; + layerFlying = 2, + layerUnderwater = 3; private PhysicsWorld physics; private Seq refs = new Seq<>(false); @@ -153,15 +154,15 @@ public class PhysicsProcess implements AsyncProcess{ for(int i = 0; i < bodySize; i++){ PhysicsBody body = bodyItems[i]; + if(body.layer < 0) continue; body.collided = false; trees[body.layer].insert(body); } for(int i = 0; i < bodySize; 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. - if(!body.local) continue; + if(!body.local || body.layer < 0) continue; body.hitbox(rect); diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 328d838c4c..025de0c738 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -292,7 +292,7 @@ public class Logic implements ApplicationListener{ //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()) || - (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){ //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)); }else if(state.rules.attackMode){ //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){ //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)); state.gameOver = true; } @@ -413,6 +413,9 @@ public class Logic implements ApplicationListener{ @Override public void update(){ + PerfCounter.frame.end(); + PerfCounter.frame.begin(); + Events.fire(Trigger.update); universe.updateGlobal(); @@ -489,7 +492,9 @@ public class Logic implements ApplicationListener{ state.envAttrs.add(state.rules.attributes); Groups.weather.each(w -> state.envAttrs.add(w.weather.attrs, w.opacity)); + PerfCounter.entityUpdate.begin(); Groups.update(); + PerfCounter.entityUpdate.end(); Events.fire(Trigger.afterGameUpdate); } diff --git a/core/src/mindustry/core/NetServer.java b/core/src/mindustry/core/NetServer.java index b882706e83..0d122ec249 100644 --- a/core/src/mindustry/core/NetServer.java +++ b/core/src/mindustry/core/NetServer.java @@ -990,6 +990,7 @@ public class NetServer implements ApplicationListener{ //write all entities now dataStream.writeInt(entity.id()); //write id dataStream.writeByte(entity.classId() & 0xFF); //write type ID + entity.beforeWrite(); entity.writeSync(Writes.get(dataStream)); //write entity sent++; diff --git a/core/src/mindustry/core/PerfCounter.java b/core/src/mindustry/core/PerfCounter.java new file mode 100644 index 0000000000..02de189ab6 --- /dev/null +++ b/core/src/mindustry/core/PerfCounter.java @@ -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(); + } +} diff --git a/core/src/mindustry/core/Renderer.java b/core/src/mindustry/core/Renderer.java index f8babe63b5..e7e5ebe317 100644 --- a/core/src/mindustry/core/Renderer.java +++ b/core/src/mindustry/core/Renderer.java @@ -150,6 +150,7 @@ public class Renderer implements ApplicationListener{ @Override public void update(){ + PerfCounter.render.begin(); Color.white.set(1f, 1f, 1f, 1f); float baseTarget = targetscale; @@ -220,6 +221,8 @@ public class Renderer implements ApplicationListener{ camera.position.sub(camShakeOffset); } + + PerfCounter.render.end(); } public void updateAllDarkness(){ @@ -313,7 +316,6 @@ public class Renderer implements ApplicationListener{ Draw.draw(Layer.block - 0.09f, () -> { blocks.floor.beginDraw(); blocks.floor.drawLayer(CacheLayer.walls); - blocks.floor.endDraw(); }); Draw.drawRange(Layer.blockBuilding, () -> Draw.shader(Shaders.blockbuild, true), Draw::shader); diff --git a/core/src/mindustry/core/World.java b/core/src/mindustry/core/World.java index a9e858a0c7..37794f8413 100644 --- a/core/src/mindustry/core/World.java +++ b/core/src/mindustry/core/World.java @@ -123,7 +123,7 @@ public class World{ Tile tile = tiles.get(x, y); if(tile == null) return null; if(tile.build != null){ - return tile.build.tile(); + return tile.build.tile; } return tile; } @@ -458,10 +458,12 @@ public class World{ return 0; } - public void checkMapArea(){ + public void checkMapArea(int x, int y, int w, int h){ for(var build : Groups.build){ - //reset map-area-based disabled blocks. - if(!build.enabled && build.block.autoResetEnabled){ + //if the map area contracts, disable the block + 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; } } diff --git a/core/src/mindustry/editor/EditorTile.java b/core/src/mindustry/editor/EditorTile.java index 68272ce8d1..15d48819c4 100644 --- a/core/src/mindustry/editor/EditorTile.java +++ b/core/src/mindustry/editor/EditorTile.java @@ -147,8 +147,8 @@ public class EditorTile extends Tile{ if(block.hasBuilding()){ build = entityprov.get().init(this, team, false, rotation); if(block.hasItems) build.items = new ItemModule(); - if(block.hasLiquids) build.liquids(new LiquidModule()); - if(block.hasPower) build.power(new PowerModule()); + if(block.hasLiquids) build.liquids = new LiquidModule(); + if(block.hasPower) build.power = new PowerModule(); } } diff --git a/core/src/mindustry/editor/MapObjectivesDialog.java b/core/src/mindustry/editor/MapObjectivesDialog.java index c8f021f214..ab781c7419 100644 --- a/core/src/mindustry/editor/MapObjectivesDialog.java +++ b/core/src/mindustry/editor/MapObjectivesDialog.java @@ -136,6 +136,7 @@ public class MapObjectivesDialog extends BaseDialog{ name(cont, name, remover, indexer); cont.table(t -> t.left().button( b -> b.image(Tex.whiteui).size(iconSmall).update(i -> i.setColor(get.get().color)), + Styles.squarei, () -> showTeamSelect(set) ).fill().pad(4f)).growX().fillY(); }); @@ -529,6 +530,8 @@ public class MapObjectivesDialog extends BaseDialog{ public void rebuildObjectives(Seq objectives){ canvas.clearObjectives(); + objectives.each(MapObjective::validate); + if( objectives.any() && ( // If the objectives were previously programmatically made... @@ -592,9 +595,23 @@ public class MapObjectivesDialog extends BaseDialog{ } public static void showTeamSelect(Cons cons){ + showTeamSelect(false, cons); + } + + public static void showTeamSelect(boolean allowNull, Cons cons){ 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){ - 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())) .tooltip(team.localized()).get().clicked(() -> { cons.get(team); diff --git a/core/src/mindustry/editor/WaveInfoDialog.java b/core/src/mindustry/editor/WaveInfoDialog.java index b4bc26acc2..55e675caa0 100644 --- a/core/src/mindustry/editor/WaveInfoDialog.java +++ b/core/src/mindustry/editor/WaveInfoDialog.java @@ -288,6 +288,13 @@ public class WaveInfoDialog extends BaseDialog{ buildGroups(); }).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 -> { a.add("@waves.spawn").padRight(8); diff --git a/core/src/mindustry/entities/Damage.java b/core/src/mindustry/entities/Damage.java index 7f13c16546..a8bcc1eba4 100644 --- a/core/src/mindustry/entities/Damage.java +++ b/core/src/mindustry/entities/Damage.java @@ -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){ 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){ 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){ 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){ + 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){ 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); @@ -123,7 +127,7 @@ public class Damage{ 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); explosionFx.at(x, y, radius / 8f); } @@ -549,7 +553,12 @@ public class Damage{ //this needs to be compensated if(in != null && in.team != team && in.block.size > 1 && in.health > damage){ //deal the damage of an entire side, to be equivalent with maximum 'standard' damage - in.damage(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 return; } diff --git a/core/src/mindustry/entities/EntityCollisions.java b/core/src/mindustry/entities/EntityCollisions.java index 273449bb6a..c92d32cb93 100644 --- a/core/src/mindustry/entities/EntityCollisions.java +++ b/core/src/mindustry/entities/EntityCollisions.java @@ -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 px = vx1, py = vy1; diff --git a/core/src/mindustry/entities/Lightning.java b/core/src/mindustry/entities/Lightning.java index 76bbcb82b5..0bd26d7ceb 100644 --- a/core/src/mindustry/entities/Lightning.java +++ b/core/src/mindustry/entities/Lightning.java @@ -17,7 +17,7 @@ import static mindustry.Vars.*; public class Lightning{ private static final Rand random = new Rand(); private static final Rect rect = new Rect(); - private static final Seq entities = new Seq<>(); + private static final Seq entities = new Seq<>(); private static final IntSet hit = new IntSet(); private static final int maxChain = 8; 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){ hit.add(furthest.id()); diff --git a/core/src/mindustry/entities/Puddles.java b/core/src/mindustry/entities/Puddles.java index 7c3a555154..cb0bcbe45f 100644 --- a/core/src/mindustry/entities/Puddles.java +++ b/core/src/mindustry/entities/Puddles.java @@ -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){ if(tile == null) return; @@ -126,7 +132,7 @@ public class Puddles{ if(Mathf.chance(0.8f * amount)){ Fx.steam.at(x, y); } - return -0.4f * amount; + return -0.7f * amount; } return dest.react(liquid, amount, tile, x, y); } diff --git a/core/src/mindustry/entities/TargetPriority.java b/core/src/mindustry/entities/TargetPriority.java index f8d361bfac..b1c166ba96 100644 --- a/core/src/mindustry/entities/TargetPriority.java +++ b/core/src/mindustry/entities/TargetPriority.java @@ -4,7 +4,9 @@ package mindustry.entities; public class TargetPriority{ public static final float //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 = -1f, //most blocks diff --git a/core/src/mindustry/entities/UnitSorts.java b/core/src/mindustry/entities/UnitSorts.java index 25afb0def6..8cbf497061 100644 --- a/core/src/mindustry/entities/UnitSorts.java +++ b/core/src/mindustry/entities/UnitSorts.java @@ -1,6 +1,7 @@ package mindustry.entities; import arc.math.*; +import mindustry.content.*; import mindustry.entities.Units.*; import mindustry.gen.*; @@ -11,4 +12,9 @@ public class UnitSorts{ farthest = (u, x, y) -> -u.dst2(x, y), 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; + + public static BuildingPriorityf + + buildingDefault = b -> b.block.priority, + buildingWater = b -> b.block.priority + (b.liquids != null && b.liquids.get(Liquids.water) > 5f ? 10f : 0f); } \ No newline at end of file diff --git a/core/src/mindustry/entities/Units.java b/core/src/mindustry/entities/Units.java index d334651092..5bc18116fd 100644 --- a/core/src/mindustry/entities/Units.java +++ b/core/src/mindustry/entities/Units.java @@ -95,7 +95,7 @@ public class Units{ public static int getCap(Team team){ //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 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. */ public static Building findEnemyTile(Team team, float x, float y, float range, Boolf 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 pred){ 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); } @@ -258,7 +248,7 @@ public class Units{ if(unit != null){ return unit; }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){ return unit; }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 */ public static boolean any(float x, float y, float width, float height, Boolf 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. */ @@ -494,4 +484,8 @@ public class Units{ public interface Sortf{ float cost(Unit unit, float x, float y); } + + public interface BuildingPriorityf{ + float priority(Building build); + } } diff --git a/core/src/mindustry/entities/abilities/Ability.java b/core/src/mindustry/entities/abilities/Ability.java index d64a77ffea..4dc6b17363 100644 --- a/core/src/mindustry/entities/abilities/Ability.java +++ b/core/src/mindustry/entities/abilities/Ability.java @@ -4,6 +4,7 @@ import arc.*; import arc.scene.ui.layout.*; import mindustry.gen.*; import mindustry.type.*; +import mindustry.ui.*; public abstract class Ability implements Cloneable{ protected static final float descriptionWidth = 350f; @@ -13,11 +14,26 @@ public abstract class Ability implements Cloneable{ public float data; public void update(Unit unit){} + public void draw(Unit unit){} + public void death(Unit unit){} + public void created(Unit unit){} + public void init(UnitType type){} + 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){ if(Core.bundle.has(getBundle() + ".description")){ t.add(Core.bundle.get(getBundle() + ".description")).wrap().width(descriptionWidth); diff --git a/core/src/mindustry/entities/abilities/LiquidRegenAbility.java b/core/src/mindustry/entities/abilities/LiquidRegenAbility.java index 6f864035ce..227a72e8e5 100644 --- a/core/src/mindustry/entities/abilities/LiquidRegenAbility.java +++ b/core/src/mindustry/entities/abilities/LiquidRegenAbility.java @@ -13,8 +13,8 @@ import static mindustry.Vars.*; public class LiquidRegenAbility extends Ability{ public Liquid liquid; - public float slurpSpeed = 9f; - public float regenPerSlurp = 2.9f; + public float slurpSpeed = 5f; + public float regenPerSlurp = 6f; public float slurpEffectChance = 0.4f; public Effect slurpEffect = Fx.heal; @@ -31,7 +31,7 @@ public class LiquidRegenAbility extends Ability{ //TODO timer? //TODO effects? - if(unit.damaged()){ + if(unit.damaged() && !unit.isFlying()){ boolean healed = false; int tx = unit.tileX(), ty = unit.tileY(); int rad = Math.max((int)(unit.hitSize / tilesize * 0.6f), 1); diff --git a/core/src/mindustry/entities/abilities/MoveEffectAbility.java b/core/src/mindustry/entities/abilities/MoveEffectAbility.java index 48e2ba709c..8e0254f13a 100644 --- a/core/src/mindustry/entities/abilities/MoveEffectAbility.java +++ b/core/src/mindustry/entities/abilities/MoveEffectAbility.java @@ -1,6 +1,7 @@ package mindustry.entities.abilities; import arc.graphics.*; +import arc.math.*; import arc.util.*; import mindustry.*; import mindustry.content.*; @@ -9,8 +10,9 @@ import mindustry.gen.*; public class MoveEffectAbility extends Ability{ public float minVelocity = 0.08f; - public float interval = 3f; - public float x, y, rotation; + public float interval = 3f, chance = 0f; + public int amount = 1; + public float x, y, rotation, rangeX, rangeY, rangeLengthMin, rangeLengthMax; public boolean rotateEffect = false; public float effectParam = 3f; public boolean teamColor = false; @@ -38,10 +40,17 @@ public class MoveEffectAbility extends Ability{ if(Vars.headless) return; counter += Time.delta; - if(unit.vel.len2() >= minVelocity * minVelocity && (counter >= interval) && !unit.inFogTo(Vars.player.team())){ - Tmp.v1.trns(unit.rotation - 90f, x, y); + if(unit.vel.len2() >= minVelocity * minVelocity && (counter >= interval || (chance > 0 && Mathf.chanceDelta(chance))) && !unit.inFogTo(Vars.player.team())){ + 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; - 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); + } } } } diff --git a/core/src/mindustry/entities/bullet/BulletType.java b/core/src/mindustry/entities/bullet/BulletType.java index 2c7a6184b0..3c774dde32 100644 --- a/core/src/mindustry/entities/bullet/BulletType.java +++ b/core/src/mindustry/entities/bullet/BulletType.java @@ -30,16 +30,24 @@ public class BulletType extends Content implements Cloneable{ /** Lifetime in ticks. */ 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. */ 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. */ public float damage = 1f; /** Hitbox size. */ public float hitSize = 4; /** Clipping hitbox. */ public float drawSize = 40f; + /** Angle offset applied to bullet when spawned each time. */ + public float angleOffset = 0f, randomAngleOffset = 0f; /** Drag as fraction of velocity. */ public float drag = 0f; + /** Acceleration per frame. */ + public float accel = 0f; /** Whether to pierce units. */ public boolean pierce; /** Whether to pierce buildings. */ @@ -155,6 +163,8 @@ public class BulletType extends Content implements Cloneable{ public float healAmount = 0f; /** Whether to make fire on impact */ 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. */ public boolean despawnHit = false; /** 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; /** If true, unit armor is ignored in damage calculations. */ 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. */ public boolean setDefaults = true; /** 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; /** Number of bullet spawned per interval. */ public int intervalBullets = 1; - /** Random spread of interval bullets. */ + /** Random angle added to interval bullets. */ public float intervalRandomSpread = 360f; /** Angle spread between individual interval bullets. */ public float intervalSpread = 0f; @@ -204,6 +218,9 @@ public class BulletType extends Content implements Cloneable{ /** Use a negative value to disable interval bullet delay. */ public float intervalDelay = -1f; + /** If true, this bullet is rendered underwater. Highly experimental! */ + public boolean underwater = false; + /** Color used for hit/despawn effects. */ public Color hitColor = Color.white; /** Color used for block heal effects. */ @@ -212,6 +229,8 @@ public class BulletType extends Content implements Cloneable{ public Effect healEffect = Fx.healBlockFull; /** Bullets spawned when this bullet is created. Rarely necessary, used for visuals. */ public Seq spawnBullets = new Seq<>(); + /** Random angle spread of spawn bullets. */ + public float spawnBulletRandomSpread = 0f; /** Unit spawned _instead of_ this bullet. Useful for missiles. */ public @Nullable UnitType spawnUnit; /** 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; /** Uniform interval in which trail effect is spawned. */ public float trailInterval = 0f; + /** Min velocity required for trail effect to spawn. */ + public float trailMinVelocity = 0f; /** Trail effect that is spawned. */ 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. */ public float trailParam = 2f; /** 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; /** If trailSinMag > 0, these values are applied as a sine curve to trail width. */ 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. */ public float splashDamageRadius = -1f; @@ -299,6 +330,8 @@ public class BulletType extends Content implements Cloneable{ public float weaveMag = 0f; /** If true, the bullet weave will randomly switch directions on spawn. */ public boolean weaveRandom = true; + /** Rotation speed of the bullet velocity as it travels. */ + public float rotateSpeed = 0f; /** Number of individual puddles created. */ public int puddles; @@ -624,7 +657,7 @@ public class BulletType extends Content implements Cloneable{ if(spawnBullets.size > 0){ 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){ 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){ - if(trailChance > 0){ + boolean canSpawn = trailMinVelocity <= 0f || b.vel.len2() >= trailMinVelocity * trailMinVelocity; + + if(trailChance > 0 && canSpawn){ 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)){ - 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, 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(ignoreSpawnAngle) angle = 0; if(spawnUnit != null){ @@ -846,13 +903,13 @@ public class BulletType extends Content implements Cloneable{ bullet.aimX = aimX; bullet.aimY = aimY; - bullet.initVel(angle, speed * velocityScl); + bullet.initVel(angle, speed * velocityScl * (velocityScaleRandMin != 1f || velocityScaleRandMax != 1f ? Mathf.random(velocityScaleRandMin, velocityScaleRandMax) : 1f)); if(backMove){ bullet.set(x - bullet.vel.x * Time.delta, y - bullet.vel.y * Time.delta); }else{ 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.drag = drag; bullet.hitSize = hitSize; diff --git a/core/src/mindustry/entities/bullet/InterceptorBulletType.java b/core/src/mindustry/entities/bullet/InterceptorBulletType.java new file mode 100644 index 0000000000..015b7337f9 --- /dev/null +++ b/core/src/mindustry/entities/bullet/InterceptorBulletType.java @@ -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; + } + } + } +} diff --git a/core/src/mindustry/entities/bullet/MultiBulletType.java b/core/src/mindustry/entities/bullet/MultiBulletType.java new file mode 100644 index 0000000000..5b6d3edf8e --- /dev/null +++ b/core/src/mindustry/entities/bullet/MultiBulletType.java @@ -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; + } +} diff --git a/core/src/mindustry/entities/bullet/SapBulletType.java b/core/src/mindustry/entities/bullet/SapBulletType.java index 2ee88eae1d..6bffa4588c 100644 --- a/core/src/mindustry/entities/bullet/SapBulletType.java +++ b/core/src/mindustry/entities/bullet/SapBulletType.java @@ -3,6 +3,7 @@ package mindustry.entities.bullet; import arc.*; import arc.graphics.*; import arc.graphics.g2d.*; +import arc.math.*; import arc.math.geom.*; import arc.util.*; import mindustry.content.*; @@ -11,7 +12,7 @@ import mindustry.gen.*; import mindustry.graphics.*; public class SapBulletType extends BulletType{ - public float length = 100f; + public float length = 100f, lengthRand = 0f; public float sapStrength = 0.5f; public Color color = Color.white.cpy(); public float width = 0.4f; @@ -72,7 +73,9 @@ public class SapBulletType extends BulletType{ public void init(Bullet 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; if(target != null){ @@ -92,7 +95,7 @@ public class SapBulletType extends BulletType{ hit(b, tile.x, tile.y); } }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); } } } diff --git a/core/src/mindustry/entities/comp/BlockUnitComp.java b/core/src/mindustry/entities/comp/BlockUnitComp.java index 5ab3327f76..7c11c71a30 100644 --- a/core/src/mindustry/entities/comp/BlockUnitComp.java +++ b/core/src/mindustry/entities/comp/BlockUnitComp.java @@ -63,6 +63,11 @@ abstract class BlockUnitComp implements Unitc{ return tile != null && tile.isValid(); } + @Replace + public boolean isAdded(){ + return tile != null && tile.isValid(); + } + @Replace public void team(Team team){ if(tile != null && this.team != team){ diff --git a/core/src/mindustry/entities/comp/BoundedComp.java b/core/src/mindustry/entities/comp/BoundedComp.java deleted file mode 100644 index 8fc1927bc0..0000000000 --- a/core/src/mindustry/entities/comp/BoundedComp.java +++ /dev/null @@ -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(); - } - } -} diff --git a/core/src/mindustry/entities/comp/BuilderComp.java b/core/src/mindustry/entities/comp/BuilderComp.java index 575d5ffc0b..4ce5151b88 100644 --- a/core/src/mindustry/entities/comp/BuilderComp.java +++ b/core/src/mindustry/entities/comp/BuilderComp.java @@ -136,7 +136,7 @@ abstract class BuilderComp implements Posc, Statusc, Teamc, Rotc{ } 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)){ boolean hasAll = infinite || current.isRotation(team) || //derelict repair diff --git a/core/src/mindustry/entities/comp/BuildingComp.java b/core/src/mindustry/entities/comp/BuildingComp.java index 7bdbecfed0..4875457ffb 100644 --- a/core/src/mindustry/entities/comp/BuildingComp.java +++ b/core/src/mindustry/entities/comp/BuildingComp.java @@ -16,7 +16,6 @@ import arc.util.*; import arc.util.io.*; import mindustry.*; import mindustry.annotations.Annotations.*; -import mindustry.audio.*; import mindustry.content.*; import mindustry.core.*; import mindustry.ctype.*; @@ -47,7 +46,7 @@ import java.util.*; import static mindustry.Vars.*; @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{ //region vars and initialization 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 Block block; - transient Seq proximity = new Seq<>(6); + transient Seq proximity = new Seq<>(true, 6, Building.class); transient int cdump; transient int rotation; 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 dumpAccum; - private transient @Nullable SoundLoop sound; - private transient boolean sleeping; private transient float sleepTime; private transient boolean initialized; @@ -126,6 +123,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, add(); } + checkAllowUpdate(); created(); return self(); @@ -136,10 +134,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, this.block = block; this.team = team; - if(block.loopSound != Sounds.none){ - sound = new SoundLoop(block.loopSound, block.loopSoundVolume); - } - health = block.health; maxHealth(block.health); 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 solid){ + if(to == null) return null; Tile best = null; float mindst = 0f; for(var point : Edges.getEdges(block.size)){ 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)){ best = other; - mindst = other.dst2(other); + mindst = other.dst2(to); } } return best; @@ -473,6 +468,15 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, return lastDamageTime + recentDamageTime >= Time.time; } + public void eachEdge(Cons 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){ return world.build(tile.x + dx, tile.y + dy); } @@ -809,6 +813,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, return false; } + public boolean canBeReplaced(Block other){ + return other.canReplace(block); + } + public void handleItem(Building source, Item item){ items.add(item, 1); } @@ -1066,7 +1074,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, } 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. */ @@ -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. */ + //TODO ??? this is just onProximityUpdate ? public void onProximityAdded(){ if(power != null){ updatePowerGraph(); @@ -1156,16 +1168,6 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, 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.*/ public boolean shouldAmbientSound(){ return shouldConsume(); @@ -1315,14 +1317,23 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, 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){ } + /** 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. */ 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. */ public void configured(@Nullable Unit builder, @Nullable Object value){ //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; } + 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. */ public void onDeconstructed(@Nullable Unit builder){ //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. */ public void onDestroyed(){ - if(sound != null){ - sound.stop(); - } float explosiveness = block.baseExplosiveness; float flammability = 0f; @@ -1411,22 +1432,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, if(block.hasLiquids && state.rules.damageExplosions){ - liquids.each((liquid, 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); - } - }); - } - }); + liquids.each(this::splashLiquid); } //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){ 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(other.team, damage); - Events.fire(bulletDamageEvent.set(self(), other)); + damage(other, other.team, damage); if(health <= 0 && !wasDead){ 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. */ public void changeTeam(Team next){ if(this.team == next) return; + if(block.forceTeam != null) team = block.forceTeam; Team last = this.team; boolean was = isValid(); @@ -1693,10 +1703,12 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, indexer.addIndex(tile); Events.fire(teamChangeEvent.set(last, self())); } + + checkAllowUpdate(); } public boolean canPickup(){ - return true; + return block.canPickup; } /** 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(){ for(Consume cons : block.consumers){ cons.trigger(self()); @@ -2094,22 +2108,11 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, return true; } - @Override - public void remove(){ - stopSound(); - } - - public void stopSound(){ - if(sound != null){ - sound.stop(); - } - } - @Override public void killed(){ dead = true; Events.fire(new BlockDestroyEvent(tile)); - block.destroySound.at(tile); + block.destroySound.at(tile, Mathf.random(block.destroyPitchMin, block.destroyPitchMax)); onDestroyed(); if(tile != emptyTile){ tile.remove(); @@ -2118,48 +2121,44 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, afterDestroyed(); } + public void checkAllowUpdate(){ + if(!allowUpdate()){ + enabled = false; + } + } + @Final @Replace @Override public void update(){ - //TODO should just avoid updating buildings instead - if(state.isEditor()) return; //TODO refactor to timestamp-based system? if((timeScaleDuration -= Time.delta) <= 0f || !block.canOverdrive){ timeScale = 1f; } - if(!allowUpdate()){ - enabled = false; - } - - 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()); - } + //TODO separate multithreaded system for sound? AudioSource, etc + if(!headless && block.ambientSound != Sounds.none && shouldAmbientSound()){ + control.sound.loop(block.ambientSound, self(), block.ambientSoundVolume * ambientVolume()); } updateConsumption(); - //TODO just handle per-block instead if(enabled || !block.noUpdateDisabled){ 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 public void hitbox(Rect out){ out.setCentered(x, y, block.size * tilesize, block.size * tilesize); diff --git a/core/src/mindustry/entities/comp/BulletComp.java b/core/src/mindustry/entities/comp/BulletComp.java index 35916e43f9..74c1eca3db 100644 --- a/core/src/mindustry/entities/comp/BulletComp.java +++ b/core/src/mindustry/entities/comp/BulletComp.java @@ -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. 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 @Nullable Tile aimTile; transient float aimX, aimY; @@ -48,6 +49,9 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw transient @Nullable Trail trail; transient int frags; + transient Posc stickyTarget; + transient float stickyX, stickyY, stickyRotation, stickyRotationOffset; + @Override public void getCollisions(Cons consumer){ Seq data = state.teams.present; @@ -106,24 +110,43 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw @Override public boolean collides(Hitboxc other){ return type.collides && (other instanceof Teamc t && t.team() != team) - && !(other instanceof Flyingc f && !f.checkTarget(type.collidesAir, type.collidesGround)) - && !(type.pierce && hasCollided(other.id())); //prevent multiple collisions + && !(other instanceof Unit f && !f.checkTarget(type.collidesAir, type.collidesGround)) + && !(type.pierce && hasCollided(other.id())) && stickyTarget == null; //prevent multiple collisions } @MethodPriority(100) @Override public void collision(Hitboxc other, float x, float y){ - type.hit(self(), x, y); - - //must be last. - if(!type.pierce){ - hit = true; - remove(); + if(type.sticky){ + if(stickyTarget == null){ + //tunnel into the target a bit for better visuals + this.x = x + vel.x; + this.y = y + vel.y; + stickTo(other); + } }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 @@ -132,9 +155,21 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw mover.move(self()); } + if(type.accel != 0){ + vel.setLength(vel.len() + type.accel * Time.delta); + } + 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()); } @@ -165,6 +200,8 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw (!build.block.underBullets || //direct hit on correct tile (aimTile != null && aimTile.build == build) || + //bullet type allows hitting under bullets + type.hitUnder || //same team has no 'under build' mechanics (build.team == team) || //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.dead() && (type.collidesTeam || build.team != team) && !(type.pierceBuilding && hasCollided(build.id))){ - boolean remove = false; - float health = build.health; + if(type.sticky){ + 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){ - remove = build.collision(self()); - } + stickTo(build); - if(remove || type.collidesTeam){ - if(Mathf.dst2(lastX, lastY, x * tilesize, y * tilesize) < Mathf.dst2(lastX, lastY, this.x, this.y)){ - this.x = x * tilesize; - this.y = y * tilesize; + return; + } + }else{ + boolean remove = false; + float health = build.health; + + if(build.team != team){ + remove = build.collision(self()); } - if(!type.pierceBuilding){ - hit = true; - remove(); - }else{ - collided.add(build.id); + if(remove || type.collidesTeam){ + if(Mathf.dst2(lastX, lastY, x * tilesize, y * tilesize) < Mathf.dst2(lastX, lastY, this.x, this.y)){ + this.x = x * tilesize; + this.y = y * tilesize; + } + + 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; @@ -247,7 +299,11 @@ abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Draw public void draw(){ Draw.z(type.layer); - type.draw(self()); + if(type.underwater){ + Drawf.underwater(() -> type.draw(self())); + }else{ + type.draw(self()); + } type.drawLight(self()); Draw.reset(); diff --git a/core/src/mindustry/entities/comp/CrawlComp.java b/core/src/mindustry/entities/comp/CrawlComp.java index d638d45c0b..918023197c 100644 --- a/core/src/mindustry/entities/comp/CrawlComp.java +++ b/core/src/mindustry/entities/comp/CrawlComp.java @@ -1,10 +1,8 @@ package mindustry.entities.comp; import arc.math.*; -import arc.math.geom.*; import arc.util.*; import mindustry.*; -import mindustry.ai.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.entities.*; @@ -22,7 +20,6 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{ @Import float x, y, speedMultiplier, rotation, hitSize; @Import UnitType type; @Import Team team; - @Import Vec2 vel; transient Floor lastDeepFloor; transient float lastCrawlSlowdown = 1f; @@ -31,13 +28,7 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{ @Replace @Override public SolidPred solidity(){ - return EntityCollisions::legsSolid; - } - - @Override - @Replace - public int pathType(){ - return Pathfinder.costLegs; + return ignoreSolids() ? null : EntityCollisions::legsSolid; } @Override @@ -110,6 +101,6 @@ abstract class CrawlComp implements Posc, Rotc, Hitboxc, Unitc{ } segmentRot = Angles.clampRange(segmentRot, rotation, type.segmentMaxRot); - crawlTime += vel.len() * Time.delta; + crawlTime += deltaLen(); } } diff --git a/core/src/mindustry/entities/comp/ElevationMoveComp.java b/core/src/mindustry/entities/comp/ElevationMoveComp.java index f87bc1de0f..413e3fd367 100644 --- a/core/src/mindustry/entities/comp/ElevationMoveComp.java +++ b/core/src/mindustry/entities/comp/ElevationMoveComp.java @@ -6,13 +6,13 @@ import mindustry.entities.EntityCollisions.*; import mindustry.gen.*; @Component -abstract class ElevationMoveComp implements Velc, Posc, Flyingc, Hitboxc{ +abstract class ElevationMoveComp implements Velc, Posc, Hitboxc, Unitc{ @Import float x, y; @Replace @Override public SolidPred solidity(){ - return isFlying() ? null : EntityCollisions::solid; + return isFlying() || ignoreSolids() ? null : EntityCollisions::solid; } } diff --git a/core/src/mindustry/entities/comp/EntityComp.java b/core/src/mindustry/entities/comp/EntityComp.java index 37b7bb7a2b..2db21e1ed3 100644 --- a/core/src/mindustry/entities/comp/EntityComp.java +++ b/core/src/mindustry/entities/comp/EntityComp.java @@ -59,12 +59,16 @@ abstract class EntityComp{ } + void beforeWrite(){ + + } + void afterRead(){ } - /** Called after *all* entities are read. */ - void afterAllRead(){ + //called after all entities have been read (useful for ID resolution) + void afterReadAll(){ } } diff --git a/core/src/mindustry/entities/comp/FlyingComp.java b/core/src/mindustry/entities/comp/FlyingComp.java deleted file mode 100644 index c2d6dacea4..0000000000 --- a/core/src/mindustry/entities/comp/FlyingComp.java +++ /dev/null @@ -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); - } -} diff --git a/core/src/mindustry/entities/comp/LegsComp.java b/core/src/mindustry/entities/comp/LegsComp.java index cc57b1e87d..dbb5c76059 100644 --- a/core/src/mindustry/entities/comp/LegsComp.java +++ b/core/src/mindustry/entities/comp/LegsComp.java @@ -4,7 +4,6 @@ import arc.math.*; import arc.math.geom.*; import arc.util.*; import mindustry.*; -import mindustry.ai.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.entities.*; @@ -19,7 +18,7 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; @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(); @Import float x, y, rotation, speedMultiplier; @@ -37,13 +36,7 @@ abstract class LegsComp implements Posc, Rotc, Hitboxc, Flyingc, Unitc{ @Replace @Override public SolidPred solidity(){ - return type.allowLegStep ? EntityCollisions::legsSolid : EntityCollisions::solid; - } - - @Override - @Replace - public int pathType(){ - return type.allowLegStep ? Pathfinder.costLegs : Pathfinder.costGround; + return ignoreSolids() ? null : type.allowLegStep ? EntityCollisions::legsSolid : EntityCollisions::solid; } @Override @@ -111,6 +104,7 @@ abstract class LegsComp implements Posc, Rotc, Hitboxc, Flyingc, Unitc{ legs[i] = l; } + totalLength = Mathf.random(100f); } @Override diff --git a/core/src/mindustry/entities/comp/MechComp.java b/core/src/mindustry/entities/comp/MechComp.java index 9d599b9bdd..5e4f518653 100644 --- a/core/src/mindustry/entities/comp/MechComp.java +++ b/core/src/mindustry/entities/comp/MechComp.java @@ -12,7 +12,7 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; @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 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){ diff --git a/core/src/mindustry/entities/comp/PhysicsComp.java b/core/src/mindustry/entities/comp/PhysicsComp.java index 1924de8eb9..e0648d6874 100644 --- a/core/src/mindustry/entities/comp/PhysicsComp.java +++ b/core/src/mindustry/entities/comp/PhysicsComp.java @@ -10,7 +10,7 @@ import mindustry.gen.*; * Will bounce off of other objects that are at similar elevations. * Has mass.*/ @Component -abstract class PhysicsComp implements Velc, Hitboxc, Flyingc{ +abstract class PhysicsComp implements Velc, Hitboxc{ @Import float hitSize, x, y; @Import Vec2 vel; diff --git a/core/src/mindustry/entities/comp/PuddleComp.java b/core/src/mindustry/entities/comp/PuddleComp.java index a46bf01c32..4fbc555193 100644 --- a/core/src/mindustry/entities/comp/PuddleComp.java +++ b/core/src/mindustry/entities/comp/PuddleComp.java @@ -23,7 +23,7 @@ abstract class PuddleComp implements Posc, Puddlec, Drawc, Syncc{ private static Puddle paramPuddle; private static Cons unitCons = unit -> { - if(unit.isGrounded() && !unit.hovering){ + if(unit.isGrounded() && !unit.type.hovering){ unit.hitbox(rect2); if(rect.overlaps(rect2)){ unit.apply(paramPuddle.liquid.effect, 60 * 2); @@ -104,6 +104,10 @@ abstract class PuddleComp implements Posc, Puddlec, Drawc, Syncc{ } updateTime = 40f; + + if(tile.build != null){ + tile.build.puddleOn(self()); + } } if(!headless && liquid.particleEffect != Fx.none){ diff --git a/core/src/mindustry/entities/comp/SegmentComp.java b/core/src/mindustry/entities/comp/SegmentComp.java new file mode 100644 index 0000000000..5090db452d --- /dev/null +++ b/core/src/mindustry/entities/comp/SegmentComp.java @@ -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); + } + } + +} diff --git a/core/src/mindustry/entities/comp/StatusComp.java b/core/src/mindustry/entities/comp/StatusComp.java index 4d3dd11dd0..0b192e75a5 100644 --- a/core/src/mindustry/entities/comp/StatusComp.java +++ b/core/src/mindustry/entities/comp/StatusComp.java @@ -15,7 +15,7 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; @Component -abstract class StatusComp implements Posc, Flyingc{ +abstract class StatusComp implements Posc{ private Seq statuses = new Seq<>(4); private transient Bits applied = new Bits(content.getBy(ContentType.status).size); @@ -28,12 +28,12 @@ abstract class StatusComp implements Posc, Flyingc{ @Import float maxHealth; /** Apply a status effect for 1 tick (for permanent effects) **/ - void apply(StatusEffect effect){ + public void apply(StatusEffect effect){ apply(effect, 1); } /** 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 //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); return entry == null ? 0 : entry.time; } - void clearStatuses(){ + public void clearStatuses(){ statuses.each(e -> e.effect.onRemoved(self())); statuses.clear(); } /** Removes a status effect. */ - void unapply(StatusEffect effect){ + public void unapply(StatusEffect effect){ statuses.remove(e -> { if(e.effect == effect){ e.effect.onRemoved(self()); @@ -89,13 +89,15 @@ abstract class StatusComp implements Posc, Flyingc{ }); } - boolean isBoss(){ + public boolean isBoss(){ 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){ return Tmp.c1.set(Color.white); } @@ -168,6 +170,8 @@ abstract class StatusComp implements Posc, Flyingc{ applyDynamicStatus().armorOverride = armor; } + public abstract boolean isGrounded(); + @Override public void update(){ 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); } } diff --git a/core/src/mindustry/entities/comp/TankComp.java b/core/src/mindustry/entities/comp/TankComp.java index eb324b2158..d1bc238263 100644 --- a/core/src/mindustry/entities/comp/TankComp.java +++ b/core/src/mindustry/entities/comp/TankComp.java @@ -17,9 +17,9 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; @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 boolean hovering, disarmed; + @Import boolean disarmed; @Import UnitType type; @Import Team team; @@ -27,6 +27,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec transient float treadTime; transient boolean walked; + transient Floor lastDeepFloor; @Override 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 int r = Math.max((int)(hitSize * 0.6f / tilesize), 0); @@ -62,6 +66,12 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec 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 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 @@ -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)); //trigger animation only when walking manually @@ -89,7 +103,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec @Override @Replace 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 return on.speedMultiplier * speedMultiplier * lastSlowdown; } @@ -97,17 +111,7 @@ abstract class TankComp implements Posc, Flyingc, Hitboxc, Unitc, ElevationMovec @Replace @Override public @Nullable Floor drownFloor(){ - //tanks can only drown when all the nearby floors are deep - //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; + return canDrown() ? lastDeepFloor : null; } @Override diff --git a/core/src/mindustry/entities/comp/UnderwaterMoveComp.java b/core/src/mindustry/entities/comp/UnderwaterMoveComp.java new file mode 100644 index 0000000000..1159043120 --- /dev/null +++ b/core/src/mindustry/entities/comp/UnderwaterMoveComp.java @@ -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); + } +} + diff --git a/core/src/mindustry/entities/comp/UnitComp.java b/core/src/mindustry/entities/comp/UnitComp.java index eaef42f100..266c9c122a 100644 --- a/core/src/mindustry/entities/comp/UnitComp.java +++ b/core/src/mindustry/entities/comp/UnitComp.java @@ -7,7 +7,6 @@ import arc.math.*; import arc.math.geom.*; import arc.scene.ui.layout.*; import arc.util.*; -import mindustry.ai.*; import mindustry.ai.types.*; import mindustry.annotations.Annotations.*; import mindustry.async.*; @@ -34,10 +33,12 @@ import static mindustry.Vars.*; import static mindustry.logic.GlobalVars.*; @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 float x, y, rotation, elevation, maxHealth, drag, armor, hitSize, health, shield, ammo, dragMultiplier, armorOverride, speedMultiplier; + @Import boolean dead, disarmed; + @Import float x, y, rotation, maxHealth, drag, armor, hitSize, health, shield, ammo, dragMultiplier, armorOverride, speedMultiplier; @Import Team team; @Import int id; @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 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. */ 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 @Replace 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 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){ 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; } - public boolean canTarget(Unit other){ - return other != null && other.checkTarget(type.targetAir, type.targetGround); + public boolean canTarget(Teamc other){ + return other != null && (other instanceof Unit u ? u.checkTarget(type.targetAir, type.targetGround) : (other instanceof Building b && type.targetGround)); } public CommandAI command(){ @@ -475,9 +507,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I this.drag = type.drag; this.armor = type.armor; this.hitSize = type.hitSize; - this.hovering = type.hovering; - if(controller == null) controller(type.createController(self())); if(mounts().length != type.weapons.size) setupWeapons(type); if(abilities.length != 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(); } } + if(controller == null) controller(type.createController(self())); + } + + public boolean playerControllable(){ + return type.playerControllable; } public boolean targetable(Team targeter){ @@ -504,7 +539,8 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I @Override public void afterRead(){ - afterSync(); + setType(this.type); + controller.unit(self()); //reset controller state if(!(controller instanceof AIController ai && ai.keepState())){ controller(type.createController(self())); @@ -512,7 +548,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I } @Override - public void afterAllRead(){ + public void afterReadAll(){ 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 public void update(){ 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){ healTime = 1f; } @@ -647,8 +770,9 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I } } - Tile tile = tileOn(); - Floor floor = floorOn(); + if(tile != null && tile.build != null){ + tile.build.unitOnAny(self()); + } if(tile != null && isGrounded() && !type.hovering){ //unit block update @@ -673,7 +797,7 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I } //AI only updates on the server - if(!net.client() && !dead){ + if(!net.client() && !dead && shouldUpdateController()){ 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. */ public TextureRegion icon(){ return type.uiIcon; @@ -770,11 +898,6 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I type.display(self(), table); } - @Override - public boolean isImmune(StatusEffect effect){ - return type.immunities.contains(effect); - } - @Override public void draw(){ type.draw(self()); diff --git a/core/src/mindustry/entities/comp/VelComp.java b/core/src/mindustry/entities/comp/VelComp.java index 8be3a30151..0a68f1c56a 100644 --- a/core/src/mindustry/entities/comp/VelComp.java +++ b/core/src/mindustry/entities/comp/VelComp.java @@ -39,6 +39,10 @@ abstract class VelComp implements Posc{ return null; } + boolean ignoreSolids(){ + return false; + } + /** @return whether this entity can move through a location*/ boolean canPass(int tileX, int tileY){ SolidPred s = solidity(); diff --git a/core/src/mindustry/entities/comp/WaterCrawlComp.java b/core/src/mindustry/entities/comp/WaterCrawlComp.java new file mode 100644 index 0000000000..e49cc013ff --- /dev/null +++ b/core/src/mindustry/entities/comp/WaterCrawlComp.java @@ -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; + } +} + diff --git a/core/src/mindustry/entities/comp/WaterMoveComp.java b/core/src/mindustry/entities/comp/WaterMoveComp.java index 9509b891f6..e442ec9b3f 100644 --- a/core/src/mindustry/entities/comp/WaterMoveComp.java +++ b/core/src/mindustry/entities/comp/WaterMoveComp.java @@ -4,7 +4,6 @@ import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; import arc.util.*; -import mindustry.ai.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.entities.*; @@ -18,7 +17,7 @@ import mindustry.world.blocks.environment.*; import static mindustry.Vars.*; @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 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 public void add(){ tleft.clear(); @@ -59,6 +45,7 @@ abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Flyingc, Unitc{ @Override public void draw(){ + //TODO: move to UnitType float z = Draw.z(); Draw.z(Layer.debris); @@ -76,7 +63,7 @@ abstract class WaterMoveComp implements Posc, Velc, Hitboxc, Flyingc, Unitc{ @Replace @Override public SolidPred solidity(){ - return isFlying() ? null : EntityCollisions::waterSolid; + return isFlying() || ignoreSolids() ? null : EntityCollisions::waterSolid; } @Replace diff --git a/core/src/mindustry/entities/part/RegionPart.java b/core/src/mindustry/entities/part/RegionPart.java index 527ccae184..9474e819b7 100644 --- a/core/src/mindustry/entities/part/RegionPart.java +++ b/core/src/mindustry/entities/part/RegionPart.java @@ -23,6 +23,8 @@ public class RegionPart extends DrawPart{ public boolean mirror = false; /** If true, an outline is drawn under the part. */ 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. */ public boolean drawRegion = true; /** If true, the heat region produces light. */ @@ -38,7 +40,8 @@ public class RegionPart extends DrawPart{ public Blending blending = Blending.normal; public float layer = -1, layerOffset = 0f, heatLayerOffset = 1f, turretHeatLayer = Layer.turretHeat; 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 heatLightOpacity = 0.3f; public @Nullable Color color, colorTo, mixColor, mixColorTo; @@ -99,16 +102,21 @@ public class RegionPart extends DrawPart{ float sign = (i == 0 ? 1 : -1) * params.sideMultiplier; 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 rx = params.x + Tmp.v1.x, ry = params.y + Tmp.v1.y, rot = mr * sign + params.rotation - 90; - Draw.xscl *= sign; - if(outline && drawRegion){ 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); } @@ -126,7 +134,7 @@ public class RegionPart extends DrawPart{ } Draw.blend(blending); - Draw.rect(region, rx, ry, rot); + rect(region, rx, ry, rot); Draw.blend(); if(color != null) Draw.color(); } @@ -134,7 +142,7 @@ public class RegionPart extends DrawPart{ if(heat.found()){ float hprog = heatProgress.getClamp(params, clampProgress); 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); } @@ -167,6 +175,11 @@ public class RegionPart extends DrawPart{ 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 public void load(String name){ String realName = this.name == null ? name + suffix : this.name; diff --git a/core/src/mindustry/entities/pattern/ShootHelix.java b/core/src/mindustry/entities/pattern/ShootHelix.java index 0b61c594c1..cf1dd1ad31 100644 --- a/core/src/mindustry/entities/pattern/ShootHelix.java +++ b/core/src/mindustry/entities/pattern/ShootHelix.java @@ -6,6 +6,20 @@ import arc.util.*; public class ShootHelix extends ShootPattern{ 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 public void shoot(int totalShots, BulletHandler handler, @Nullable Runnable barrelIncrementer){ for(int i = 0; i < shots; i++){ diff --git a/core/src/mindustry/entities/pattern/ShootSpread.java b/core/src/mindustry/entities/pattern/ShootSpread.java index 0508fc8a91..471513be98 100644 --- a/core/src/mindustry/entities/pattern/ShootSpread.java +++ b/core/src/mindustry/entities/pattern/ShootSpread.java @@ -14,6 +14,10 @@ public class ShootSpread extends ShootPattern{ public ShootSpread(){ } + public static ShootSpread circle(int points){ + return new ShootSpread(points, 360f / points); + } + @Override public void shoot(int totalShots, BulletHandler handler, @Nullable Runnable barrelIncrementer){ for(int i = 0; i < shots; i++){ diff --git a/core/src/mindustry/entities/units/AIController.java b/core/src/mindustry/entities/units/AIController.java index f91b707699..47d3611755 100644 --- a/core/src/mindustry/entities/units/AIController.java +++ b/core/src/mindustry/entities/units/AIController.java @@ -21,11 +21,12 @@ public class AIController implements UnitController{ protected Unit unit; protected Interval timer = new Interval(4); - protected AIController fallback; + protected @Nullable AIController fallback; protected float noTargetTime; /** main target that is being faced */ - protected Teamc target; + protected @Nullable Teamc target; + protected @Nullable Teamc bomberTarget; { resetTimers(); @@ -124,15 +125,27 @@ public class AIController implements UnitController{ } 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(); 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(){ @@ -146,6 +159,9 @@ public class AIController implements UnitController{ noTargetTime += Time.delta; if(invalid(target)){ + if(target != null && !target.isAdded()){ + targetInvalidated(); + } target = null; }else{ noTargetTime = 0f; @@ -185,6 +201,13 @@ public class AIController implements UnitController{ if(mount.target != null){ 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); mount.aimX = to.x; mount.aimY = to.y; diff --git a/core/src/mindustry/entities/units/UnitController.java b/core/src/mindustry/entities/units/UnitController.java index 240728b2cc..b8e282207c 100644 --- a/core/src/mindustry/entities/units/UnitController.java +++ b/core/src/mindustry/entities/units/UnitController.java @@ -31,8 +31,4 @@ public interface UnitController{ default void afterRead(Unit unit){ } - - default boolean isBeingControlled(Unit player){ - return false; - } } diff --git a/core/src/mindustry/game/EventType.java b/core/src/mindustry/game/EventType.java index 4fdd36885a..84e6ae3afa 100644 --- a/core/src/mindustry/game/EventType.java +++ b/core/src/mindustry/game/EventType.java @@ -9,6 +9,7 @@ import mindustry.net.*; import mindustry.net.Packets.*; import mindustry.type.*; import mindustry.world.*; +import mindustry.world.blocks.environment.*; import mindustry.world.blocks.storage.CoreBlock.*; public class EventType{ @@ -82,6 +83,8 @@ public class EventType{ public static class BlockInfoEvent{} /** Called *after* all content has been initialized. */ 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. */ public static class ModContentLoadEvent{} /** 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. * Event object is reused, do not nest! diff --git a/core/src/mindustry/game/FogControl.java b/core/src/mindustry/game/FogControl.java index 0753b44ee8..8d67a35d7f 100644 --- a/core/src/mindustry/game/FogControl.java +++ b/core/src/mindustry/game/FogControl.java @@ -36,6 +36,7 @@ public final class FogControl implements CustomChunk{ private boolean justLoaded = false; private boolean loadedStatic = false; + private int lastEntityUpdateIndex = 0; public FogControl(){ Events.on(ResetEvent.class, e -> { @@ -131,6 +132,7 @@ public final class FogControl implements CustomChunk{ } void stop(){ + lastEntityUpdateIndex = 0; fog = null; //I don't care whether the fog thread crashes here, it's about to die anyway staticEvents.clear(); @@ -214,6 +216,31 @@ public final class FogControl implements CustomChunk{ //clear to prepare for queuing fog radius from units and buildings 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){ //AI teams do not have fog if(!team.team.isOnlyAI()){ diff --git a/core/src/mindustry/game/MapObjectives.java b/core/src/mindustry/game/MapObjectives.java index 7f158b264a..7e7d5af4af 100644 --- a/core/src/mindustry/game/MapObjectives.java +++ b/core/src/mindustry/game/MapObjectives.java @@ -277,6 +277,11 @@ public class MapObjectives implements Iterable, Eachable, Eachable, Eachable, Eachable, Eachable, Eachable, Eachable, Eachable, Eachable cons){ + Unit unit = type.spawn(team, x, y, rotation, cons); if(effect != null){ unit.apply(effect, 999999f); @@ -104,6 +103,11 @@ public class SpawnGroup implements JsonSerializable, Cloneable{ 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 public void write(Json json){ if(type == null) type = UnitTypes.dagger; @@ -120,7 +124,7 @@ public class SpawnGroup implements JsonSerializable, Cloneable{ if(spawn != -1) json.writeValue("spawn", spawn); 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(team != null) json.writeValue("team", team.id); } @Override @@ -140,6 +144,7 @@ public class SpawnGroup implements JsonSerializable, Cloneable{ 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("items")) items = json.readValue(ItemStack.class, data.get("items")); + if(data.has("team")) team = Team.get(data.getInt("team")); //old boss effect ID diff --git a/core/src/mindustry/game/Team.java b/core/src/mindustry/game/Team.java index 7da94f45df..962d24bbdf 100644 --- a/core/src/mindustry/game/Team.java +++ b/core/src/mindustry/game/Team.java @@ -19,6 +19,7 @@ public class Team implements Comparable, Senseable{ public final Color color = new Color(); public final Color[] palette = {new Color(), new Color(), new Color()}; public final int[] palettei = new int[3]; + public boolean ignoreUnitCap = false; public String emoji = ""; public boolean hasPalette; public String name; @@ -50,6 +51,8 @@ public class Team implements Comparable, Senseable{ 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()); + + neoplastic.ignoreUnitCap = true; } public static Team get(int id){ @@ -95,10 +98,16 @@ public class Team implements Comparable, Senseable{ 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(){ 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. */ public boolean isAI(){ return (state.rules.waves || state.rules.attackMode) && this != state.rules.defaultTeam && !state.rules.pvp; @@ -114,12 +123,6 @@ public class Team implements Comparable, Senseable{ return isAI() && !rules().rtsAi; } - /** @deprecated There is absolutely no reason to use this. */ - @Deprecated - public boolean isEnemy(Team other){ - return this != other; - } - public Seq cores(){ return state.teams.cores(this); } @@ -161,6 +164,7 @@ public class Team implements Comparable, Senseable{ @Override public double sense(LAccess sensor){ if(sensor == LAccess.id) return id; - return 0; + if(sensor == LAccess.color) return color.toDoubleBits(); + return Double.NaN; } } diff --git a/core/src/mindustry/game/Teams.java b/core/src/mindustry/game/Teams.java index eec1b68ee8..e4c5a9e190 100644 --- a/core/src/mindustry/game/Teams.java +++ b/core/src/mindustry/game/Teams.java @@ -126,6 +126,15 @@ public class Teams{ 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){ TeamData data = get(core.team); //add core if not present @@ -407,13 +416,18 @@ public class Teams{ } 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(){ 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(){ return cores.isEmpty(); } diff --git a/core/src/mindustry/graphics/BlockRenderer.java b/core/src/mindustry/graphics/BlockRenderer.java index 10e29ef135..4949a90bc3 100644 --- a/core/src/mindustry/graphics/BlockRenderer.java +++ b/core/src/mindustry/graphics/BlockRenderer.java @@ -49,6 +49,7 @@ public class BlockRenderer{ private IntSet procLinks = new IntSet(), procLights = new IntSet(); 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)); public BlockRenderer(){ @@ -64,7 +65,9 @@ public class BlockRenderer{ Events.on(WorldLoadEvent.class, event -> { 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())); + shadowEvents.clear(); updateFloors.clear(); lastCamY = lastCamX = -99; //invalidate camera position so blocks get updated @@ -93,7 +96,7 @@ public class BlockRenderer{ 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); } } @@ -116,7 +119,10 @@ public class BlockRenderer{ Events.on(TilePreChangeEvent.class, event -> { 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); }); @@ -209,7 +215,10 @@ public class BlockRenderer{ } 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); } @@ -295,7 +304,7 @@ public class BlockRenderer{ for(Tile tile : shadowEvents){ if(tile == null) continue; //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); } @@ -354,16 +363,17 @@ public class BlockRenderer{ //draw floor lights 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 -> { if(tile.build == null || procLinks.add(tile.build.id)){ tileview.add(tile); } - //lights are drawn even in the expanded range - if(((tile.build != null && procLights.add(tile.build.pos())) || tile.block().emitLight)){ - lightview.add(tile); - } - if(tile.build != null && tile.build.power != null && tile.build.power.links.size > 0){ for(Building other : tile.build.getPowerConnections(outArray2)){ if(other.block instanceof PowerNode && procLinks.add(other.id)){ //TODO need a generic way to render connections! @@ -519,6 +529,24 @@ public class BlockRenderer{ } } + static class BlockLightQuadtree extends QuadTree{ + + 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 newChild(Rect rect){ + return new BlockLightQuadtree(rect); + } + } + static class FloorQuadtree extends QuadTree{ public FloorQuadtree(Rect bounds){ @@ -528,7 +556,7 @@ public class BlockRenderer{ @Override public void hitbox(Tile tile){ 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 diff --git a/core/src/mindustry/graphics/CacheLayer.java b/core/src/mindustry/graphics/CacheLayer.java index 9ffd468ac2..31de034b7f 100644 --- a/core/src/mindustry/graphics/CacheLayer.java +++ b/core/src/mindustry/graphics/CacheLayer.java @@ -17,6 +17,7 @@ public class CacheLayer{ public static CacheLayer[] all = {}; public int id; + public boolean liquid; /** Registers cache layers that will render before the 'normal' layer. */ public static void add(CacheLayer... layers){ @@ -66,7 +67,7 @@ public class CacheLayer{ slag = new ShaderLayer(Shaders.slag), arkycite = new ShaderLayer(Shaders.arkycite), cryofluid = new ShaderLayer(Shaders.cryofluid), - space = new ShaderLayer(Shaders.space), + space = new ShaderLayer(Shaders.space, false), normal = new CacheLayer(), walls = new CacheLayer() ); @@ -86,7 +87,11 @@ public class CacheLayer{ public @Nullable 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; } @@ -94,7 +99,6 @@ public class CacheLayer{ public void begin(){ if(!Core.settings.getBool("animatedwater")) return; - renderer.blocks.floor.endc(); renderer.effectBuffer.begin(); Core.graphics.clear(Color.clear); renderer.blocks.floor.beginc(); @@ -104,11 +108,8 @@ public class CacheLayer{ public void end(){ if(!Core.settings.getBool("animatedwater")) return; - renderer.blocks.floor.endc(); renderer.effectBuffer.end(); - renderer.effectBuffer.blit(shader); - renderer.blocks.floor.beginc(); } } diff --git a/core/src/mindustry/graphics/Drawf.java b/core/src/mindustry/graphics/Drawf.java index 497736e6c9..caf5a067c6 100644 --- a/core/src/mindustry/graphics/Drawf.java +++ b/core/src/mindustry/graphics/Drawf.java @@ -26,6 +26,10 @@ public class Drawf{ } } + public static void underwater(Runnable run){ + renderer.blocks.floor.drawUnderwater(run); + } + //TODO offset unused 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); @@ -130,6 +134,17 @@ public class Drawf{ 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){ float pz = Draw.z(); Draw.z(layer); @@ -141,6 +156,17 @@ public class Drawf{ 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){ if(start.within(dest, len1 + len2)){ return; @@ -267,7 +293,7 @@ public class Drawf{ } public static void selected(Building tile, Color color){ - selected(tile.tile(), color); + selected(tile.tile, color); } public static void selected(Tile tile, Color color){ diff --git a/core/src/mindustry/graphics/FloorRenderer.java b/core/src/mindustry/graphics/FloorRenderer.java index 872c71953c..754d6d7d27 100644 --- a/core/src/mindustry/graphics/FloorRenderer.java +++ b/core/src/mindustry/graphics/FloorRenderer.java @@ -42,21 +42,29 @@ public class FloorRenderer{ private static final boolean dynamic = false; private float[] vertices = new float[maxSprites * vertexSize * 4]; - private short[] indices = new short[maxSprites * 6]; private int vidx; private FloorRenderBatch batch = new FloorRenderBatch(); private Shader shader; private Texture texture; private TextureRegion error; - private Mesh[][][] cache; + private IndexData indexData; + private ChunkMesh[][][] cache; private IntSet drawnLayerSet = new IntSet(); private IntSet recacheSet = new IntSet(); private IntSeq drawnLayers = new IntSeq(); private ObjectSet used = new ObjectSet<>(); + private Seq 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(){ short j = 0; + short[] indices = new short[maxSprites * 6]; for(int i = 0; i < indices.length; i += 6, j += 4){ indices[i] = j; indices[i + 1] = (short)(j + 1); @@ -66,6 +74,14 @@ public class FloorRenderer{ 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( """ attribute vec4 a_position; @@ -121,6 +137,8 @@ public class FloorRenderer{ drawnLayers.clear(); drawnLayerSet.clear(); + Rect bounds = camera.bounds(Tmp.r3); + //preliminary layer check for(int x = minx; x <= maxx; x++){ for(int y = miny; y <= maxy; y++){ @@ -131,11 +149,11 @@ public class FloorRenderer{ cacheChunk(x, y); } - Mesh[] chunk = cache[x][y]; + ChunkMesh[] chunk = cache[x][y]; //loop through all layers, and add layer index if it exists for(int i = 0; i < layers; i++){ - if(chunk[i] != null && i != CacheLayer.walls.id){ + if(chunk[i] != null && i != CacheLayer.walls.id && chunk[i].bounds.overlaps(bounds)){ drawnLayerSet.add(i); } } @@ -149,16 +167,13 @@ public class FloorRenderer{ drawnLayers.sort(); - Draw.flush(); beginDraw(); for(int i = 0; i < drawnLayers.size; i++){ - CacheLayer layer = CacheLayer.all[drawnLayers.get(i)]; - - drawLayer(layer); + drawLayer(CacheLayer.all[drawnLayers.get(i)]); } - endDraw(); + underwaterDraw.clear(); } public void beginc(){ @@ -167,29 +182,6 @@ public class FloorRenderer{ //only ever use the base environment texture 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(){ @@ -205,6 +197,10 @@ public class FloorRenderer{ } } + public void drawUnderwater(Runnable run){ + underwaterDraw.add(run); + } + public void beginDraw(){ if(cache == null){ return; @@ -217,14 +213,6 @@ public class FloorRenderer{ Gl.enable(Gl.blend); } - public void endDraw(){ - if(cache == null){ - return; - } - - endc(); - } - public void drawLayer(CacheLayer layer){ if(cache == null){ return; @@ -240,6 +228,8 @@ public class FloorRenderer{ layer.begin(); + Rect bounds = camera.bounds(Tmp.r3); + for(int x = minx; x <= maxx; x++){ for(int y = miny; y <= maxy; y++){ @@ -249,33 +239,29 @@ public class FloorRenderer{ 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.vertices instanceof VertexBufferObject vbo && mesh.indices instanceof IndexBufferObject ibo){ - - //bindi the buffer and update its contents, but do not unnecessarily enable all the attributes again - vbo.bind(); - //set up vertex attribute pointers for this specific VBO - int offset = 0; - for(VertexAttribute attribute : attributes){ - int location = shader.getAttributeLocation(attribute.alias); - int aoffset = offset; - offset += attribute.size; - if(location < 0) continue; - - 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); + if(mesh != null && mesh.bounds.overlaps(bounds)){ + mesh.render(shader, Gl.triangles, 0, mesh.getMaxVertices() * 6 / 4); } } } + //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(); } @@ -298,7 +284,7 @@ public class FloorRenderer{ } 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]; @@ -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; Batch current = Core.batch; @@ -345,13 +331,13 @@ public class FloorRenderer{ Core.batch = current; int floats = vidx; - //every 4 vertices need 6 indices - int vertCount = floats / vertexSize, indCount = vertCount * 6/4; + ChunkMesh mesh = new ChunkMesh(true, floats / vertexSize, 0, attributes, + 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.setAutoBind(false); - mesh.setIndices(indices, 0, indCount); + //all vertices are shared + mesh.indices = indexData; return mesh; } @@ -372,7 +358,7 @@ public class FloorRenderer{ recacheSet.clear(); int chunksx = Mathf.ceil((float)(world.width()) / chunksize), chunksy = Mathf.ceil((float)(world.height()) / chunksize); - cache = new Mesh[chunksx][chunksy][dynamic ? 0 : CacheLayer.all.length]; + cache = new ChunkMesh[chunksx][chunksy][dynamic ? 0 : CacheLayer.all.length]; texture = Core.atlas.find("grass1").texture; 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{ + //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 protected void draw(TextureRegion region, float x, float y, float originX, float originY, float width, float height, float rotation){ diff --git a/core/src/mindustry/graphics/LightRenderer.java b/core/src/mindustry/graphics/LightRenderer.java index 7c30b94796..c20e841ab3 100644 --- a/core/src/mindustry/graphics/LightRenderer.java +++ b/core/src/mindustry/graphics/LightRenderer.java @@ -32,6 +32,8 @@ public class LightRenderer{ public void add(float x, float y, float radius, Color color, float opacity){ if(!enabled() || radius <= 0f) return; + //TODO: clipping. + float res = Color.toFloatBits(color.r, color.g, color.b, opacity); if(circles.size <= circleIndex) circles.add(new CircleLight()); diff --git a/core/src/mindustry/graphics/MenuRenderer.java b/core/src/mindustry/graphics/MenuRenderer.java index 949d03f914..ad32be8314 100644 --- a/core/src/mindustry/graphics/MenuRenderer.java +++ b/core/src/mindustry/graphics/MenuRenderer.java @@ -11,6 +11,7 @@ import arc.struct.*; import arc.util.*; import arc.util.noise.*; import mindustry.content.*; +import mindustry.game.*; import mindustry.type.*; import mindustry.world.*; @@ -239,35 +240,17 @@ public class MenuRenderer implements Disposable{ } private void drawFlyers(){ - Draw.color(0f, 0f, 0f, 0.4f); - - TextureRegion icon = flyerType.fullIcon; + flyerType.sample.elevation = 1f; + flyerType.sample.team = Team.sharded; + flyerType.sample.rotation = flyerRot; + flyerType.sample.heal(); 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){ diff --git a/core/src/mindustry/graphics/MinimapRenderer.java b/core/src/mindustry/graphics/MinimapRenderer.java index e5f42b2c9a..6ad5544c42 100644 --- a/core/src/mindustry/graphics/MinimapRenderer.java +++ b/core/src/mindustry/graphics/MinimapRenderer.java @@ -30,8 +30,6 @@ public class MinimapRenderer{ private Rect rect = new Rect(); private float zoom = 4; - private float lastX, lastY, lastW, lastH, lastScl; - private boolean worldSpace; private IntSet updates = new IntSet(); private float updateCounter = 0f; @@ -123,13 +121,7 @@ public class MinimapRenderer{ region = new TextureRegion(texture); } - public void drawEntities(float x, float y, float w, float h, float scaling, boolean fullView){ - lastX = x; - lastY = y; - lastW = w; - lastH = h; - lastScl = scaling; - worldSpace = fullView; + public void drawEntities(float x, float y, float w, float h, boolean fullView){ if(!fullView){ updateUnitArray(); @@ -150,12 +142,12 @@ public class MinimapRenderer{ float scaleFactor; var trans = Tmp.m1.idt(); - trans.translate(lastX, lastY); - if(!worldSpace){ - trans.scl(Tmp.v1.set(scaleFactor = lastW / rect.width, lastH / rect.height)); + trans.translate(x, y); + if(!fullView){ + trans.scl(Tmp.v1.set(scaleFactor = w / rect.width, h / rect.height)); trans.translate(-rect.x, -rect.y); }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); Draw.trans(trans); diff --git a/core/src/mindustry/graphics/OverlayRenderer.java b/core/src/mindustry/graphics/OverlayRenderer.java index 6ec2d283ac..f2630eee53 100644 --- a/core/src/mindustry/graphics/OverlayRenderer.java +++ b/core/src/mindustry/graphics/OverlayRenderer.java @@ -131,7 +131,7 @@ public class OverlayRenderer{ Building build = (select instanceof BlockUnitc b ? b.tile() : select instanceof Building b ? b : null); 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()); } diff --git a/core/src/mindustry/graphics/Pal.java b/core/src/mindustry/graphics/Pal.java index ba63dcdb68..97a219ae87 100644 --- a/core/src/mindustry/graphics/Pal.java +++ b/core/src/mindustry/graphics/Pal.java @@ -116,6 +116,8 @@ public class Pal{ neoplasm1 = Color.valueOf("f98f4a"), neoplasmMid = Color.valueOf("e05438"), neoplasm2 = Color.valueOf("9e172c"), + neoplasmAcid = Color.valueOf("8ead44"), + neoplasmAcidGlow = Color.valueOf("68e43e"), logicBlocks = Color.valueOf("d4816b"), logicControl = Color.valueOf("6bb2b2"), diff --git a/core/src/mindustry/graphics/g3d/PlanetParams.java b/core/src/mindustry/graphics/g3d/PlanetParams.java index 6c7d2e6dc3..b13d54751b 100644 --- a/core/src/mindustry/graphics/g3d/PlanetParams.java +++ b/core/src/mindustry/graphics/g3d/PlanetParams.java @@ -18,8 +18,6 @@ public class PlanetParams{ public Vec3 camUp = new Vec3(0f, 1f, 0f); /** the unit length direction vector of the camera **/ 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. */ public Planet planet = Planets.serpulo; diff --git a/core/src/mindustry/input/DesktopInput.java b/core/src/mindustry/input/DesktopInput.java index 0ec79e1ab6..c0212b8081 100644 --- a/core/src/mindustry/input/DesktopInput.java +++ b/core/src/mindustry/input/DesktopInput.java @@ -474,7 +474,7 @@ public class DesktopInput extends InputHandler{ cursorType = cursor.build.getCursor(); } - if(canRepairDerelict(cursor)){ + if(canRepairDerelict(cursor) && !player.dead() && player.unit().canBuild()){ cursorType = ui.repairCursor; } diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index 36c55360cd..757852c2fa 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -312,6 +312,9 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ //only assign a group when this is not a queued command if(ai.commandQueue.size == 0 && unitIds.length > 1){ int layer = unit.collisionLayer(); + + if(layer == -1) layer = 0; + if(groups[layer] == null){ 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(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.itemAmount = amount; }))){ @@ -606,7 +609,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ if(build == null) return; 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."); } @@ -693,7 +696,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ if(unit == null){ //just clear the unit (is this used?) player.clearUnit(); //make sure it's AI controlled, so players can't overwrite each other - }else if(unit.isAI() && unit.team == player.team() && !unit.dead && unit.type.playerControllable){ + }else if(unit.isAI() && unit.team == player.team() && !unit.dead && unit.playerControllable()){ if(net.client() && player.isLocal()){ player.justSwitchFrom = player.unit(); player.justSwitchTo = unit; @@ -878,7 +881,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } 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){ //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(){ - 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){ unit.hitbox(Tmp.r1); 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){ diff --git a/core/src/mindustry/input/MobileInput.java b/core/src/mindustry/input/MobileInput.java index f667e7444f..2531bf40bb 100644 --- a/core/src/mindustry/input/MobileInput.java +++ b/core/src/mindustry/input/MobileInput.java @@ -706,7 +706,7 @@ public class MobileInput extends InputHandler implements GestureListener{ payloadTarget = null; //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); recentRespawnTimer = 1f; }else if(buildingTapped != null && state.rules.possessionAllowed){ diff --git a/core/src/mindustry/io/JsonIO.java b/core/src/mindustry/io/JsonIO.java index 34957d1443..9906aa2655 100644 --- a/core/src/mindustry/io/JsonIO.java +++ b/core/src/mindustry/io/JsonIO.java @@ -308,6 +308,7 @@ public class JsonIO{ } exec.all.add(obj); + obj.validate(); } // Second iteration to map the parents. diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index 1dff34d35f..2b5915156f 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -382,6 +382,7 @@ public abstract class SaveVersion extends SaveFileReader{ writeChunk(stream, true, out -> { out.writeByte(entity.classId()); out.writeInt(entity.id()); + entity.beforeWrite(); 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 var mapping = this.entityMapping == null ? EntityMapping.idMap : this.entityMapping; - Seq entities = new Seq<>(); - int amount = stream.readInt(); for(int j = 0; j < amount; j++){ readChunk(stream, true, in -> { @@ -450,7 +449,6 @@ public abstract class SaveVersion extends SaveFileReader{ int id = in.readInt(); Entityc entity = (Entityc)mapping[typeid].get(); - entities.add(entity); EntityGroup.checkNextId(id); entity.id(id); entity.read(Reads.get(in)); @@ -458,9 +456,7 @@ public abstract class SaveVersion extends SaveFileReader{ }); } - for(var e : entities){ - e.afterAllRead(); - } + Groups.all.each(Entityc::afterReadAll); } public void readEntityMapping(DataInput stream) throws IOException{ diff --git a/core/src/mindustry/logic/LExecutor.java b/core/src/mindustry/logic/LExecutor.java index 1ba96cc93a..8c69ebe3bb 100644 --- a/core/src/mindustry/logic/LExecutor.java +++ b/core/src/mindustry/logic/LExecutor.java @@ -1434,7 +1434,7 @@ public class LExecutor{ if(type.obj() instanceof UnitType type && !type.internal && !type.hidden && t != null && Units.canCreate(t, type)){ //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)); - spawner.spawnEffect(unit, rotation.numf()); + spawner.spawnEffect(unit); result.setobj(unit); } } @@ -1603,11 +1603,12 @@ public class LExecutor{ }else if(full){ //disable the rule, covers the whole map if(set){ + int prevX = state.rules.limitX, prevY = state.rules.limitY, prevW = state.rules.limitWidth, prevH = state.rules.limitHeight; state.rules.limitMapArea = false; if(!headless){ renderer.updateAllDarkness(); } - world.checkMapArea(); + world.checkMapArea(prevX, prevY, prevW, prevH); return false; } } @@ -1616,12 +1617,20 @@ public class LExecutor{ } 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.limitX = x; state.rules.limitY = y; state.rules.limitWidth = w; state.rules.limitHeight = h; - world.checkMapArea(); + world.checkMapArea(prevX, prevY, prevW, prevH); if(!headless){ renderer.updateAllDarkness(); @@ -1933,9 +1942,7 @@ public class LExecutor{ for(int i = 0; i < spawned; i++){ Tmp.v1.rnd(spread); - Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1); - unit.set(spawnX + Tmp.v1.x, spawnY + Tmp.v1.y); - Vars.spawner.spawnEffect(unit); + spawner.spawnUnit(group, spawnX + Tmp.v1.x, spawnY + Tmp.v1.y); } } } diff --git a/core/src/mindustry/mod/ContentParser.java b/core/src/mindustry/mod/ContentParser.java index 442348bf2e..9cd88c001e 100644 --- a/core/src/mindustry/mod/ContentParser.java +++ b/core/src/mindustry/mod/ContentParser.java @@ -136,6 +136,8 @@ public class ContentParser{ put(BulletType.class, (type, data) -> { if(data.isString()){ 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); data.remove("type"); diff --git a/core/src/mindustry/mod/Mods.java b/core/src/mindustry/mod/Mods.java index 45dc6530ab..af694480a9 100644 --- a/core/src/mindustry/mod/Mods.java +++ b/core/src/mindustry/mod/Mods.java @@ -366,7 +366,7 @@ public class Mods implements Loadable{ } Log.debug("Time to generate icons: @", Time.elapsed()); - //dispose old atlas data + //replace old atlas data Core.atlas = packer.flush(filter, new TextureAtlas(){ PixmapRegion fake = new PixmapRegion(new Pixmap(1, 1)); boolean didWarn = false; @@ -392,6 +392,8 @@ public class Mods implements Loadable{ Log.debug("Total pages: @", Core.atlas.getTextures().size); packer.printStats(); + + Events.fire(new AtlasPackEvent()); } packer.dispose(); @@ -1151,7 +1153,7 @@ public class Mods implements Loadable{ !skipModLoading() && Core.settings.getBool("mod-" + baseName + "-enabled", true) && Version.isAtLeast(meta.minGameVersion) && - (meta.getMinMajor() >= 136 || headless) && + (meta.getMinMajor() >= minJavaModGameVersion || headless) && !skipModCode && initialize ){ @@ -1249,7 +1251,7 @@ public class Mods implements Loadable{ /** @return whether this is a java class mod. */ public boolean isJava(){ - return meta.java || main != null; + return meta.java || main != null || meta.main != null; } @Nullable @@ -1292,10 +1294,10 @@ public class Mods implements Loadable{ 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(){ //must be at least 136 to indicate v7 compat - return getMinMajor() < 136; + return getMinMajor() < (isJava() ? minJavaModGameVersion : minModGameVersion); } public int getMinMajor(){ @@ -1397,11 +1399,6 @@ public class Mods implements Loadable{ /** If set, load the mod content in this order by content names */ public String[] contentOrder; - public String displayName(){ - //useless, kept for legacy reasons - return displayName; - } - public String shortDescription(){ return Strings.truncate(subtitle == null ? (description == null || description.length() > maxModSubtitleLength ? "" : description) : subtitle, maxModSubtitleLength, "..."); } diff --git a/core/src/mindustry/service/GameService.java b/core/src/mindustry/service/GameService.java index 4cad46716b..341fad2879 100644 --- a/core/src/mindustry/service/GameService.java +++ b/core/src/mindustry/service/GameService.java @@ -198,7 +198,7 @@ public class GameService{ if(campaign() && e.unit != null && e.unit.isLocal() && !e.breaking){ SStat.blocksBuilt.add(); - if(e.tile.block() == Blocks.router && e.tile.build.proximity().contains(t -> t.block == Blocks.router)){ + if(e.tile.block() == Blocks.router && e.tile.build.proximity.contains(t -> t.block == Blocks.router)){ chainRouters.complete(); } diff --git a/core/src/mindustry/type/UnitType.java b/core/src/mindustry/type/UnitType.java index 68513a7f58..8962c8e138 100644 --- a/core/src/mindustry/type/UnitType.java +++ b/core/src/mindustry/type/UnitType.java @@ -135,6 +135,8 @@ public class UnitType extends UnlockableContent implements Senseable{ lightRadius = -1f, /** light color opacity*/ 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. */ fogRadius = -1f, @@ -159,6 +161,8 @@ public class UnitType extends UnlockableContent implements Senseable{ faceTarget = true, /** AI flag: if true, this flying unit circles around its target like a bomber */ 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*/ canBoost = false, /** if true, this unit will always boost when using builder AI */ @@ -199,7 +203,7 @@ public class UnitType extends UnlockableContent implements Senseable{ allowLegStep = false, /** for legged units, setting this to false forces it to be on the ground physics layer. */ 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, /** 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, @@ -219,6 +223,8 @@ public class UnitType extends UnlockableContent implements Senseable{ hidden = false, /** if true, this unit is for internal use only and does not have a sprite generated. */ 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. */ bounded = true, /** 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, /** 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, + /** for vanilla content only - if false, skips the full icon generation step. */ + generateFullIcon = true, /** if true, this unit has a square shadow. */ squareShape = false, /** if true, this unit will draw its building beam towards blocks. */ @@ -248,6 +256,8 @@ public class UnitType extends UnlockableContent implements Senseable{ drawShields = true, /** if false, the unit body is not drawn. */ drawBody = true, + /** if false, the soft shadow is not drawn. */ + drawSoftShadow = true, /** if false, the unit is not drawn on the minimap. */ drawMinimap = true; @@ -300,6 +310,8 @@ public class UnitType extends UnlockableContent implements Senseable{ /** override for engine trail color */ 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. */ 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(). */ @@ -386,12 +398,18 @@ public class UnitType extends UnlockableContent implements Senseable{ /** how straight the leg outward angles are (0 = circular, 1 = horizontal line) */ 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". */ public boolean lockLegBase = false; /** If true, legs always try to move around even when the unit is not moving (leads to more natural behavior) */ public boolean legContinuousMove; /** TODO neither of these appear to do much */ 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 @@ -417,6 +435,14 @@ public class UnitType extends UnlockableContent implements Senseable{ /** number of independent segments */ 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 */ public float segmentMag = 2f, /** scale of sine offset between segments */ @@ -427,6 +453,10 @@ public class UnitType extends UnlockableContent implements Senseable{ segmentRotSpeed = 1f, /** maximum difference between segment angles */ 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. */ crawlSlowdown = 0.5f, /** damage dealt to blocks under this tank/crawler every frame. */ @@ -485,13 +515,49 @@ public class UnitType extends UnlockableContent implements Senseable{ 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 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); - out.set(x, y); + out.rotation = rotation; + out.set(x + offsetX, y + offsetY); 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; } + 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){ 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){ - 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(); var count = new float[]{-1}; @@ -699,12 +765,13 @@ public class UnitType extends UnlockableContent implements Senseable{ Unit example = constructor.get(); - allowLegStep = example instanceof Legsc; + allowLegStep = example instanceof Legsc || example instanceof Crawlc; //water preset - if(example instanceof WaterMovec){ + if(example instanceof WaterMovec || example instanceof WaterCrawlc){ naval = true; canDrown = false; + emitWalkSound = false; omniMovement = false; immunities.add(StatusEffects.wet); 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){ pathCost = naval ? ControlPathfinder.costNaval : - allowLegStep || example instanceof Crawlc ? ControlPathfinder.costLegs : + allowLegStep ? ControlPathfinder.costLegs : hovering ? ControlPathfinder.costHover : ControlPathfinder.costGround; } @@ -783,6 +859,10 @@ public class UnitType extends UnlockableContent implements Senseable{ mechStride = 4f + (hitSize -8f)/2.1f; } + if(segmentSpacing < 0){ + segmentSpacing = hitSize; + } + if(aimDst < 0){ aimDst = weapons.contains(w -> !w.rotate) ? hitSize * 2f : hitSize / 2f; } @@ -1229,19 +1309,27 @@ public class UnitType extends UnlockableContent implements Senseable{ //region drawing public void draw(Unit unit){ + float scl = xscl; if(unit.inFogTo(Vars.player.team())) return; - unit.drawBuilding(); - drawMining(unit); + if(buildSpeed > 0f){ + unit.drawBuilding(); + } + + if(unit.mining()){ + drawMining(unit); + } boolean isPayload = !unit.isAdded(); - Mechc mech = unit instanceof Mechc ? (Mechc)unit : null; - float z = isPayload ? Draw.z() : (unit.elevation > 0.5f ? flyingLayer : groundLayer) + Mathf.clamp(hitSize / 4000f, 0, 0.01f); - - if(unit.controller().isBeingControlled(player.unit())){ - drawControl(unit); - } + Mechc mech = unit instanceof Mechc m ? m : null; + Segmentc seg = unit instanceof Segmentc c ? c : null; + float z = + isPayload ? Draw.z() : + //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)){ Draw.z(Math.min(Layer.darkness, z - 1f)); @@ -1276,7 +1364,7 @@ public class UnitType extends UnlockableContent implements Senseable{ drawPayload((Unit & Payloadc)unit); } - drawSoftShadow(unit); + if(drawSoftShadow) drawSoftShadow(unit); Draw.z(z); @@ -1294,6 +1382,7 @@ public class UnitType extends UnlockableContent implements Senseable{ Draw.z(z); if(drawBody) drawBody(unit); if(drawCell && !(unit instanceof Crawlc)) drawCell(unit); + Draw.scl(scl); //TODO this is a hack for neoplasm turrets drawWeapons(unit); if(drawItems) drawItems(unit); 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){ float e = Mathf.clamp(unit.elevation, shadowElevation, 1f) * shadowElevationScl * (1f - unit.drownTime); 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){ Draw.color(0, 0, 0, 0.4f * alpha); 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.color(); } @@ -1528,6 +1608,11 @@ public class UnitType extends UnlockableContent implements Senseable{ public void drawBody(Unit 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.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)); } - Lines.stroke(legRegion.height * legRegion.scl() * flips); - Lines.line(legRegion, position.x, position.y, leg.joint.x, leg.joint.y, false); + if(legBaseUnder){ + 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.line(legBaseRegion, leg.joint.x + Tmp.v1.x, leg.joint.y + Tmp.v1.y, leg.base.x, leg.base.y, false); + Lines.stroke(legRegion.height * legRegion.scl() * flips); + 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()){ Draw.rect(jointRegion, leg.joint.x, leg.joint.y); @@ -1640,27 +1733,29 @@ public class UnitType extends UnlockableContent implements Senseable{ Draw.reset(); } - //TODO public void drawCrawl(Crawlc crawl){ Unit unit = (Unit)crawl; 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++){ TextureRegion[] regions = p == 0 ? segmentOutlineRegions : segmentRegions; 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 float rot = Mathf.slerp(crawl.segmentRot(), unit.rotation, i / (float)(segments - 1)); float tx = Angles.trnsx(rot, trns), ty = Angles.trnsy(rot, trns); //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); - applyColor(unit); + //applyColor(unit); //TODO merge outlines? Draw.rect(regions[i], unit.x + tx, unit.y + ty, rot - 90); diff --git a/core/src/mindustry/type/Weapon.java b/core/src/mindustry/type/Weapon.java index d097677d3c..50dfe94b01 100644 --- a/core/src/mindustry/type/Weapon.java +++ b/core/src/mindustry/type/Weapon.java @@ -46,7 +46,7 @@ public class Weapon implements Cloneable{ public boolean rotate = false; /** Whether to show the sprite of the weapon in the database. */ public boolean showStatSprite = true; - /** rotation at which this weapon starts at. TODO buggy!*/ + /** rotation at which this weapon starts at. */ public float baseRotation = 0f; /** whether to draw the outline on top. */ public boolean top = true; @@ -92,14 +92,16 @@ public class Weapon implements Cloneable{ public float shootX = 0f, shootY = 3f; /** offsets of weapon position on unit */ public float x = 5f, y = 0f; - /** Random spread on the X axis. */ - public float xRand = 0f; + /** Random spread on the X/Y axis. */ + public float xRand = 0f, yRand = 0f; /** pattern used for bullets */ public ShootPattern shoot = new ShootPattern(); /** radius of shadow drawn under the weapon; <0 to disable */ public float shadow = -1f; /** fraction of velocity that is random */ 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. */ public float shootCone = 5f; /** 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 unit.type.applyColor(unit); @@ -255,7 +259,7 @@ public class Weapon implements Cloneable{ Draw.color(); } - Draw.xscl = 1f; + Draw.xscl = prev; if(parts.size > 0){ //TODO does it need an outline? @@ -479,17 +483,18 @@ public class Weapon implements Cloneable{ mount.charging = false; float xSpread = Mathf.range(xRand), + ySpread = Mathf.range(yRand), weaponRotation = unit.rotation - 90 + (rotate ? mount.rotation : baseRotation), mountX = unit.x + Angles.trnsx(unit.rotation - 90, x, y), mountY = unit.y + Angles.trnsy(unit.rotation - 90, x, y), - bulletX = mountX + Angles.trnsx(weaponRotation, this.shootX + xOffset + xSpread, this.shootY + yOffset), - bulletY = mountY + Angles.trnsy(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 + ySpread), shootAngle = bulletRotation(unit, mount, bulletX, bulletY) + angleOffset, lifeScl = bullet.scaleLife ? Mathf.clamp(Mathf.dst(bulletX, bulletY, mount.aimX, mount.aimY) / bullet.range) : 1f, 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 - 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); if(!continuous){ diff --git a/core/src/mindustry/type/unit/MissileUnitType.java b/core/src/mindustry/type/unit/MissileUnitType.java index 6b0d3174fc..e80ec4c486 100644 --- a/core/src/mindustry/type/unit/MissileUnitType.java +++ b/core/src/mindustry/type/unit/MissileUnitType.java @@ -18,6 +18,7 @@ public class MissileUnitType extends UnitType{ logicControllable = false; isEnemy = false; useUnitCap = false; + drawCell = false; allowedInPayloads = false; controller = u -> new MissileAI(); flying = true; diff --git a/core/src/mindustry/type/weapons/PointDefenseBulletWeapon.java b/core/src/mindustry/type/weapons/PointDefenseBulletWeapon.java new file mode 100644 index 0000000000..f4c849a66a --- /dev/null +++ b/core/src/mindustry/type/weapons/PointDefenseBulletWeapon.java @@ -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; + } + } + +} diff --git a/core/src/mindustry/ui/Fonts.java b/core/src/mindustry/ui/Fonts.java index c02e09ff53..ef68462bf6 100644 --- a/core/src/mindustry/ui/Fonts.java +++ b/core/src/mindustry/ui/Fonts.java @@ -105,11 +105,38 @@ public class Fonts{ }); } - public static void loadContentIcons(){ - Seq fonts = Seq.with(Fonts.def, Fonts.outline); - Texture uitex = Core.atlas.find("logo").texture; + public static void registerIcon(String name, String regionName, int ch, TextureRegion region){ int size = (int)(Fonts.def.getData().lineHeight/Fonts.def.getData().scaleY); + unicodeIcons.put(name, ch); + stringIcons.put(name, ((char)ch) + ""); + unicodeToName.put(ch, regionName); + + Vec2 out = Scaling.fit.apply(region.width, region.height, size, size); + + Glyph glyph = new Glyph(); + glyph.id = ch; + glyph.srcX = 0; + glyph.srcY = 0; + glyph.width = (int)out.x; + glyph.height = (int)out.y; + glyph.u = region.u; + glyph.v = region.v2; + glyph.u2 = region.u2; + glyph.v2 = region.v; + glyph.xoffset = 0; + glyph.yoffset = -size; + glyph.xadvance = size; + glyph.kerning = null; + glyph.fixedWidth = true; + glyph.page = 0; + Fonts.def.getData().setGlyph(ch, glyph); + Fonts.outline.getData().setGlyph(ch, glyph); + } + + public static void loadContentIcons(){ + Texture uitex = Core.atlas.find("logo").texture; + try(var reader = Core.files.internal("icons/icons.properties").reader(Vars.bufferSize)){ String line; while((line = reader.readLine()) != null){ @@ -123,29 +150,7 @@ public class Fonts{ continue; } - unicodeIcons.put(nametex[0], ch); - stringIcons.put(nametex[0], ((char)ch) + ""); - unicodeToName.put(ch, texture); - - Vec2 out = Scaling.fit.apply(region.width, region.height, size, size); - - Glyph glyph = new Glyph(); - glyph.id = ch; - glyph.srcX = 0; - glyph.srcY = 0; - glyph.width = (int)out.x; - glyph.height = (int)out.y; - glyph.u = region.u; - glyph.v = region.v2; - glyph.u2 = region.u2; - glyph.v2 = region.v; - glyph.xoffset = 0; - glyph.yoffset = -size; - glyph.xadvance = size; - glyph.kerning = null; - glyph.fixedWidth = true; - glyph.page = 0; - fonts.each(f -> f.getData().setGlyph(ch, glyph)); + registerIcon(nametex[0], texture, ch, region); } }catch(IOException e){ throw new RuntimeException(e); @@ -153,11 +158,30 @@ public class Fonts{ stringIcons.put("alphachan", stringIcons.get("alphaaaa")); + //TODO: mod emojis can't work because most mod icons are not on the UI page! + /* + if(Vars.mods.list().contains(m -> m.shouldBeEnabled())){ + ContentType[] types = {ContentType.liquid, ContentType.item, ContentType.block, ContentType.status, ContentType.unit}; + int startChar = 0xE000 + 1; + + for(var type : types){ + for(var cont : Vars.content.getBy(type)){ + if(!cont.isVanilla() && cont instanceof UnlockableContent u && u.uiIcon.found()){ + int id = startChar; + + registerIcon(u.name, u.uiIcon instanceof AtlasRegion atlas ? atlas.name : u.name, id, u.uiIcon); + + startChar ++; + } + } + } + }*/ + for(Team team : Team.baseTeams){ team.emoji = stringIcons.get(team.name, ""); } } - + public static void loadContentIconsHeadless(){ try(var reader = Core.files.internal("icons/icons.properties").reader(Vars.bufferSize)){ String line; diff --git a/core/src/mindustry/ui/Minimap.java b/core/src/mindustry/ui/Minimap.java index ee03fc2435..e807ad3b75 100644 --- a/core/src/mindustry/ui/Minimap.java +++ b/core/src/mindustry/ui/Minimap.java @@ -56,7 +56,7 @@ public class Minimap extends Table{ if(renderer.minimap.getTexture() != null){ Draw.alpha(parentAlpha); - renderer.minimap.drawEntities(x, y, width, height, 0.75f, false); + renderer.minimap.drawEntities(x, y, width, height, false); } clipEnd(); diff --git a/core/src/mindustry/ui/dialogs/DatabaseDialog.java b/core/src/mindustry/ui/dialogs/DatabaseDialog.java index 262afe96e6..df3b0c68e7 100644 --- a/core/src/mindustry/ui/dialogs/DatabaseDialog.java +++ b/core/src/mindustry/ui/dialogs/DatabaseDialog.java @@ -119,7 +119,7 @@ public class DatabaseDialog extends BaseDialog{ for(int i = 0; i < array.size; i++){ UnlockableContent unlock = array.get(i); - Image image = unlocked(unlock) ? new Image(unlock.uiIcon).setScaling(Scaling.fit) : new Image(Icon.lock, Pal.gray); + Image image = unlocked(unlock) ? new Image(new TextureRegionDrawable(unlock.uiIcon), mobile ? Color.white : Color.lightGray).setScaling(Scaling.fit) : new Image(Icon.lock, Pal.gray); //banned cross if(state.isGame() && (unlock instanceof UnitType u && u.isBanned() || unlock instanceof Block b && state.rules.isBanned(b))){ diff --git a/core/src/mindustry/ui/dialogs/ModsDialog.java b/core/src/mindustry/ui/dialogs/ModsDialog.java index 4d955d9fef..0b59d0eb14 100644 --- a/core/src/mindustry/ui/dialogs/ModsDialog.java +++ b/core/src/mindustry/ui/dialogs/ModsDialog.java @@ -365,7 +365,7 @@ public class ModsDialog extends BaseDialog{ private @Nullable String getStateDetails(LoadedMod item){ if(item.isOutdated()){ - return "@mod.outdatedv7.details"; + return Core.bundle.format("mod.outdated.details", item.isJava() ? minJavaModGameVersion : minModGameVersion); }else if(item.isBlacklisted()){ return "@mod.blacklisted.details"; }else if(!item.isSupported()){ diff --git a/core/src/mindustry/ui/fragments/ConsoleFragment.java b/core/src/mindustry/ui/fragments/ConsoleFragment.java index 823c2d70a6..0374fdae62 100644 --- a/core/src/mindustry/ui/fragments/ConsoleFragment.java +++ b/core/src/mindustry/ui/fragments/ConsoleFragment.java @@ -179,6 +179,7 @@ public class ConsoleFragment extends Table{ public String injectConsoleVariables(){ return "var unit = Vars.player.unit();" + + "var player = Vars.player;" + "var team = Vars.player.team();" + "var core = Vars.player.core();" + "var items = Vars.player.team().items();" + diff --git a/core/src/mindustry/ui/fragments/HudFragment.java b/core/src/mindustry/ui/fragments/HudFragment.java index 0b414e59d4..397814ae4a 100644 --- a/core/src/mindustry/ui/fragments/HudFragment.java +++ b/core/src/mindustry/ui/fragments/HudFragment.java @@ -687,44 +687,6 @@ public class HudFragment{ } } - /** @deprecated see {@link CoreBuild#beginLaunch(CoreBlock)} */ - @Deprecated - public void showLaunch(){ - float margin = 30f; - - Image image = new Image(); - image.color.a = 0f; - image.touchable = Touchable.disabled; - image.setFillParent(true); - image.actions(Actions.delay((coreLandDuration - margin) / 60f), Actions.fadeIn(margin / 60f, Interp.pow2In), Actions.delay(6f / 60f), Actions.remove()); - image.update(() -> { - image.toFront(); - ui.loadfrag.toFront(); - if(state.isMenu()){ - image.remove(); - } - }); - Core.scene.add(image); - } - - /** @deprecated see {@link CoreBuild#beginLaunch(CoreBlock)} */ - @Deprecated - public void showLand(){ - Image image = new Image(); - image.color.a = 1f; - image.touchable = Touchable.disabled; - image.setFillParent(true); - image.actions(Actions.fadeOut(35f / 60f), Actions.remove()); - image.update(() -> { - image.toFront(); - ui.loadfrag.toFront(); - if(state.isMenu()){ - image.remove(); - } - }); - Core.scene.add(image); - } - private void toggleMenus(){ if(flip != null){ flip.getStyle().imageUp = shown ? Icon.downOpen : Icon.upOpen; diff --git a/core/src/mindustry/ui/fragments/MinimapFragment.java b/core/src/mindustry/ui/fragments/MinimapFragment.java index 2fe8df586e..d595b9beae 100644 --- a/core/src/mindustry/ui/fragments/MinimapFragment.java +++ b/core/src/mindustry/ui/fragments/MinimapFragment.java @@ -48,7 +48,7 @@ public class MinimapFragment{ Draw.rect(reg, w/2f + panx*zoom, h/2f + pany*zoom, size, size * ratio); Rect bounds = getRectBounds(); - renderer.minimap.drawEntities(bounds.x, bounds.y, bounds.width, bounds.height, zoom, true); + renderer.minimap.drawEntities(bounds.x, bounds.y, bounds.width, bounds.height, true); } Draw.reset(); diff --git a/core/src/mindustry/ui/fragments/PlacementFragment.java b/core/src/mindustry/ui/fragments/PlacementFragment.java index d798d69f96..e14edc448c 100644 --- a/core/src/mindustry/ui/fragments/PlacementFragment.java +++ b/core/src/mindustry/ui/fragments/PlacementFragment.java @@ -332,7 +332,7 @@ public class PlacementFragment{ }; //top table with hover info - frame.table(Tex.buttonEdge2,top -> { + frame.table(Tex.buttonEdge2, top -> { topTable = top; top.add(new Table()).growX().update(topTable -> { diff --git a/core/src/mindustry/world/Block.java b/core/src/mindustry/world/Block.java index 47afa2a88a..dd7995623b 100644 --- a/core/src/mindustry/world/Block.java +++ b/core/src/mindustry/world/Block.java @@ -156,6 +156,8 @@ public class Block extends UnlockableContent implements Senseable{ public boolean updateInUnits = true; /** if true, this block updates in payloads in units regardless of the experimental game rule */ public boolean alwaysUpdateInUnits = false; + /** if true, this block can be picked up in payloads */ + public boolean canPickup = true; /** if false, only incinerable liquids are dropped when deconstructing; otherwise, all liquids are dropped. */ public boolean deconstructDropAllLiquid = false; /** Whether to use this block's color in the minimap. Only used for overlays. */ @@ -174,6 +176,8 @@ public class Block extends UnlockableContent implements Senseable{ public float armor = 0f; /** base block explosiveness */ public float baseExplosiveness = 0f; + /** base value for screen shake upon destruction */ + public float baseShake = 3f; /** bullet that this block spawns when destroyed */ public @Nullable BulletType destroyBullet = null; /** if true, destroyBullet is spawned on the block's team instead of Derelict team */ @@ -194,6 +198,8 @@ public class Block extends UnlockableContent implements Senseable{ public int sizeOffset = 0; /** Clipping size of this block. Should be as large as the block will draw. */ public float clipSize = -1f; + /** Clipping size for lights only. */ + public float lightClipSize; /** When placeRangeCheck is enabled, this is the range checked for enemy blocks. */ public float placeOverlapRange = 50f; /** Multiplier of damage dealt to this block by tanks. Does not apply to crawlers. */ @@ -290,13 +296,13 @@ public class Block extends UnlockableContent implements Senseable{ public Sound breakSound = Sounds.breaks; /** Sounds made when this block is destroyed.*/ public Sound destroySound = Sounds.boom; + /** Range of destroy sound. */ + public float destroyPitchMin = 1f, destroyPitchMax = 1f; /** How reflective this block is. */ public float albedo = 0f; /** Environmental passive light color. */ public Color lightColor = Color.white.cpy(); - /** - * Whether this environmental block passively emits light. - * Does not change behavior for non-environmental blocks, but still updates clipSize. */ + /** If true, drawLight() will be called for this block. */ public boolean emitLight = false; /** Radius of the light emitted by this block. */ public float lightRadius = 60f; @@ -304,11 +310,6 @@ public class Block extends UnlockableContent implements Senseable{ /** How much fog this block uncovers, in tiles. Cannot be dynamic. <= 0 to disable. */ public int fogRadius = -1; - /** The sound that this block makes while active. One sound loop. Do not overuse. */ - public Sound loopSound = Sounds.none; - /** Active sound base volume. */ - public float loopSoundVolume = 0.5f; - /** The sound that this block makes while idle. Uses one sound loop for all blocks. */ public Sound ambientSound = Sounds.none; /** Idle sound base volume. */ @@ -344,6 +345,8 @@ public class Block extends UnlockableContent implements Senseable{ public ObjectFloatMap researchCostMultipliers = new ObjectFloatMap<>(); /** Override for research cost. Uses multipliers above and building requirements if not set. */ public @Nullable ItemStack[] researchCost; + /** If set, all blocks will be forced to be this team. */ + public @Nullable Team forceTeam; /** Whether this block has instant transfer.*/ public boolean instantTransfer = false; /** Whether you can rotate this block after it is placed. */ @@ -434,6 +437,18 @@ public class Block extends UnlockableContent implements Senseable{ drawOverlay(x * tilesize + offset, y * tilesize + offset, rotation); } + /** Draws a region to overlay a specific side of this block. This method makes sure it is placed at the edge of the side. */ + public void drawSideRegion(TextureRegion region, float x, float y, int rotation){ + var p = Geometry.d4[Mathf.mod(rotation, 4)]; + float s = size * tilesize/2f; + + Draw.rect(region, + x + p.x * (s - region.width/2f * region.scl()), + y + p.y * (s - region.width/2f * region.scl()), + rotation * 90f + ); + } + public void drawPotentialLinks(int x, int y){ if((consumesPower || outputsPower) && hasPower && connectedPower){ Tile tile = world.tile(x, y); @@ -492,6 +507,10 @@ public class Block extends UnlockableContent implements Senseable{ public void drawOverlay(float x, float y, int rotation){ } + public boolean displayShadow(Tile tile){ + return hasShadow; + } + public float sumAttribute(@Nullable Attribute attr, int x, int y){ if(attr == null) return 0; Tile tile = world.tile(x, y); @@ -887,8 +906,8 @@ public class Block extends UnlockableContent implements Senseable{ return buildType.get(); } - public void updateClipRadius(float size){ - clipSize = Math.max(clipSize, size * tilesize + size * 2f); + public void updateClipRadius(float radiusInWorldUnits){ + clipSize = Math.max(clipSize, this.size * tilesize + radiusInWorldUnits * 2f); } public Rect bounds(int x, int y, Rect rect){ @@ -1196,6 +1215,10 @@ public class Block extends UnlockableContent implements Senseable{ hasShadow = false; } + if(underBullets){ + priority = TargetPriority.under; + } + if(fogRadius > 0){ flags = flags.with(BlockFlag.hasFogRadius); } @@ -1222,12 +1245,15 @@ public class Block extends UnlockableContent implements Senseable{ clipSize = Math.max(clipSize, size * tilesize); + lightClipSize = Math.max(lightClipSize, clipSize); + if(hasLiquids && drawLiquidLight){ - clipSize = Math.max(size * 30f * 2f, clipSize); + emitLight = true; + lightClipSize = Math.max(lightClipSize, size * 30f * 2f); } if(emitLight){ - clipSize = Math.max(clipSize, lightRadius * 2f); + lightClipSize = Math.max(lightClipSize, lightRadius * 2f); } if(group == BlockGroup.transportation || category == Category.distribution){ diff --git a/core/src/mindustry/world/Build.java b/core/src/mindustry/world/Build.java index fe48fd74b7..8340229257 100644 --- a/core/src/mindustry/world/Build.java +++ b/core/src/mindustry/world/Build.java @@ -164,7 +164,12 @@ public class Build{ /** @return whether a tile can be placed at this location by this team. */ public static boolean validPlace(Block type, Team team, int x, int y, int rotation, boolean checkVisible){ - return validPlaceIgnoreUnits(type, team, x, y, rotation, checkVisible) && checkNoUnitOverlap(type, x, y); + return validPlace(type, team, x, y, rotation, checkVisible, true); + } + + /** @return whether a tile can be placed at this location by this team. */ + public static boolean validPlace(Block type, Team team, int x, int y, int rotation, boolean checkVisible, boolean ignoreCoreRadius){ + return validPlaceIgnoreUnits(type, team, x, y, rotation, checkVisible, ignoreCoreRadius) && checkNoUnitOverlap(type, x, y); } /** @return whether a tile can be placed at this location by this team. */ @@ -172,14 +177,14 @@ public class Build{ return (!type.solid && !type.solidifes) || !Units.anyEntities(x * tilesize + type.offset - type.size * tilesize / 2f, y * tilesize + type.offset - type.size * tilesize / 2f, type.size * tilesize, type.size * tilesize); } - /** Returns whether a tile can be placed at this location by this team. Ignores units at this location. */ - public static boolean validPlaceIgnoreUnits(Block type, Team team, int x, int y, int rotation, boolean checkVisible){ + /** @return whether a tile can be placed at this location by this team. Ignores units at this location. */ + public static boolean validPlaceIgnoreUnits(Block type, Team team, int x, int y, int rotation, boolean checkVisible, boolean checkCoreRadius){ //the wave team can build whatever they want as long as it's visible - banned blocks are not applicable if(type == null || (!state.rules.editor && (checkVisible && (!type.environmentBuildable() || (!type.isPlaceable() && !(state.rules.waves && team == state.rules.waveTeam && type.isVisible())))))){ return false; } - if(!state.rules.editor){ + if(!state.rules.editor && checkCoreRadius){ //find closest core, if it doesn't match the team, placing is not legal if(state.rules.polygonCoreProtection){ float mindst = Float.MAX_VALUE; @@ -240,8 +245,9 @@ public class Build{ (type == check.block() && check.build != null && rotation == check.build.rotation && type.rotate && !((type == check.block && team != Team.derelict && check.team() == Team.derelict))) || //same block, same rotation !check.interactable(team) || //cannot interact !check.floor().placeableOn && !type.ignoreBuildDarkness || //solid floor - (!checkVisible && !check.block().alwaysReplace) || //replacing a block that should be replaced (e.g. payload placement) - !(((type.canReplace(check.block()) || (type == check.block && team != Team.derelict && state.rules.derelictRepair && check.team() == Team.derelict)) || //can replace type OR can replace derelict block of same type + //when you have a payload, you cannot place blocks on things, even if normal placement rules allow it. this is a hack that assumes checkVisible = true means it's coming from a payload + (!checkVisible && checkCoreRadius && !check.block().alwaysReplace) || //replacing a block that should be replaced (e.g. payload placement) + !(((type.canReplace(check.block()) || (check.build != null && check.build.canBeReplaced(type)) || (type == check.block && team != Team.derelict && state.rules.derelictRepair && check.team() == Team.derelict)) || //can replace type OR can replace derelict block of same type (check.build instanceof ConstructBuild build && build.current == type && check.centerX() == tile.x && check.centerY() == tile.y)) && //same type in construction type.bounds(tile.x, tile.y, Tmp.r1).grow(0.01f).contains(check.block.bounds(check.centerX(), check.centerY(), Tmp.r2))) || //no replacement (type.requiresWater && check.floor().liquidDrop != Liquids.water) //requires water but none found @@ -249,7 +255,7 @@ public class Build{ } } - if(state.rules.placeRangeCheck && !state.isEditor() && getEnemyOverlap(type, team, x, y) != null){ + if(state.rules.placeRangeCheck && checkCoreRadius && !state.isEditor() && getEnemyOverlap(type, team, x, y) != null){ return false; } diff --git a/core/src/mindustry/world/CachedTile.java b/core/src/mindustry/world/CachedTile.java index e1d58f83ad..2a5d31aabb 100644 --- a/core/src/mindustry/world/CachedTile.java +++ b/core/src/mindustry/world/CachedTile.java @@ -28,11 +28,11 @@ public class CachedTile extends Tile{ if(block.hasBuilding()){ Building n = entityprov.get(); - n.tile(this); + n.tile = this; n.block = block; if(block.hasItems) n.items = new ItemModule(); - if(block.hasLiquids) n.liquids(new LiquidModule()); - if(block.hasPower) n.power(new PowerModule()); + if(block.hasLiquids) n.liquids = new LiquidModule(); + if(block.hasPower) n.power = new PowerModule(); build = n; } } diff --git a/core/src/mindustry/world/Edges.java b/core/src/mindustry/world/Edges.java index 44b5a48175..7a7ad0dd0b 100644 --- a/core/src/mindustry/world/Edges.java +++ b/core/src/mindustry/world/Edges.java @@ -50,7 +50,7 @@ public class Edges{ } public static Tile getFacingEdge(Building tile, Building other){ - Tile res = getFacingEdge(tile.block, tile.tileX(), tile.tileY(), other.tile()); + Tile res = getFacingEdge(tile.block, tile.tileX(), tile.tileY(), other.tile); return res == null ? tile.tile : res; } diff --git a/core/src/mindustry/world/Tile.java b/core/src/mindustry/world/Tile.java index 45fad8a3e0..af6a0b105f 100644 --- a/core/src/mindustry/world/Tile.java +++ b/core/src/mindustry/world/Tile.java @@ -23,6 +23,7 @@ import static mindustry.Vars.*; public class Tile implements Position, QuadTreeObject, Displayable{ private static final TileChangeEvent tileChange = new TileChangeEvent(); private static final TilePreChangeEvent preChange = new TilePreChangeEvent(); + private static final TileFloorChangeEvent floorChange = new TileFloorChangeEvent(); private static final ObjectSet tileSet = new ObjectSet<>(); /** Extra data for very specific blocks. */ @@ -223,6 +224,8 @@ public class Tile implements Position, QuadTreeObject, Displayable{ recacheWall(); } + if(type.forceTeam != null) team = type.forceTeam; + preChanged(); this.block = type; @@ -282,6 +285,7 @@ public class Tile implements Position, QuadTreeObject, Displayable{ /** This resets the overlay! */ public void setFloor(Floor type){ + var prev = this.floor; this.floor = type; this.overlay = (Floor)Blocks.air; @@ -293,9 +297,13 @@ public class Tile implements Position, QuadTreeObject, Displayable{ if(build != null){ build.onProximityUpdate(); } - if(!world.isGenerating() && pathfinder != null){ + if(!world.isGenerating() && pathfinder != null && !state.isEditor()){ pathfinder.updateTile(this); } + + if(!world.isGenerating() && prev != type){ + Events.fire(floorChange.set(this, prev, type)); + } } public boolean isEditorTile(){ diff --git a/core/src/mindustry/world/Tiles.java b/core/src/mindustry/world/Tiles.java index 51bc22568b..b81603da00 100644 --- a/core/src/mindustry/world/Tiles.java +++ b/core/src/mindustry/world/Tiles.java @@ -40,7 +40,6 @@ public class Tiles implements Iterable{ fires[pos] = f; } - public void each(Intc2 cons){ for(int x = 0; x < width; x++){ for(int y = 0; y < height; y++){ diff --git a/core/src/mindustry/world/blocks/campaign/LandingPad.java b/core/src/mindustry/world/blocks/campaign/LandingPad.java index 65202fd0cd..dbff30f05e 100644 --- a/core/src/mindustry/world/blocks/campaign/LandingPad.java +++ b/core/src/mindustry/world/blocks/campaign/LandingPad.java @@ -342,7 +342,6 @@ public class LandingPad extends Block{ @Override public void drawSelect(){ if(config != null){ - drawItemSelection(config); float dx = x - size * tilesize/2f, dy = y + size * tilesize/2f, s = iconSmall / 4f; Draw.mixcol(Color.darkGray, 1f); diff --git a/core/src/mindustry/world/blocks/defense/Door.java b/core/src/mindustry/world/blocks/defense/Door.java index 7a48b6bb09..e35f0f9c36 100644 --- a/core/src/mindustry/world/blocks/defense/Door.java +++ b/core/src/mindustry/world/blocks/defense/Door.java @@ -52,7 +52,7 @@ public class Door extends Wall{ if(chainEffect) entity.effect(); entity.open = open; - if(!world.isGenerating()) pathfinder.updateTile(entity.tile()); + if(!world.isGenerating()) pathfinder.updateTile(entity.tile); } }); } diff --git a/core/src/mindustry/world/blocks/defense/MendProjector.java b/core/src/mindustry/world/blocks/defense/MendProjector.java index ff4696bbf4..34a69c5f18 100644 --- a/core/src/mindustry/world/blocks/defense/MendProjector.java +++ b/core/src/mindustry/world/blocks/defense/MendProjector.java @@ -3,6 +3,7 @@ package mindustry.world.blocks.defense; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; +import arc.struct.*; import arc.util.*; import arc.util.io.*; import mindustry.annotations.Annotations.*; @@ -39,6 +40,7 @@ public class MendProjector extends Block{ lightRadius = 50f; suppressable = true; envEnabled |= Env.space; + flags = EnumSet.of(BlockFlag.blockRepair); } @Override diff --git a/core/src/mindustry/world/blocks/defense/OverdriveProjector.java b/core/src/mindustry/world/blocks/defense/OverdriveProjector.java index d5261b0604..9e957d0eee 100644 --- a/core/src/mindustry/world/blocks/defense/OverdriveProjector.java +++ b/core/src/mindustry/world/blocks/defense/OverdriveProjector.java @@ -19,9 +19,6 @@ import mindustry.world.meta.*; import static mindustry.Vars.*; public class OverdriveProjector extends Block{ - @Deprecated - public final int timerUse = timers++; - public @Load("@-top") TextureRegion topRegion; public float reload = 60f; public float range = 80f; diff --git a/core/src/mindustry/world/blocks/defense/RegenProjector.java b/core/src/mindustry/world/blocks/defense/RegenProjector.java index 5d05295de8..f136560f38 100644 --- a/core/src/mindustry/world/blocks/defense/RegenProjector.java +++ b/core/src/mindustry/world/blocks/defense/RegenProjector.java @@ -46,6 +46,7 @@ public class RegenProjector extends Block{ suppressable = true; envEnabled |= Env.space; rotateDraw = false; + flags = EnumSet.of(BlockFlag.blockRepair); } @Override @@ -133,8 +134,6 @@ public class RegenProjector extends Block{ return; } - anyTargets = targets.contains(b -> b.damaged()); - if(efficiency > 0){ if((optionalTimer += Time.delta * optionalEfficiency) >= optionalUseTime){ consume(); @@ -148,6 +147,7 @@ public class RegenProjector extends Block{ if(!build.damaged() || build.isHealSuppressed()) continue; didRegen = true; + anyTargets = true; int pos = build.pos(); //TODO periodic effect diff --git a/core/src/mindustry/world/blocks/defense/turrets/Turret.java b/core/src/mindustry/world/blocks/defense/turrets/Turret.java index b1ec009b60..5b1251dc82 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/Turret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/Turret.java @@ -10,6 +10,7 @@ import arc.math.geom.*; import arc.struct.*; import arc.util.*; import arc.util.io.*; +import mindustry.audio.*; import mindustry.content.*; import mindustry.core.*; import mindustry.entities.*; @@ -118,6 +119,10 @@ public class Turret extends ReloadTurret{ public Sound shootSound = Sounds.shoot; /** Sound emitted when shoot.firstShotDelay is >0 and shooting begins. */ public Sound chargeSound = Sounds.none; + /** The sound that this block makes while active. One sound loop. Do not overuse. */ + public Sound loopSound = Sounds.none; + /** Active sound base volume. */ + public float loopSoundVolume = 0.5f; /** Range for pitch of shoot sound. */ public float soundPitchMin = 0.9f, soundPitchMax = 1.1f; /** Backwards Y offset of ammo eject effect. */ @@ -254,8 +259,26 @@ public class Turret extends ReloadTurret{ public float heatReq; public float[] sideHeat = new float[4]; + public @Nullable SoundLoop soundLoop = (loopSound == Sounds.none ? null : new SoundLoop(loopSound, loopSoundVolume)); + float lastRangeChange; + @Override + public void remove(){ + super.remove(); + if(soundLoop != null){ + soundLoop.stop(); + } + } + + @Override + public void onDestroyed(){ + super.onDestroyed(); + if(soundLoop != null){ + soundLoop.stop(); + } + } + @Override public float estimateDps(){ if(!hasAmmo()) return 0f; @@ -410,6 +433,10 @@ public class Turret extends ReloadTurret{ public void updateTile(){ if(!validateTarget()) target = null; + if(soundLoop != null){ + soundLoop.update(x, y, shouldActiveSound(), activeSoundVolume()); + } + float warmupTarget = (isShooting() && canConsume()) || charging() ? 1f : 0f; if(warmupTarget > 0 && !isControlled()){ warmupHold = 1f; @@ -705,12 +732,10 @@ public class Turret extends ReloadTurret{ } - @Override public float activeSoundVolume(){ return shootWarmup; } - @Override public boolean shouldActiveSound(){ return shootWarmup > 0.01f && loopSound != Sounds.none; } diff --git a/core/src/mindustry/world/blocks/distribution/ArmoredConveyor.java b/core/src/mindustry/world/blocks/distribution/ArmoredConveyor.java index 8c5d2f6e23..103e335ff0 100644 --- a/core/src/mindustry/world/blocks/distribution/ArmoredConveyor.java +++ b/core/src/mindustry/world/blocks/distribution/ArmoredConveyor.java @@ -29,7 +29,7 @@ public class ArmoredConveyor extends Conveyor{ public class ArmoredConveyorBuild extends ConveyorBuild{ @Override public boolean acceptItem(Building source, Item item){ - return super.acceptItem(source, item) && (source.block instanceof Conveyor || Edges.getFacingEdge(source.tile(), tile).relativeTo(tile) == rotation); + return super.acceptItem(source, item) && (source.block instanceof Conveyor || Edges.getFacingEdge(source.tile, tile).relativeTo(tile) == rotation); } } } diff --git a/core/src/mindustry/world/blocks/distribution/Duct.java b/core/src/mindustry/world/blocks/distribution/Duct.java index 14f29fd9de..e6c0998972 100644 --- a/core/src/mindustry/world/blocks/distribution/Duct.java +++ b/core/src/mindustry/world/blocks/distribution/Duct.java @@ -1,6 +1,7 @@ package mindustry.world.blocks.distribution; import arc.*; +import arc.func.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; @@ -30,7 +31,7 @@ public class Duct extends Block implements Autotiler{ public @Load(value = "@-top-#", length = 5) TextureRegion[] topRegions; public @Load(value = "@-bottom-#", length = 5, fallback = "duct-bottom-#") TextureRegion[] botRegions; - public @Nullable Block bridgeReplacement; + public @Nullable Block bridgeReplacement, junctionReplacement; public Duct(String name){ super(name); @@ -63,6 +64,7 @@ public class Duct extends Block implements Autotiler{ super.init(); if(bridgeReplacement == null || !(bridgeReplacement instanceof DuctBridge || bridgeReplacement instanceof ItemBridge)) bridgeReplacement = Blocks.ductBridge; + //if(junctionReplacement == null) junctionReplacement = Blocks.ductJunction; } @Override @@ -103,11 +105,24 @@ public class Duct extends Block implements Autotiler{ return new TextureRegion[]{Core.atlas.find("duct-bottom"), topRegions[0]}; } + @Override + public Block getReplacement(BuildPlan req, Seq plans){ + if(junctionReplacement == null || !junctionReplacement.unlockedNow() || junctionReplacement.isHidden()) return this; + + Boolf cont = p -> plans.contains(o -> o.x == req.x + p.x && o.y == req.y + p.y && (req.block instanceof Duct || req.block instanceof DuctJunction)); + return cont.get(Geometry.d4(req.rotation)) && + cont.get(Geometry.d4(req.rotation - 2)) && + req.tile() != null && + req.tile().block() instanceof Duct && + Mathf.mod(req.tile().build.rotation - req.rotation, 2) == 1 ? junctionReplacement : this; + } + @Override public void handlePlacementLine(Seq plans){ if(bridgeReplacement == null) return; - if(bridgeReplacement instanceof ItemBridge bridge) Placement.calculateBridges(plans, bridge, false, b -> b instanceof Duct || b instanceof StackConveyor || b instanceof Conveyor); - if(bridgeReplacement instanceof DuctBridge bridge) Placement.calculateBridges(plans, bridge, false, b -> b instanceof Duct || b instanceof StackConveyor || b instanceof Conveyor); + boolean hasJunction = junctionReplacement != null && junctionReplacement.unlockedNow() && !junctionReplacement.isHidden(); + if(bridgeReplacement instanceof ItemBridge bridge) Placement.calculateBridges(plans, bridge, hasJunction, b -> b instanceof Duct || b instanceof StackConveyor || b instanceof Conveyor); + if(bridgeReplacement instanceof DuctBridge bridge) Placement.calculateBridges(plans, bridge, hasJunction, b -> b instanceof Duct || b instanceof StackConveyor || b instanceof Conveyor); } public class DuctBuild extends Building{ @@ -137,7 +152,7 @@ public class Duct extends Block implements Autotiler{ Draw.z(Layer.blockUnder + 0.1f); Tmp.v1.set(Geometry.d4x(recDir) * tilesize / 2f, Geometry.d4y(recDir) * tilesize / 2f) .lerp(Geometry.d4x(r) * tilesize / 2f, Geometry.d4y(r) * tilesize / 2f, - Mathf.clamp((progress + 1f) / 2f)); + Mathf.clamp((progress + 1f) / (2f - 1f/speed))); Draw.rect(current.fullIcon, x + Tmp.v1.x, y + Tmp.v1.y, itemSize, itemSize); } @@ -188,7 +203,7 @@ public class Duct extends Block implements Autotiler{ (armored ? //armored acceptance ((source.block.rotate && source.front() == this && source.block.hasItems && source.block.isDuct) || - Edges.getFacingEdge(source.tile(), tile).relativeTo(tile) == rotation) : + Edges.getFacingEdge(source.tile, tile).relativeTo(tile) == rotation) : //standard acceptance - do not accept from front !(source.block.rotate && next == source) && Edges.getFacingEdge(source.tile, tile) != null && Math.abs(Edges.getFacingEdge(source.tile, tile).relativeTo(tile.x, tile.y) - rotation) != 2 ); diff --git a/core/src/mindustry/world/blocks/distribution/DuctJunction.java b/core/src/mindustry/world/blocks/distribution/DuctJunction.java new file mode 100644 index 0000000000..d461926e5a --- /dev/null +++ b/core/src/mindustry/world/blocks/distribution/DuctJunction.java @@ -0,0 +1,179 @@ +package mindustry.world.blocks.distribution; + +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.util.io.*; +import mindustry.annotations.Annotations.*; +import mindustry.entities.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.io.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.meta.*; + +import static mindustry.Vars.*; + +public class DuctJunction extends Block{ + public Color transparentColor = new Color(0.4f, 0.4f, 0.4f, 0.1f); + public @Load("@-bottom") TextureRegion bottomRegion; + public @Load("@-top") TextureRegion topRegion; + public float speed = 5f; + + public DuctJunction(String name){ + super(name); + update = true; + solid = false; + underBullets = true; + group = BlockGroup.transportation; + unloadable = false; + floating = true; + noUpdateDisabled = true; + hasItems = true; + + priority = TargetPriority.transport; + envEnabled = Env.space | Env.terrestrial | Env.underwater; + } + + @Override + public void setStats(){ + super.setStats(); + //4 tems is misleading + stats.remove(Stat.itemCapacity); + } + + @Override + public void load(){ + super.load(); + squareSprite = true; + } + + @Override + public boolean outputsItems(){ + return true; + } + + @Override + public void init(){ + itemCapacity = 4; + super.init(); + } + + public class DuctJunctionBuild extends Building{ + Item[] itemdata = new Item[4]; + float[] times = new float[4]; + + @Override + public void draw(){ + Draw.z(Layer.blockUnder); + Draw.rect(bottomRegion, x, y); + + Draw.z(Layer.blockUnder + 0.1f); + + for(int i = 0; i < 4; i++){ + Item current = itemdata[i]; + + if(current != null){ + float progress = (Mathf.clamp((times[i] + 1f) / (2f - 1f/speed)) - 0.5f) * 2f; + + Draw.rect(current.fullIcon, + x + Geometry.d4x(i) * tilesize / 2f * progress, + y + Geometry.d4y(i) * tilesize / 2f * progress, + itemSize, itemSize + ); + } + } + + Draw.color(transparentColor); + Draw.rect(bottomRegion, x, y); + Draw.color(); + + Draw.z(Layer.blockUnder + 0.2f); + + Draw.rect(topRegion, x, y); + } + + @Override + public void updateTile(){ + float inc = edelta() / speed * 2f; + + for(int i = 0; i < 4; i++){ + Item item = itemdata[i]; + if(item != null){ + + times[i] += inc; + if(times[i] >= (1f - 1f/speed)){ + Building next = nearby(i); + + if(next != null && next.team == team && next.acceptItem(this, item)){ + next.handleItem(this, item); + itemdata[i] = null; + items.remove(item, 1); + times[i] %= (1f - 1f/speed); + } + } + }else{ + //TODO: reset progress or not? + times[i] = 0f; + } + } + } + + @Override + public void handleItem(Building source, Item item){ + int relative = source.relativeTo(tile); + if(relative == -1) return; + itemdata[relative] = item; + times[relative] = -1f; + items.add(item, 1); + } + + @Override + public boolean acceptItem(Building source, Item item){ + int relative = source.relativeTo(tile); + + if(relative == -1 || itemdata[relative] != null) return false; + Building to = nearby(relative); + return to != null && to.team == team; + } + + @Override + public int acceptStack(Item item, int amount, Teamc source){ + return 0; + } + + @Override + public int removeStack(Item item, int amount){ + int removed = 0; + for(int i = 0; i < 4 && amount > 0; i++){ + if(itemdata[i] == item){ + amount --; + removed ++; + itemdata[i] = null; + items.remove(item, 1); + } + } + return removed; + } + + @Override + public void write(Writes write){ + super.write(write); + for(int i = 0; i < 4; i++){ + write.f(times[i]); + TypeIO.writeItem(write, itemdata[i]); + } + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + for(int i = 0; i < 4; i++){ + times[i] = read.f(); + itemdata[i] = TypeIO.readItem(read); + } + } + } +} diff --git a/core/src/mindustry/world/blocks/distribution/DuctRouter.java b/core/src/mindustry/world/blocks/distribution/DuctRouter.java index 5ba1ff7c21..97bef1ebe9 100644 --- a/core/src/mindustry/world/blocks/distribution/DuctRouter.java +++ b/core/src/mindustry/world/blocks/distribution/DuctRouter.java @@ -147,7 +147,7 @@ public class DuctRouter extends Block{ @Override public boolean acceptItem(Building source, Item item){ return current == null && items.total() == 0 && - (Edges.getFacingEdge(source.tile(), tile).relativeTo(tile) == rotation); + (Edges.getFacingEdge(source.tile, tile).relativeTo(tile) == rotation); } @Override diff --git a/core/src/mindustry/world/blocks/distribution/MassDriver.java b/core/src/mindustry/world/blocks/distribution/MassDriver.java index 7fc0d5f0ec..4aef759357 100644 --- a/core/src/mindustry/world/blocks/distribution/MassDriver.java +++ b/core/src/mindustry/world/blocks/distribution/MassDriver.java @@ -248,7 +248,7 @@ public class MassDriver extends Block{ if(linkValid()){ Building target = world.build(link); - Drawf.circles(target.x, target.y, (target.block().size / 2f + 1) * tilesize + sin - 2f, Pal.place); + Drawf.circles(target.x, target.y, (target.block.size / 2f + 1) * tilesize + sin - 2f, Pal.place); Drawf.arrow(x, y, target.x, target.y, size * tilesize + sin, 4f + sin); } diff --git a/core/src/mindustry/world/blocks/distribution/OverflowDuct.java b/core/src/mindustry/world/blocks/distribution/OverflowDuct.java index d7099268b2..c804f5e48f 100644 --- a/core/src/mindustry/world/blocks/distribution/OverflowDuct.java +++ b/core/src/mindustry/world/blocks/distribution/OverflowDuct.java @@ -132,7 +132,7 @@ public class OverflowDuct extends Block{ @Override public boolean acceptItem(Building source, Item item){ return current == null && items.total() == 0 && - (Edges.getFacingEdge(source.tile(), tile).relativeTo(tile) == rotation); + (Edges.getFacingEdge(source.tile, tile).relativeTo(tile) == rotation); } @Override diff --git a/core/src/mindustry/world/blocks/distribution/Router.java b/core/src/mindustry/world/blocks/distribution/Router.java index 1e4a9399aa..cbe6d64d52 100644 --- a/core/src/mindustry/world/blocks/distribution/Router.java +++ b/core/src/mindustry/world/blocks/distribution/Router.java @@ -83,7 +83,7 @@ public class Router extends Block{ items.add(item, 1); lastItem = item; time = 0f; - lastInput = source.tile(); + lastInput = source.tile; } @Override diff --git a/core/src/mindustry/world/blocks/distribution/StackRouter.java b/core/src/mindustry/world/blocks/distribution/StackRouter.java index 1186401c9c..7f176d6d4c 100644 --- a/core/src/mindustry/world/blocks/distribution/StackRouter.java +++ b/core/src/mindustry/world/blocks/distribution/StackRouter.java @@ -85,7 +85,7 @@ public class StackRouter extends DuctRouter{ @Override public boolean acceptItem(Building source, Item item){ return !unloading && (current == null || item == current) && items.total() < itemCapacity && - (Edges.getFacingEdge(source.tile(), tile).relativeTo(tile) == rotation); + (Edges.getFacingEdge(source.tile, tile).relativeTo(tile) == rotation); } } } diff --git a/core/src/mindustry/world/blocks/environment/Floor.java b/core/src/mindustry/world/blocks/environment/Floor.java index 2ea499d015..79d31cd648 100644 --- a/core/src/mindustry/world/blocks/environment/Floor.java +++ b/core/src/mindustry/world/blocks/environment/Floor.java @@ -213,6 +213,11 @@ public class Floor extends Block{ return new TextureRegion[]{Core.atlas.find(Core.atlas.has(name) ? name : name + "1")}; } + /** @return whether to index this floor by flag */ + public boolean shouldIndex(Tile tile){ + return true; + } + //TODO currently broken for dynamically edited floor tiles /** @return true if this floor should be updated in the render loop, e.g. for effects. Do NOT overuse this! */ public boolean updateRender(Tile tile){ @@ -319,11 +324,6 @@ public class Floor extends Block{ return edges(x, y)[rx][2 - ry]; } - @Deprecated - protected TextureRegion[][] edges(){ - return edges(0, 0); - } - /** @return whether the edges from {@param other} should be drawn onto this tile **/ protected boolean doEdge(Tile tile, Tile otherTile, Floor other){ return (other.realBlendId(otherTile) > realBlendId(tile) || edges(tile.x, tile.y) == null); diff --git a/core/src/mindustry/world/blocks/environment/SteamVent.java b/core/src/mindustry/world/blocks/environment/SteamVent.java index ef2414e1cf..a41efba7a0 100644 --- a/core/src/mindustry/world/blocks/environment/SteamVent.java +++ b/core/src/mindustry/world/blocks/environment/SteamVent.java @@ -4,12 +4,14 @@ import arc.graphics.*; import arc.graphics.g2d.*; import arc.math.*; import arc.math.geom.*; +import arc.struct.*; import arc.util.*; import mindustry.*; import mindustry.content.*; import mindustry.entities.*; import mindustry.graphics.*; import mindustry.world.*; +import mindustry.world.meta.*; import static mindustry.Vars.*; @@ -41,6 +43,7 @@ public class SteamVent extends Floor{ public SteamVent(String name){ super(name); variants = 2; + flags = EnumSet.of(BlockFlag.steamVent); } @Override @@ -58,6 +61,16 @@ public class SteamVent extends Floor{ return checkAdjacent(tile); } + @Override + public boolean shouldIndex(Tile tile){ + return isCenterVent(tile); + } + + public boolean isCenterVent(Tile tile){ + Tile topRight = tile.nearby(1, 1); + return topRight != null && topRight.floor() == tile.floor() && checkAdjacent(topRight); + } + @Override public void renderUpdate(UpdateRenderState state){ if(state.tile.nearby(-1, -1) != null && state.tile.nearby(-1, -1).block() == Blocks.air && (state.data += Time.delta) >= effectSpacing){ @@ -66,6 +79,7 @@ public class SteamVent extends Floor{ } } + //note that only the top right tile works for this; render order reasons. public boolean checkAdjacent(Tile tile){ for(var point : offsets){ Tile other = Vars.world.tile(tile.x + point.x, tile.y + point.y); diff --git a/core/src/mindustry/world/blocks/heat/HeatProducer.java b/core/src/mindustry/world/blocks/heat/HeatProducer.java index b2bcd8ab2d..e07cc083a4 100644 --- a/core/src/mindustry/world/blocks/heat/HeatProducer.java +++ b/core/src/mindustry/world/blocks/heat/HeatProducer.java @@ -1,6 +1,7 @@ package mindustry.world.blocks.heat; import arc.math.*; +import arc.struct.*; import arc.util.io.*; import mindustry.graphics.*; import mindustry.ui.*; @@ -20,6 +21,8 @@ public class HeatProducer extends GenericCrafter{ rotate = true; canOverdrive = false; drawArrow = true; + //it doesn't count as a standard crafter + flags = EnumSet.of(); } @Override diff --git a/core/src/mindustry/world/blocks/logic/MessageBlock.java b/core/src/mindustry/world/blocks/logic/MessageBlock.java index 33178c151e..f00b7f2ce7 100644 --- a/core/src/mindustry/world/blocks/logic/MessageBlock.java +++ b/core/src/mindustry/world/blocks/logic/MessageBlock.java @@ -88,7 +88,7 @@ public class MessageBlock extends Block{ Draw.color(0f, 0f, 0f, 0.2f); Fill.rect(x, y - tilesize/2f - l.height/2f - offset, l.width + offset*2f, l.height + offset*2f); Draw.color(); - font.setColor(Color.white); + font.setColor(message.length() == 0 ? Color.lightGray : Color.white); font.draw(text, x - l.width/2f, y - tilesize/2f - offset, 90f, Align.left, true); font.setUseIntegerPositions(ints); diff --git a/core/src/mindustry/world/blocks/payloads/BuildPayload.java b/core/src/mindustry/world/blocks/payloads/BuildPayload.java index f192625818..bdbbecaf7a 100644 --- a/core/src/mindustry/world/blocks/payloads/BuildPayload.java +++ b/core/src/mindustry/world/blocks/payloads/BuildPayload.java @@ -84,7 +84,7 @@ public class BuildPayload implements Payload{ @Override public void remove(){ - build.stopSound(); + build.remove(); } @Override diff --git a/core/src/mindustry/world/blocks/payloads/PayloadBlock.java b/core/src/mindustry/world/blocks/payloads/PayloadBlock.java index 0ff62bbcb1..491a8077dc 100644 --- a/core/src/mindustry/world/blocks/payloads/PayloadBlock.java +++ b/core/src/mindustry/world/blocks/payloads/PayloadBlock.java @@ -207,7 +207,7 @@ public class PayloadBlock extends Block{ payVector.approach(dest, payloadSpeed * delta()); Building front = front(); - boolean canDump = front == null || !front.tile().solid(); + boolean canDump = front == null || !front.tile.solid(); boolean canMove = front != null && (front.block.outputsPayload || front.block.acceptsPayload); if(canDump && !canMove){ diff --git a/core/src/mindustry/world/blocks/payloads/PayloadLoader.java b/core/src/mindustry/world/blocks/payloads/PayloadLoader.java index 9d9328597e..3ec56f4a81 100644 --- a/core/src/mindustry/world/blocks/payloads/PayloadLoader.java +++ b/core/src/mindustry/world/blocks/payloads/PayloadLoader.java @@ -94,7 +94,7 @@ public class PayloadLoader extends PayloadBlock{ //item container (build.build.block.hasItems && build.block().unloadable && build.block().itemCapacity >= 10 && build.block().size <= maxBlockSize) || //liquid container - (build.build.block().hasLiquids && build.block().liquidCapacity >= 10f) || + (build.build.block.hasLiquids && build.block().liquidCapacity >= 10f) || //battery (build.build.block.consPower != null && build.build.block.consPower.buffered) ); diff --git a/core/src/mindustry/world/blocks/payloads/PayloadMassDriver.java b/core/src/mindustry/world/blocks/payloads/PayloadMassDriver.java index 0e129a7b39..5595352ab9 100644 --- a/core/src/mindustry/world/blocks/payloads/PayloadMassDriver.java +++ b/core/src/mindustry/world/blocks/payloads/PayloadMassDriver.java @@ -22,7 +22,7 @@ import static mindustry.world.blocks.payloads.PayloadMassDriver.PayloadDriverSta public class PayloadMassDriver extends PayloadBlock{ public float range = 100f; - public float rotateSpeed = 2f; + public float rotateSpeed = 5f; public float length = 89 / 8f; public float knockback = 5f; public float reload = 30f; @@ -72,7 +72,7 @@ public class PayloadMassDriver extends PayloadBlock{ @Override public void init(){ super.init(); - updateClipRadius(range); + updateClipRadius(range + 4f); } @Override @@ -430,7 +430,7 @@ public class PayloadMassDriver extends PayloadBlock{ if(linkValid()){ Building target = world.build(link); - Drawf.circles(target.x, target.y, (target.block().size / 2f + 1) * tilesize + sin - 2f, Pal.place); + Drawf.circles(target.x, target.y, (target.block.size / 2f + 1) * tilesize + sin - 2f, Pal.place); Drawf.arrow(x, y, target.x, target.y, size * tilesize + sin, 4f + sin); } diff --git a/core/src/mindustry/world/blocks/payloads/UnitPayload.java b/core/src/mindustry/world/blocks/payloads/UnitPayload.java index a9c57941ce..cffdcaafc6 100644 --- a/core/src/mindustry/world/blocks/payloads/UnitPayload.java +++ b/core/src/mindustry/world/blocks/payloads/UnitPayload.java @@ -149,6 +149,7 @@ public class UnitPayload implements Payload{ float e = unit.elevation; unit.elevation = 0f; + Draw.scl(1f, 1f); unit.type.draw(unit); unit.elevation = e; diff --git a/core/src/mindustry/world/blocks/power/Battery.java b/core/src/mindustry/world/blocks/power/Battery.java index ce3867666b..651ac7ec56 100644 --- a/core/src/mindustry/world/blocks/power/Battery.java +++ b/core/src/mindustry/world/blocks/power/Battery.java @@ -5,7 +5,6 @@ import arc.graphics.g2d.*; import arc.math.*; import arc.struct.*; import arc.util.*; -import mindustry.annotations.Annotations.*; import mindustry.entities.units.*; import mindustry.gen.*; import mindustry.world.draw.*; @@ -17,9 +16,6 @@ public class Battery extends PowerDistributor{ public Color emptyLightColor = Color.valueOf("f8c266"); public Color fullLightColor = Color.valueOf("fb9567"); - @Deprecated - public @Load("@-top") TextureRegion topRegion; - public Battery(String name){ super(name); outputsPower = true; diff --git a/core/src/mindustry/world/blocks/power/LightBlock.java b/core/src/mindustry/world/blocks/power/LightBlock.java index 8beccc9f76..529ba33ac0 100644 --- a/core/src/mindustry/world/blocks/power/LightBlock.java +++ b/core/src/mindustry/world/blocks/power/LightBlock.java @@ -39,7 +39,7 @@ public class LightBlock extends Block{ @Override public void init(){ lightRadius = radius*2.5f; - clipSize = Math.max(clipSize, lightRadius * 3f); + lightClipSize = Math.max(lightClipSize, lightRadius * 3f); emitLight = true; super.init(); diff --git a/core/src/mindustry/world/blocks/power/NuclearReactor.java b/core/src/mindustry/world/blocks/power/NuclearReactor.java index 8e28d5b331..580848c1e0 100644 --- a/core/src/mindustry/world/blocks/power/NuclearReactor.java +++ b/core/src/mindustry/world/blocks/power/NuclearReactor.java @@ -49,6 +49,7 @@ public class NuclearReactor extends PowerGenerator{ hasItems = true; hasLiquids = true; rebuildable = false; + emitLight = true; flags = EnumSet.of(BlockFlag.reactor, BlockFlag.generator); schematicPriority = -5; envEnabled = Env.any; diff --git a/core/src/mindustry/world/blocks/power/PowerNode.java b/core/src/mindustry/world/blocks/power/PowerNode.java index 89ddae8cbc..20fe08dc75 100644 --- a/core/src/mindustry/world/blocks/power/PowerNode.java +++ b/core/src/mindustry/world/blocks/power/PowerNode.java @@ -194,7 +194,7 @@ public class PowerNode extends PowerBlock{ } protected boolean overlaps(Building src, Building other, float range){ - return overlaps(src.x, src.y, other.tile(), range); + return overlaps(src.x, src.y, other.tile, range); } protected boolean overlaps(Tile src, Tile other, float range){ @@ -209,9 +209,9 @@ public class PowerNode extends PowerBlock{ protected void getPotentialLinks(Tile tile, Team team, Cons others){ if(!autolink) return; - Boolf valid = other -> other != null && other.tile() != tile && other.block.connectedPower && other.power != null && + Boolf valid = other -> other != null && other.tile != tile && other.block.connectedPower && other.power != null && (other.block.outputsPower || other.block.consumesPower || other.block instanceof PowerNode) && - overlaps(tile.x * tilesize + offset, tile.y * tilesize + offset, other.tile(), laserRange * tilesize) && other.team == team && + overlaps(tile.x * tilesize + offset, tile.y * tilesize + offset, other.tile, laserRange * tilesize) && other.team == team && !graphs.contains(other.power.graph) && !PowerNode.insulated(tile, other.tile) && !(other instanceof PowerNodeBuild obuild && obuild.power.links.size >= ((PowerNode)obuild.block).maxNodes) && @@ -264,7 +264,7 @@ public class PowerNode extends PowerBlock{ //TODO code duplication w/ method above? /** Iterates through linked nodes of a block at a tile. All returned buildings are power nodes. */ public static void getNodeLinks(Tile tile, Block block, Team team, Cons others){ - Boolf valid = other -> other != null && other.tile() != tile && other.block instanceof PowerNode node && + Boolf valid = other -> other != null && other.tile != tile && other.block instanceof PowerNode node && node.autolink && other.power.links.size < node.maxNodes && node.overlaps(other.x, other.y, tile, block, node.laserRange * tilesize) && other.team == team diff --git a/core/src/mindustry/world/blocks/power/ThermalGenerator.java b/core/src/mindustry/world/blocks/power/ThermalGenerator.java index 1af92ce5da..85bf3c424d 100644 --- a/core/src/mindustry/world/blocks/power/ThermalGenerator.java +++ b/core/src/mindustry/world/blocks/power/ThermalGenerator.java @@ -37,9 +37,10 @@ public class ThermalGenerator extends PowerGenerator{ outputsLiquid = true; hasLiquids = true; } + emitLight = true; super.init(); //proper light clipping - clipSize = Math.max(clipSize, 45f * size * 2f * 2f); + lightClipSize = Math.max(lightClipSize, 45f * size * 2f * 2f); } @Override diff --git a/core/src/mindustry/world/blocks/storage/CoreBlock.java b/core/src/mindustry/world/blocks/storage/CoreBlock.java index c7d7871c57..c9411718a5 100644 --- a/core/src/mindustry/world/blocks/storage/CoreBlock.java +++ b/core/src/mindustry/world/blocks/storage/CoreBlock.java @@ -43,7 +43,7 @@ public class CoreBlock extends StorageBlock{ public @Load(value = "@-thruster1", fallback = "clear-effect") TextureRegion thruster1; //top right public @Load(value = "@-thruster2", fallback = "clear-effect") TextureRegion thruster2; //bot left - public float thrusterLength = 14f/4f; + public float thrusterLength = 14f/4f, thrusterOffset = 0f; public boolean isFirstTier; /** If true, this core type requires a core zone to upgrade. */ public boolean requiresCoreZone; @@ -69,8 +69,6 @@ public class CoreBlock extends StorageBlock{ priority = TargetPriority.core; flags = EnumSet.of(BlockFlag.core); unitCapModifier = 10; - loopSound = Sounds.respawning; - loopSoundVolume = 1f; drawDisabled = false; canOverdrive = false; envEnabled |= Env.space; @@ -92,6 +90,10 @@ public class CoreBlock extends StorageBlock{ if(!net.client()){ Unit unit = spawnType.create(tile.team()); + //reset reload so that the player can't shoot immediately + for(var mount : unit.mounts){ + mount.reload = mount.weapon.reload; + } unit.set(core); unit.rotation(90f); unit.impulse(0f, 3f); @@ -649,24 +651,23 @@ public class CoreBlock extends StorageBlock{ super.onProximityUpdate(); for(Building other : state.teams.cores(team)){ - if(other.tile() != tile){ + if(other.tile != tile){ this.items = other.items; } } state.teams.registerCore(this); - storageCapacity = itemCapacity + proximity().sum(e -> owns(e) ? e.block.itemCapacity : 0); + storageCapacity = itemCapacity + proximity.sum(e -> owns(e) ? e.block.itemCapacity : 0); proximity.each(this::owns, t -> { t.items = items; ((StorageBuild)t).linkedCore = this; }); for(Building other : state.teams.cores(team)){ - if(other.tile() == tile) continue; - storageCapacity += other.block.itemCapacity + other.proximity().sum(e -> owns(other, e) ? e.block.itemCapacity : 0); + if(other.tile == tile) continue; + storageCapacity += other.block.itemCapacity + other.proximity.sum(e -> owns(other, e) ? e.block.itemCapacity : 0); } - //Team.sharded.core().items.set(Items.surgeAlloy, 12000) if(!world.isGenerating()){ for(Item item : content.items()){ items.set(item, Math.min(items.get(item), storageCapacity)); diff --git a/core/src/mindustry/world/blocks/units/Reconstructor.java b/core/src/mindustry/world/blocks/units/Reconstructor.java index 8c243d878c..c87a05d828 100644 --- a/core/src/mindustry/world/blocks/units/Reconstructor.java +++ b/core/src/mindustry/world/blocks/units/Reconstructor.java @@ -142,15 +142,12 @@ public class Reconstructor extends UnitBlock{ public @Nullable Vec2 commandPos; public @Nullable UnitCommand command; + boolean constructing; + public float fraction(){ return progress / constructTime; } - @Override - public boolean shouldActiveSound(){ - return shouldConsume(); - } - @Override public Vec2 getCommandPosition(){ return commandPos; @@ -290,6 +287,8 @@ public class Reconstructor extends UnitBlock{ @Override public void updateTile(){ + //cache value to prevent repeated calls and multithreading issues + constructing = constructing(); boolean valid = false; if(payload != null){ @@ -338,7 +337,7 @@ public class Reconstructor extends UnitBlock{ @Override public boolean shouldConsume(){ - return constructing() && enabled; + return constructing && enabled; } @Override diff --git a/core/src/mindustry/world/blocks/units/RepairTurret.java b/core/src/mindustry/world/blocks/units/RepairTurret.java index 86b29c79a4..7dd8b27a2e 100644 --- a/core/src/mindustry/world/blocks/units/RepairTurret.java +++ b/core/src/mindustry/world/blocks/units/RepairTurret.java @@ -85,7 +85,7 @@ public class RepairTurret extends Block{ } consumePowerCond(powerUse, (RepairPointBuild entity) -> entity.target != null); - updateClipRadius(repairRadius); + updateClipRadius(repairRadius + tilesize); super.init(); } diff --git a/core/src/mindustry/world/blocks/units/UnitAssembler.java b/core/src/mindustry/world/blocks/units/UnitAssembler.java index 89dd833402..271ac5d744 100644 --- a/core/src/mindustry/world/blocks/units/UnitAssembler.java +++ b/core/src/mindustry/world/blocks/units/UnitAssembler.java @@ -142,7 +142,7 @@ public class UnitAssembler extends PayloadBlock{ @Override public void init(){ - updateClipRadius(areaSize * tilesize); + updateClipRadius((areaSize + 1) * tilesize); consume(consPayload = new ConsumePayloadDynamic((UnitAssemblerBuild build) -> build.plan().requirements)); consume(consItem = new ConsumeItemDynamic((UnitAssemblerBuild build) -> build.plan().itemReq != null ? build.plan().itemReq : ItemStack.empty)); diff --git a/core/src/mindustry/world/blocks/units/UnitAssemblerModule.java b/core/src/mindustry/world/blocks/units/UnitAssemblerModule.java index 79cd4ddc9e..3565f34fdf 100644 --- a/core/src/mindustry/world/blocks/units/UnitAssemblerModule.java +++ b/core/src/mindustry/world/blocks/units/UnitAssemblerModule.java @@ -53,13 +53,13 @@ public class UnitAssemblerModule extends PayloadBlock{ @Override public void drawPlanRegion(BuildPlan plan, Eachable list){ Draw.rect(region, plan.drawx(), plan.drawy()); - Draw.rect(plan.rotation >= 2 ? sideRegion2 : sideRegion1, plan.drawx(), plan.drawy(), plan.rotation * 90); + drawSideRegion(plan.rotation >= 2 ? sideRegion2 : sideRegion1, plan.drawx(), plan.drawy(), plan.rotation); Draw.rect(topRegion, plan.drawx(), plan.drawy()); } @Override public TextureRegion[] icons(){ - return new TextureRegion[]{region, sideRegion1, topRegion}; + return new TextureRegion[]{region, topRegion}; } public @Nullable UnitAssemblerBuild getLink(Team team, int x, int y, int rotation){ @@ -93,11 +93,11 @@ public class UnitAssemblerModule extends PayloadBlock{ //draw input conveyors for(int i = 0; i < 4; i++){ if(blends(i) && i != rotation){ - Draw.rect(inRegion, x, y, (i * 90) - 180); + drawSideRegion(inRegion, x, y, i - 2); } } - Draw.rect(rotation >= 2 ? sideRegion2 : sideRegion1, x, y, rotdeg()); + drawSideRegion(rotation >= 2 ? sideRegion2 : sideRegion1, x, y, rotation); Draw.z(Layer.blockOver); payRotation = rotdeg(); diff --git a/core/src/mindustry/world/blocks/units/UnitCargoLoader.java b/core/src/mindustry/world/blocks/units/UnitCargoLoader.java index 5a7b1223d1..234cee635a 100644 --- a/core/src/mindustry/world/blocks/units/UnitCargoLoader.java +++ b/core/src/mindustry/world/blocks/units/UnitCargoLoader.java @@ -142,11 +142,6 @@ public class UnitCargoLoader extends Block{ return unit == null; } - @Override - public boolean shouldActiveSound(){ - return shouldConsume() && warmup > 0.01f; - } - @Override public void draw(){ Draw.rect(block.region, x, y); diff --git a/core/src/mindustry/world/blocks/units/UnitFactory.java b/core/src/mindustry/world/blocks/units/UnitFactory.java index 10ee5a3a13..71353e1899 100644 --- a/core/src/mindustry/world/blocks/units/UnitFactory.java +++ b/core/src/mindustry/world/blocks/units/UnitFactory.java @@ -236,11 +236,6 @@ public class UnitFactory extends UnitBlock{ return super.senseObject(sensor); } - @Override - public boolean shouldActiveSound(){ - return shouldConsume(); - } - @Override public double sense(LAccess sensor){ if(sensor == LAccess.progress) return Mathf.clamp(fraction()); diff --git a/core/src/mindustry/world/draw/DrawFlame.java b/core/src/mindustry/world/draw/DrawFlame.java index 60af38b497..b8da7b9675 100644 --- a/core/src/mindustry/world/draw/DrawFlame.java +++ b/core/src/mindustry/world/draw/DrawFlame.java @@ -25,8 +25,9 @@ public class DrawFlame extends DrawBlock{ @Override public void load(Block block){ + block.emitLight = true; top = Core.atlas.find(block.name + "-top"); - block.clipSize = Math.max(block.clipSize, (lightRadius + lightSinMag) * 2f * block.size); + block.lightClipSize = Math.max(block.lightClipSize, (lightRadius + lightSinMag) * 2f * block.size); } @Override diff --git a/core/src/mindustry/world/draw/DrawPlasma.java b/core/src/mindustry/world/draw/DrawPlasma.java index 7cb5c81a73..398d384fdc 100644 --- a/core/src/mindustry/world/draw/DrawPlasma.java +++ b/core/src/mindustry/world/draw/DrawPlasma.java @@ -18,6 +18,8 @@ public class DrawPlasma extends DrawFlame{ @Override public void load(Block block){ + block.emitLight = true; + regions = new TextureRegion[plasmas]; for(int i = 0; i < regions.length; i++){ regions[i] = Core.atlas.find(block.name + suffix + i); diff --git a/core/src/mindustry/world/meta/BlockFlag.java b/core/src/mindustry/world/meta/BlockFlag.java index 82d073a780..9f6cd57a33 100644 --- a/core/src/mindustry/world/meta/BlockFlag.java +++ b/core/src/mindustry/world/meta/BlockFlag.java @@ -29,7 +29,9 @@ public enum BlockFlag{ launchPad, unitCargoUnloadPoint, unitAssembler, - hasFogRadius; + hasFogRadius, + steamVent, + blockRepair; public final static BlockFlag[] all = values(); diff --git a/core/src/mindustry/world/meta/StatValues.java b/core/src/mindustry/world/meta/StatValues.java index 5720179103..00ac191ac9 100644 --- a/core/src/mindustry/world/meta/StatValues.java +++ b/core/src/mindustry/world/meta/StatValues.java @@ -581,18 +581,14 @@ public class StatValues{ int count = 0; for(Ability ability : abilities){ if(ability.display){ - t.table(Styles.grayPanel, a -> { - a.add("[accent]" + ability.localized()).padBottom(4).center().top().expandX(); - a.row(); - a.left().top().defaults().left(); - ability.addStats(a); - }).pad(5).margin(10).growX().top().uniformX(); + ability.display(t); + if((++count) == 2){ count = 0; t.row(); } } - }; + } }); }; } diff --git a/desktop/build.gradle b/desktop/build.gradle index b4d9955e70..b3f192bc75 100644 --- a/desktop/build.gradle +++ b/desktop/build.gradle @@ -45,6 +45,7 @@ task dist(type: Jar, dependsOn: configurations.runtimeClasspath){ from files(sourceSets.main.output.resourcesDir) from {configurations.runtimeClasspath.collect{ it.isDirectory() ? it : zipTree(it) }} from files(project.assetsDir) + exclude("config/**") duplicatesStrategy = DuplicatesStrategy.EXCLUDE //don't include steam shared libraries unless necessary diff --git a/desktop/src/mindustry/desktop/DesktopLauncher.java b/desktop/src/mindustry/desktop/DesktopLauncher.java index 2500d18e59..e0c2158c1c 100644 --- a/desktop/src/mindustry/desktop/DesktopLauncher.java +++ b/desktop/src/mindustry/desktop/DesktopLauncher.java @@ -53,6 +53,7 @@ public class DesktopLauncher extends ClientLauncher{ case "height": height = Integer.parseInt(arg[i + 1]); break; case "gl3": gl30 = true; break; case "gl2": gl30 = false; break; + case "coreGl": coreProfile = true; break; case "antialias": samples = 16; break; case "debug": Log.level = LogLevel.debug; break; case "maximized": maximized = Boolean.parseBoolean(arg[i + 1]); break; diff --git a/ios/build.gradle b/ios/build.gradle index d0ec38f0a9..931168d947 100644 --- a/ios/build.gradle +++ b/ios/build.gradle @@ -4,7 +4,7 @@ buildscript{ } dependencies{ - classpath "com.mobidevelop.robovm:robovm-gradle-plugin:2.3.19" + classpath "com.mobidevelop.robovm:robovm-gradle-plugin:2.3.20" } } diff --git a/server/build.gradle b/server/build.gradle index f348e41dbf..45b887b1a7 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -41,6 +41,7 @@ task dist(type: Jar, dependsOn: configurations.runtimeClasspath){ exclude("fonts/**") exclude("bundles/**") exclude("cubemaps/**") + exclude("config/**") exclude("cursors/**") exclude("shaders/**") exclude("icons/icon.icns") diff --git a/server/src/mindustry/server/ServerControl.java b/server/src/mindustry/server/ServerControl.java index 6716c1165b..e3e538cfba 100644 --- a/server/src/mindustry/server/ServerControl.java +++ b/server/src/mindustry/server/ServerControl.java @@ -1094,15 +1094,6 @@ public class ServerControl implements ApplicationListener{ } } - /** - * @deprecated - * Use {@link Maps#setNextMapOverride(Map)} instead. - */ - @Deprecated - public void setNextMap(Map map){ - maps.setNextMapOverride(map); - } - /** * Cancels the world load timer task, if it is scheduled. Can be useful for stopping a server or hosting a new game. */ diff --git a/tests/src/test/java/power/ConsumeGeneratorTests.java b/tests/src/test/java/power/ConsumeGeneratorTests.java index e1d6f058c2..5882e45746 100644 --- a/tests/src/test/java/power/ConsumeGeneratorTests.java +++ b/tests/src/test/java/power/ConsumeGeneratorTests.java @@ -98,7 +98,7 @@ public class ConsumeGeneratorTests extends PowerTestFixture{ //reset Time.setDeltaProvider(() -> 0.5f); - assertEquals(expectedEfficiency, build.efficiency(), inputType + " | " + parameterDescription + ": Base input efficiency mismatch."); + assertEquals(expectedEfficiency, build.efficiency, inputType + " | " + parameterDescription + ": Base input efficiency mismatch."); assertEquals(expectedRemainingLiquidAmount, build.liquids.get(liquid), inputType + " | " + parameterDescription + ": Remaining liquid amount mismatch."); assertEquals(expectedOutputEfficiency, build.productionEfficiency, inputType + " | " + parameterDescription + ": Output production efficiency mismatch."); } diff --git a/tools/src/mindustry/tools/Generators.java b/tools/src/mindustry/tools/Generators.java index 108784fe47..5fc22be5fa 100644 --- a/tools/src/mindustry/tools/Generators.java +++ b/tools/src/mindustry/tools/Generators.java @@ -11,6 +11,7 @@ import arc.struct.*; import arc.util.*; import arc.util.noise.*; import mindustry.ctype.*; +import mindustry.entities.part.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; @@ -508,7 +509,7 @@ public class Generators{ }); generate("unit-icons", () -> content.units().each(type -> { - if(type.internal) return; //internal hidden units don't generate + if(type.internal && !type.internalGenerateSprites) return; //internal hidden units don't generate ObjectSet outlined = new ObjectSet<>(); @@ -530,6 +531,28 @@ public class Generators{ save(pix, ((GenRegion)region).name + "-outline"); } + Seq allParts = new Seq<>(); + + //this code is complete trash + Cons>[] allDrawIter = new Cons[]{null}; + allDrawIter[0] = seq -> { + for(DrawPart part : seq){ + allParts.add(part); + if(part instanceof RegionPart){ + allDrawIter[0].get(((RegionPart)part).children); + } + } + }; + allDrawIter[0].get(type.parts); + + for(DrawPart part : allParts){ + if(part instanceof RegionPart && ((RegionPart)part).replaceOutline){ + for(TextureRegion r : ((RegionPart)part).regions){ + outliner.get(r); + } + } + } + Seq weapons = type.weapons; weapons.each(Weapon::load); weapons.removeAll(w -> !w.region.found()); @@ -580,7 +603,8 @@ public class Generators{ if(sample instanceof Legsc) outliner.get(type.legRegion); if(sample instanceof Tankc) outliner.get(type.treadRegion); - Pixmap image = type.segments > 0 ? get(type.segmentRegions[0]) : outline.get(get(type.previewRegion)); + //TODO: for drawBody false, an empty pixmap is used; this is a hack + Pixmap image = type.segments > 0 ? get(type.segmentRegions[0]) : type.drawBody ? outline.get(get(type.previewRegion)) : new Pixmap(1, 1); Func weaponRegion = weapon -> Core.atlas.has(weapon.name + "-preview") ? get(weapon.name + "-preview") : get(weapon.region); Cons2 drawWeapon = (weapon, pixmap) -> @@ -608,7 +632,7 @@ public class Generators{ //outline is currently never needed, although it could theoretically be necessary if(type.needsBodyOutline()){ save(image, type.name + "-outline"); - }else if(type.segments == 0){ + }else if(type.segments == 0 && type.drawBody){ replace(type.name, type.segments > 0 ? get(type.segmentRegions[0]) : outline.get(get(type.region))); } @@ -676,7 +700,9 @@ public class Generators{ } //TODO I can save a LOT of space by not creating a full icon. - save(image, "unit-" + type.name + "-full"); + if(type.generateFullIcon){ + save(image, "unit-" + type.name + "-full"); + } Rand rand = new Rand(); rand.setSeed(type.name.hashCode()); diff --git a/tools/src/mindustry/tools/ImagePacker.java b/tools/src/mindustry/tools/ImagePacker.java index a319555abe..7018acf998 100644 --- a/tools/src/mindustry/tools/ImagePacker.java +++ b/tools/src/mindustry/tools/ImagePacker.java @@ -16,6 +16,7 @@ import mindustry.content.*; import mindustry.core.*; import mindustry.ctype.*; import mindustry.logic.*; +import mindustry.type.*; import mindustry.world.blocks.*; import java.io.*; @@ -109,7 +110,7 @@ public class ImagePacker{ map.each((key, val) -> content2id.put(val.split("\\|")[0], key)); Seq cont = Seq.withArrays(Vars.content.blocks(), Vars.content.items(), Vars.content.liquids(), Vars.content.units(), Vars.content.statusEffects()); - cont.removeAll(u -> u instanceof ConstructBlock || u == Blocks.air); + cont.removeAll(u -> u instanceof ConstructBlock || u == Blocks.air || (u instanceof UnitType t && t.internal)); int minid = 0xF8FF; for(String key : map.keys()){