diff --git a/core/assets/maps/coastline.msav b/core/assets/maps/coastline.msav index 66c343f4d6..91e8c1db83 100644 Binary files a/core/assets/maps/coastline.msav and b/core/assets/maps/coastline.msav differ diff --git a/core/assets/maps/navalFortress.msav b/core/assets/maps/navalFortress.msav new file mode 100644 index 0000000000..1be5ea6577 Binary files /dev/null and b/core/assets/maps/navalFortress.msav differ diff --git a/core/src/mindustry/ai/WaveSpawner.java b/core/src/mindustry/ai/WaveSpawner.java index 276b86bb26..82d695fc83 100644 --- a/core/src/mindustry/ai/WaveSpawner.java +++ b/core/src/mindustry/ai/WaveSpawner.java @@ -56,7 +56,7 @@ public class WaveSpawner{ public void spawnEnemies(){ spawning = true; - eachGroundSpawn((spawnX, spawnY, doShockwave) -> { + eachGroundSpawn(-1, (spawnX, spawnY, doShockwave) -> { if(doShockwave){ doShockwave(spawnX, spawnY); } @@ -70,7 +70,7 @@ public class WaveSpawner{ if(group.type.flying){ float spread = margin / 1.5f; - eachFlyerSpawn((spawnX, spawnY) -> { + eachFlyerSpawn(group.spawn, (spawnX, spawnY) -> { for(int i = 0; i < spawned; i++){ Unit unit = group.createUnit(state.rules.waveTeam, state.wave - 1); unit.set(spawnX + Mathf.range(spread), spawnY + Mathf.range(spread)); @@ -80,7 +80,7 @@ public class WaveSpawner{ }else{ float spread = tilesize * 2; - eachGroundSpawn((spawnX, spawnY, doShockwave) -> { + eachGroundSpawn(group.spawn, (spawnX, spawnY, doShockwave) -> { for(int i = 0; i < spawned; i++){ Tmp.v1.rnd(spread); @@ -102,12 +102,14 @@ public class WaveSpawner{ } public void eachGroundSpawn(Intc2 cons){ - eachGroundSpawn((x, y, shock) -> cons.get(World.toTile(x), World.toTile(y))); + eachGroundSpawn(-1, (x, y, shock) -> cons.get(World.toTile(x), World.toTile(y))); } - private void eachGroundSpawn(SpawnConsumer cons){ + private void eachGroundSpawn(int filterPos, SpawnConsumer cons){ if(state.hasSpawns()){ for(Tile spawn : spawns){ + if(filterPos != -1 && filterPos != spawn.pos()) continue; + cons.accept(spawn.worldx(), spawn.worldy(), true); } } @@ -115,6 +117,8 @@ public class WaveSpawner{ if(state.rules.attackMode && state.teams.isActive(state.rules.waveTeam) && !state.teams.playerCores().isEmpty()){ Building firstCore = state.teams.playerCores().first(); for(Building core : state.rules.waveTeam.cores()){ + if(filterPos != -1 && filterPos != core.pos()) continue; + Tmp.v1.set(firstCore).sub(core).limit(coreMargin + core.block.size * tilesize /2f * Mathf.sqrt2); boolean valid = false; @@ -147,8 +151,10 @@ public class WaveSpawner{ } } - private void eachFlyerSpawn(Floatc2 cons){ + private void eachFlyerSpawn(int filterPos, Floatc2 cons){ for(Tile tile : spawns){ + if(filterPos != -1 && filterPos != tile.pos()) continue; + float angle = Angles.angle(world.width() / 2f, world.height() / 2f, tile.x, tile.y); float trns = Math.max(world.width(), world.height()) * Mathf.sqrt2 * tilesize; float spawnX = Mathf.clamp(world.width() * tilesize / 2f + Angles.trnsx(angle, trns), -margin, world.width() * tilesize + margin); @@ -158,6 +164,8 @@ public class WaveSpawner{ if(state.rules.attackMode && state.teams.isActive(state.rules.waveTeam)){ for(Building core : state.rules.waveTeam.data().cores){ + if(filterPos != -1 && filterPos != core.pos()) continue; + cons.get(core.x, core.y); } } @@ -171,7 +179,7 @@ public class WaveSpawner{ public int countFlyerSpawns(){ tmpCount = 0; - eachFlyerSpawn((x, y) -> tmpCount ++); + eachFlyerSpawn(-1, (x, y) -> tmpCount ++); return tmpCount; } diff --git a/core/src/mindustry/content/SectorPresets.java b/core/src/mindustry/content/SectorPresets.java index b1f0796a29..7c86779dc9 100644 --- a/core/src/mindustry/content/SectorPresets.java +++ b/core/src/mindustry/content/SectorPresets.java @@ -70,9 +70,8 @@ public class SectorPresets implements ContentList{ difficulty = 5; }}; - //TODO - if(false) navalFortress = new SectorPreset("navalFortress", serpulo, 216){{ + //TODO AI or not? useAI = true; difficulty = 8; }}; diff --git a/core/src/mindustry/editor/WaveInfoDialog.java b/core/src/mindustry/editor/WaveInfoDialog.java index d6588b3279..10c4751bd7 100644 --- a/core/src/mindustry/editor/WaveInfoDialog.java +++ b/core/src/mindustry/editor/WaveInfoDialog.java @@ -3,6 +3,7 @@ package mindustry.editor; import arc.*; import arc.graphics.*; import arc.math.*; +import arc.math.geom.*; import arc.scene.event.*; import arc.scene.style.*; import arc.scene.ui.*; @@ -316,7 +317,30 @@ public class WaveInfoDialog extends BaseDialog{ t.check("@waves.guardian", b -> { group.effect = (b ? StatusEffects.boss : null); buildGroups(); - }).padTop(4).update(b -> b.setChecked(group.effect == StatusEffects.boss)).padBottom(8f); + }).padTop(4).update(b -> b.setChecked(group.effect == StatusEffects.boss)).padBottom(8f).row(); + + //spawn positions are clunky and thus experimental for now + if(experimental){ + t.table(a -> { + a.add("spawn at "); + + a.field(group.spawn == -1 ? "" : Point2.x(group.spawn) + "", TextFieldFilter.digitsOnly, text -> { + if(Strings.canParsePositiveInt(text)){ + group.spawn = Point2.pack(Strings.parseInt(text), Point2.y(group.spawn)); + Log.info(group.spawn); + } + }).width(70f); + + a.add(","); + + a.field(group.spawn == -1 ? "" : Point2.y(group.spawn) + "", TextFieldFilter.digitsOnly, text -> { + if(Strings.canParsePositiveInt(text)){ + group.spawn = Point2.pack(Point2.x(group.spawn), Strings.parseInt(text)); + Log.info(group.spawn); + } + }).width(70f); + }).padBottom(8f).padTop(-8f).row(); + } } }).width(340f).pad(8); diff --git a/core/src/mindustry/game/SpawnGroup.java b/core/src/mindustry/game/SpawnGroup.java index c814f0a925..79bf5164eb 100644 --- a/core/src/mindustry/game/SpawnGroup.java +++ b/core/src/mindustry/game/SpawnGroup.java @@ -36,10 +36,12 @@ public class SpawnGroup implements JsonSerializable, Cloneable{ public float unitScaling = never; /** Shield points that this unit has. */ public float shields = 0f; - /** How much shields get increased per wave. */ + /** How much shields get increased by per wave. */ public float shieldScaling = 0f; /** Amount of enemies spawned initially, with no scaling */ public int unitAmount = 1; + /** If not -1, the unit will only spawn in spawnpoints with these packed coordinates. */ + public int spawn = -1; /** Seq of payloads that this unit will spawn with. */ public @Nullable Seq payloads; /** Status effect applied to the spawned unit. Null to disable. */ @@ -55,6 +57,10 @@ public class SpawnGroup implements JsonSerializable, Cloneable{ //serialization use only } + public boolean canSpawn(int position){ + return spawn == -1 || spawn == position; + } + /** @return amount of units spawned on a specific wave. */ public int getSpawned(int wave){ if(spacing == 0) spacing = 1; @@ -111,6 +117,7 @@ public class SpawnGroup implements JsonSerializable, Cloneable{ if(shieldScaling != 0) json.writeValue("shieldScaling", shieldScaling); if(unitAmount != 1) json.writeValue("amount", unitAmount); if(effect != null) json.writeValue("effect", effect.name); + if(spawn != -1) json.writeValue("spawn", spawn); if(payloads != null && payloads.size > 0){ json.writeValue("payloads", payloads.map(u -> u.name).toArray(String.class)); } @@ -130,6 +137,7 @@ public class SpawnGroup implements JsonSerializable, Cloneable{ shields = data.getFloat("shields", 0); shieldScaling = data.getFloat("shieldScaling", 0); unitAmount = data.getInt("amount", 1); + spawn = data.getInt("spawn", -1); if(data.has("payloads")){ payloads = Seq.with(json.readValue(String[].class, data.get("payloads"))).map(s -> content.getByName(ContentType.unit, s)); } diff --git a/core/src/mindustry/maps/SectorDamage.java b/core/src/mindustry/maps/SectorDamage.java index 2b1c9995e3..9fd2f0d247 100644 --- a/core/src/mindustry/maps/SectorDamage.java +++ b/core/src/mindustry/maps/SectorDamage.java @@ -321,14 +321,20 @@ public class SectorDamage{ var reg = new LinearRegression(); SpawnGroup bossGroup = null; Seq waveDps = new Seq<>(), waveHealth = new Seq<>(); + int groundSpawns = Math.max(spawner.countFlyerSpawns(), 1), airSpawns = Math.max(spawner.countGroundSpawns(), 1); for(int wave = state.wave; wave < state.wave + 10; wave ++){ float sumWaveDps = 0f, sumWaveHealth = 0f; for(SpawnGroup group : state.rules.spawns){ + //calculate the amount of spawn points used + //if there's a spawn position override, there is only one potential place they spawn + //assume that all overridden positions are valid, should always be true in properly designed campaign maps + int spawnCount = group.spawn != -1 ? 1 : group.type.flying ? airSpawns : groundSpawns; + float healthMult = 1f + Mathf.clamp(group.type.armor / 20f); StatusEffect effect = (group.effect == null ? StatusEffects.none : group.effect); - int spawned = group.getSpawned(wave); + int spawned = group.getSpawned(wave) * spawnCount; //save the boss group if(group.effect == StatusEffects.boss){ bossGroup = group; diff --git a/core/src/mindustry/maps/filters/BlendFilter.java b/core/src/mindustry/maps/filters/BlendFilter.java index 7f0d319df9..773459d75a 100644 --- a/core/src/mindustry/maps/filters/BlendFilter.java +++ b/core/src/mindustry/maps/filters/BlendFilter.java @@ -8,7 +8,7 @@ import static mindustry.maps.filters.FilterOption.*; public class BlendFilter extends GenerateFilter{ float radius = 2f; - Block block = Blocks.stone, floor = Blocks.ice, ignore = Blocks.air; + Block block = Blocks.sand, floor = Blocks.sandWater, ignore = Blocks.air; @Override public FilterOption[] options(){ @@ -16,7 +16,7 @@ public class BlendFilter extends GenerateFilter{ new SliderOption("radius", () -> radius, f -> radius = f, 1f, 10f), new BlockOption("block", () -> block, b -> block = b, anyOptional), new BlockOption("floor", () -> floor, b -> floor = b, anyOptional), - new BlockOption("ignore", () -> ignore, b -> ignore = b, floorsOptional) + new BlockOption("ignore", () -> ignore, b -> ignore = b, anyOptional) }; }