diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 39318ed99d..c3c84b0d2b 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -1419,6 +1419,8 @@ rules.alloweditworldprocessors = Allow Editing World Processors rules.alloweditworldprocessors.info = When enabled, world logic blocks can be placed and edited even outside the editor. rules.waves = Waves rules.airUseSpawns = Air Units Use Spawn Points +rules.wavespawnatcores = Waves Spawn At Cores +rules.wavespawnatcores.info = When enabled in attack mode, waves spawn near all enemy cores. rules.attack = Attack Mode rules.buildai = Base Builder AI rules.buildaitier = Builder AI Tier diff --git a/core/src/mindustry/ai/WaveSpawner.java b/core/src/mindustry/ai/WaveSpawner.java index d3e4ccc651..2bc3e421c9 100644 --- a/core/src/mindustry/ai/WaveSpawner.java +++ b/core/src/mindustry/ai/WaveSpawner.java @@ -15,6 +15,7 @@ import mindustry.game.*; import mindustry.gen.*; import mindustry.type.*; import mindustry.world.*; +import mindustry.world.blocks.storage.CoreBlock.*; import static mindustry.Vars.*; @@ -125,39 +126,46 @@ public class WaveSpawner{ } } - if(state.rules.attackMode && state.teams.isActive(state.rules.waveTeam) && !state.teams.playerCores().isEmpty()){ + if(state.rules.wavesSpawnAtCores && 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()){ + for(CoreBuild 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); + if(core.commandPos != null){ + cons.accept(core.commandPos.x, core.commandPos.y, false); + }else{ + boolean valid = false; - boolean valid = false; - int steps = 0; + Tmp.v1.set(firstCore).sub(core).limit(coreMargin + core.block.size * tilesize /2f * Mathf.sqrt2); - //keep moving forward until the max step amount is reached - while(steps++ < maxSteps){ - int tx = World.toTile(core.x + Tmp.v1.x), ty = World.toTile(core.y + Tmp.v1.y); - any = false; - Geometry.circle(tx, ty, world.width(), world.height(), 3, (x, y) -> { - if(world.solid(x, y)){ - any = true; + int steps = 0; + + //keep moving forward until the max step amount is reached + while(steps++ < maxSteps){ + int tx = World.toTile(core.x + Tmp.v1.x), ty = World.toTile(core.y + Tmp.v1.y); + any = false; + Geometry.circle(tx, ty, world.width(), world.height(), 3, (x, y) -> { + if(world.solid(x, y)){ + any = true; + } + }); + + //nothing is in the way, spawn it + if(!any){ + valid = true; + break; + }else{ + //make the vector longer + Tmp.v1.setLength(Tmp.v1.len() + tilesize*1.1f); } - }); + } - //nothing is in the way, spawn it - if(!any){ - valid = true; - break; - }else{ - //make the vector longer - Tmp.v1.setLength(Tmp.v1.len() + tilesize*1.1f); + if(valid){ + cons.accept(core.x + Tmp.v1.x, core.y + Tmp.v1.y, false); } } - if(valid){ - cons.accept(core.x + Tmp.v1.x, core.y + Tmp.v1.y, false); - } + } } } diff --git a/core/src/mindustry/editor/MapEditorDialog.java b/core/src/mindustry/editor/MapEditorDialog.java index 93b679be7c..6af7f4f280 100644 --- a/core/src/mindustry/editor/MapEditorDialog.java +++ b/core/src/mindustry/editor/MapEditorDialog.java @@ -176,10 +176,67 @@ public class MapEditorDialog extends Dialog implements Disposable{ menu.hide(); sectorGenDialog.show(); }).padTop(!steam ? -3 : 1).size(swidth * 2f + 10, 60f); - menu.cont.row(); menu.cont.row(); + //this is gated behind a property, because it's (1) not useful to most people, (2) confusing and (3) may crash or otherwise bug out + if(OS.hasProp("mindustry.editor.simulate.button")){ + + menu.cont.button("Simulate", Icon.logic, () -> { + menu.hide(); + + BaseDialog dialog = new BaseDialog("Simulate"); + + int[] seconds = {60 * 1}; + + dialog.cont.add("Seconds: "); + dialog.cont.field(seconds[0] + "", text -> seconds[0] = Strings.parseInt(text, 1)).valid(s -> Strings.parseInt(s, 9999999) < 10f * 60f); + + dialog.addCloseButton(); + + dialog.buttons.button("@ok", Icon.ok, () -> { + ui.loadAnd(() -> { + + float deltaScl = 2f; + int steps = Mathf.ceil(seconds[0] * 60f / deltaScl); + float oldDelta = Time.delta; + Time.delta = deltaScl; + + Seq builds = new Seq<>(); + Time.clear(); + + world.tiles.eachTile(t -> { + if(t.build != null && t.isCenter() && t.block().update && t.build.allowUpdate()){ + builds.add(t.build); + t.build.updateProximity(); + } + }); + + for(int i = 0; i < steps; i++){ + Time.update(); + for(var build : builds){ + build.update(); + } + Groups.powerGraph.update(); + } + + //spawned units will cause havoc, so clear them + Groups.unit.clear(); + + Time.clear(); + Time.delta = oldDelta; + }); + + dialog.hide(); + }).size(210f, 64f); + + dialog.show(); + + }).size(swidth * 2f + 10, 60f); + + menu.cont.row(); + } + menu.cont.button("@quit", Icon.exit, () -> { tryExit(); menu.hide(); diff --git a/core/src/mindustry/entities/comp/BuildingComp.java b/core/src/mindustry/entities/comp/BuildingComp.java index 9c44162eab..6616233163 100644 --- a/core/src/mindustry/entities/comp/BuildingComp.java +++ b/core/src/mindustry/entities/comp/BuildingComp.java @@ -1315,6 +1315,10 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, } } + public boolean isCommandable(){ + return block.commandable; + } + /** @return whether this building is in a payload */ public boolean isPayload(){ return tile == emptyTile; diff --git a/core/src/mindustry/game/Rules.java b/core/src/mindustry/game/Rules.java index fc367c1ad9..890b69a7e2 100644 --- a/core/src/mindustry/game/Rules.java +++ b/core/src/mindustry/game/Rules.java @@ -34,6 +34,8 @@ public class Rules{ public boolean waves; /** Whether air units spawn at spawns instead of the edge of the map */ public boolean airUseSpawns = false; + /** If true, units spawn at enemy cores in attack maps with waves enabled. */ + public boolean wavesSpawnAtCores = true; /** Whether the game objective is PvP. Note that this enables automatic hosting. */ public boolean pvp; /** Whether is waiting for players enabled in PvP. */ diff --git a/core/src/mindustry/input/DesktopInput.java b/core/src/mindustry/input/DesktopInput.java index ed819e876c..f0a41dc339 100644 --- a/core/src/mindustry/input/DesktopInput.java +++ b/core/src/mindustry/input/DesktopInput.java @@ -326,7 +326,7 @@ public class DesktopInput extends InputHandler{ selectedUnits.clear(); commandBuildings.clear(); for(var build : player.team().data().buildings){ - if(build.block.commandable){ + if(build.isCommandable()){ commandBuildings.add(build); } } diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index 52918dabfb..05424e8a1f 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -155,6 +155,8 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ Events.on(ResetEvent.class, e -> { logicCutscene = false; + commandBuildings.clear(); + selectedUnits.clear(); itemDepositCooldown = 0f; Arrays.fill(controlGroups, null); lastUnit = null; @@ -413,7 +415,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ for(int pos : buildings){ var build = world.build(pos); - if(build == null || build.team() != player.team() || !build.block.commandable) continue; + if(build == null || build.team() != player.team() || !build.isCommandable()) continue; build.onCommand(target); build.updateLastAccess(player); @@ -839,7 +841,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ itemDepositCooldown -= Time.delta / 60f; - commandBuildings.removeAll(b -> !b.isValid()); + commandBuildings.removeAll(b -> !b.isValid() || !b.isCommandable() || b.team != player.team()); if(!commandMode){ commandRect = false; @@ -1025,7 +1027,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ //deselect selectedUnits.clear(); - if(build != null && build.team == player.team() && build.block.commandable){ + if(build != null && build.team == player.team() && build.isCommandable()){ if(commandBuildings.contains(build)){ commandBuildings.remove(build); }else{ @@ -1742,7 +1744,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ boolean consumed = false, showedInventory = false; //select building for commanding - if(build.block.commandable && commandMode){ + if(build.isCommandable() && commandMode){ //TODO handled in tap. consumed = true; }else if(build.block.configurable && build.interactable(player.team())){ //check if tapped block is configurable diff --git a/core/src/mindustry/ui/dialogs/CustomRulesDialog.java b/core/src/mindustry/ui/dialogs/CustomRulesDialog.java index a4a8879abc..64563389ae 100644 --- a/core/src/mindustry/ui/dialogs/CustomRulesDialog.java +++ b/core/src/mindustry/ui/dialogs/CustomRulesDialog.java @@ -151,6 +151,7 @@ public class CustomRulesDialog extends BaseDialog{ check("@rules.wavetimer", b -> rules.waveTimer = b, () -> rules.waveTimer, () -> rules.waves); check("@rules.waitForWaveToEnd", b -> rules.waitEnemies = b, () -> rules.waitEnemies, () -> rules.waves && rules.waveTimer); check("@rules.randomwaveai", b -> rules.randomWaveAI = b, () -> rules.randomWaveAI, () -> rules.waves); + check("@rules.wavespawnatcores", b -> rules.wavesSpawnAtCores = b, () -> rules.wavesSpawnAtCores, () -> rules.waves); check("@rules.airUseSpawns", b -> rules.airUseSpawns = b, () -> rules.airUseSpawns, () -> rules.waves); numberi("@rules.wavelimit", f -> rules.winWave = f, () -> rules.winWave, () -> rules.waves, 0, Integer.MAX_VALUE); number("@rules.wavespacing", false, f -> rules.waveSpacing = f * 60f, () -> rules.waveSpacing / 60f, () -> rules.waves && rules.waveTimer, 1, Float.MAX_VALUE); diff --git a/core/src/mindustry/world/blocks/storage/CoreBlock.java b/core/src/mindustry/world/blocks/storage/CoreBlock.java index 5bfab32686..6c901628dd 100644 --- a/core/src/mindustry/world/blocks/storage/CoreBlock.java +++ b/core/src/mindustry/world/blocks/storage/CoreBlock.java @@ -13,6 +13,7 @@ import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; +import arc.util.io.*; import mindustry.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; @@ -23,6 +24,7 @@ import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.gen.*; import mindustry.graphics.*; +import mindustry.io.*; import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; @@ -71,6 +73,7 @@ public class CoreBlock extends StorageBlock{ unitCapModifier = 10; drawDisabled = false; canOverdrive = false; + commandable = true; envEnabled |= Env.space; //support everything @@ -247,9 +250,25 @@ public class CoreBlock extends StorageBlock{ public Team lastDamage = Team.derelict; public float iframes = -1f; public float thrusterTime = 0f; + public @Nullable Vec2 commandPos; protected float cloudSeed, landParticleTimer; + @Override + public boolean isCommandable(){ + return team != state.rules.defaultTeam && state.rules.attackMode; + } + + @Override + public Vec2 getCommandPosition(){ + return commandPos; + } + + @Override + public void onCommand(Vec2 target){ + commandPos = target; + } + @Override public void draw(){ //draw thrusters when just landed @@ -806,5 +825,24 @@ public class CoreBlock extends StorageBlock{ noEffect = false; } } + + @Override + public byte version(){ + return 1; + } + + @Override + public void write(Writes write){ + super.write(write); + TypeIO.writeVecNullable(write, commandPos); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + if(revision >= 1){ + commandPos = TypeIO.readVecNullable(read); + } + } } }