diff --git a/build.gradle b/build.gradle index e90d91b5cc..90d51c48c7 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ allprojects { appName = 'Mindustry' gdxVersion = '1.9.8' roboVMVersion = '2.3.0' - uCoreVersion = '628ced32dbceefe9096c6acc9639cd39b1a867f4' + uCoreVersion = 'abd096135c0c3da6f42b781851118b8725b1c676' getVersionString = { String buildVersion = getBuildVersion() diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index c7e1a9b6a4..84511dacf1 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -11,7 +11,8 @@ text.link.wiki.description=official Mindustry wiki text.linkfail=Failed to open link!\nThe URL has been copied to your cliboard. text.editor.web=The web version does not support the editor!\nDownload the game to use it. text.web.unsupported=The web version does not support this feature! Download the game to use it. -text.gameover=Your core has been destroyed +text.gameover=Game Over +text.gameover.pvp=The[accent] {0}[] team is victorious! text.sector.gameover=This sector has been lost. Re-deploy? text.sector.retry=Retry text.highscore=[accent]New highscore! @@ -209,6 +210,7 @@ text.saving=[accent]Saving... text.wave=[orange]Wave {0} text.wave.waiting=[LIGHT_GRAY]Wave in {0} text.waiting=[LIGHT_GRAY]Waiting... +text.waiting.players=Waiting for players... text.wave.enemies=[LIGHT_GRAY]{0} Enemies Remaining text.wave.enemy=[LIGHT_GRAY]{0} Enemy Remaining text.loadimage=Load Image @@ -218,7 +220,8 @@ text.custom=Custom text.builtin=Built-In text.map.delete.confirm=Are you sure you want to delete this map? This action cannot be undone! text.map.random=[accent]Random Map -text.map.nospawn=This map does not have any cores for the player to spawn in! Add a [ROYAL]blue[] core to this map in the editor. +text.map.nospawn=This map does not have any cores for the player to spawn in! Add a[ROYAL] blue[] core to this map in the editor. +text.map.nospawn.pvp=This map does not have any enemy cores for player to spawn into! Add[SCARLET] red[] cores to this map in the editor. text.map.invalid=Error loading map: corrupted or invalid map file. text.editor.brush=Brush text.editor.slope=\\ @@ -425,6 +428,8 @@ mode.sandbox.description=infinite resources and no timer for waves. mode.custom.warning=Note that blocks unlocked in custom games are not carried over to sectors.\n\n[LIGHT_GRAY]In sandbox, only blocks unlocked with sector play can be used. mode.freebuild.name=freebuild mode.freebuild.description=limited resources and no timer for waves. +mode.pvp.name=PvP +mode.pvp.description=fight against other players locally. content.item.name=Items content.liquid.name=Liquids @@ -651,6 +656,13 @@ block.rtg-generator.name=RTG Generator block.spectre.name=Spectre block.meltdown.name=Meltdown +team.blue.name=blue +team.red.name=red +team.orange.name=orange +team.none.name=gray +team.green.name=green +team.purple.name=purple + unit.alpha-drone.name=Alpha Drone unit.spirit.name=Spirit Drone unit.spirit.description=The starter drone unit. Spawns in the core by default. Automatically mines ores, collects items and repairs blocks. diff --git a/core/src/io/anuke/mindustry/core/Control.java b/core/src/io/anuke/mindustry/core/Control.java index aac708c169..19f5b7a889 100644 --- a/core/src/io/anuke/mindustry/core/Control.java +++ b/core/src/io/anuke/mindustry/core/Control.java @@ -13,6 +13,7 @@ import io.anuke.mindustry.game.Content; import io.anuke.mindustry.game.EventType.*; import io.anuke.mindustry.game.Saves; import io.anuke.mindustry.game.Unlocks; +import io.anuke.mindustry.gen.Call; import io.anuke.mindustry.input.DefaultKeybinds; import io.anuke.mindustry.input.DesktopInput; import io.anuke.mindustry.input.InputHandler; @@ -27,6 +28,10 @@ import io.anuke.ucore.entities.Entities; import io.anuke.ucore.entities.EntityQuery; import io.anuke.ucore.modules.Module; import io.anuke.ucore.util.Atlas; +import io.anuke.ucore.util.Bundles; +import io.anuke.ucore.util.Strings; + +import java.io.IOException; import static io.anuke.mindustry.Vars.*; @@ -153,11 +158,24 @@ public class Control extends Module{ threads.runGraphics(() -> { Effects.shake(5, 6, Core.camera.position.x, Core.camera.position.y); - ui.restart.show(); - state.set(State.menu); + //the restart dialog can show info for any number of scenarios + Call.onGameOver(event.winner); }); }); + //autohost for pvp sectors + Events.on(WorldLoadEvent.class, event -> { + if(state.mode.isPvp && !Net.active()){ + try{ + Net.host(port); + players[0].isAdmin = true; + }catch(IOException e){ + ui.showError(Bundles.format("text.server.error", Strings.parseException(e, false))); + threads.runDelay(() -> state.set(State.menu)); + } + } + }); + Events.on(WorldLoadEvent.class, event -> threads.runGraphics(() -> Events.fire(new WorldLoadGraphicsEvent()))); Events.on(TileChangeEvent.class, event -> { diff --git a/core/src/io/anuke/mindustry/core/Logic.java b/core/src/io/anuke/mindustry/core/Logic.java index 81b610333e..57d3b88704 100644 --- a/core/src/io/anuke/mindustry/core/Logic.java +++ b/core/src/io/anuke/mindustry/core/Logic.java @@ -6,6 +6,7 @@ import io.anuke.mindustry.core.GameState.State; import io.anuke.mindustry.entities.TileEntity; import io.anuke.mindustry.game.EventType.*; import io.anuke.mindustry.game.GameMode; +import io.anuke.mindustry.game.Team; import io.anuke.mindustry.game.Teams; import io.anuke.mindustry.net.Net; import io.anuke.mindustry.type.ItemStack; @@ -77,11 +78,26 @@ public class Logic extends Module{ Events.fire(new WaveEvent()); } - //this never triggers in PvP; only for checking sector game-overs private void checkGameOver(){ if(!state.mode.isPvp && state.teams.get(defaultTeam).cores.size == 0 && !state.gameOver){ state.gameOver = true; - Events.fire(new GameOverEvent()); + Events.fire(new GameOverEvent(waveTeam)); + }else if(state.mode.isPvp){ + Team alive = null; + + for(Team team : Team.all){ + if(state.teams.get(team).cores.size > 0){ + if(alive != null){ + return; + } + alive = team; + } + } + + if(alive != null && !state.gameOver){ + state.gameOver = true; + Events.fire(new GameOverEvent(alive)); + } } } diff --git a/core/src/io/anuke/mindustry/core/NetServer.java b/core/src/io/anuke/mindustry/core/NetServer.java index b864ab79f6..65609c3ec3 100644 --- a/core/src/io/anuke/mindustry/core/NetServer.java +++ b/core/src/io/anuke/mindustry/core/NetServer.java @@ -202,6 +202,7 @@ public class NetServer extends Module{ return Integer.MAX_VALUE; }); player.setTeam(min); + Log.info("Auto-assigned player {0} to team {1}.", player.name, player.getTeam()); } connections.put(id, player); @@ -414,6 +415,15 @@ public class NetServer extends Module{ Log.info("&y{0} has connected.", player.name); } + @Remote(called = Loc.both) + public static void onGameOver(Team winner){ + threads.runGraphics(() -> ui.restart.show(winner)); + } + + public boolean isWaitingForPlayers(){ + return state.mode.isPvp && playerGroup.size() < 2; + } + public void update(){ if(threads.isEnabled() && !threads.isOnThread()) return; diff --git a/core/src/io/anuke/mindustry/core/Renderer.java b/core/src/io/anuke/mindustry/core/Renderer.java index f6ca6b4b63..f5e7090e99 100644 --- a/core/src/io/anuke/mindustry/core/Renderer.java +++ b/core/src/io/anuke/mindustry/core/Renderer.java @@ -336,11 +336,13 @@ public class Renderer extends RendererModule{ @Override public void resize(int width, int height){ + float lastX = camera.position.x, lastY = camera.position.y; super.resize(width, height); for(Player player : players){ control.input(player.playerIndex).resetCursor(); } - camera.position.set(players[0].x, players[0].y, 0); + camera.update(); + camera.position.set(lastX, lastY, 0f); } @Override diff --git a/core/src/io/anuke/mindustry/core/World.java b/core/src/io/anuke/mindustry/core/World.java index 6c35b3a357..24191adbc7 100644 --- a/core/src/io/anuke/mindustry/core/World.java +++ b/core/src/io/anuke/mindustry/core/World.java @@ -257,13 +257,27 @@ public class World extends Module{ endMapLoad(); - if(!headless && state.teams.get(players[0].getTeam()).cores.size == 0){ - ui.showError("$text.map.nospawn"); - threads.runDelay(() -> state.set(State.menu)); - invalidMap = true; + if(!headless){ + if(state.teams.get(players[0].getTeam()).cores.size == 0){ + ui.showError("$text.map.nospawn"); + invalidMap = true; + }else if(state.mode.isPvp){ + invalidMap = true; + for(Team team : Team.all){ + if(state.teams.get(team).cores.size != 0 && team != players[0].getTeam()){ + invalidMap = false; + } + } + if(invalidMap){ + ui.showError("$text.map.nospawn.pvp"); + } + } }else{ invalidMap = false; } + + if(invalidMap) threads.runDelay(() -> state.set(State.menu)); + } public void notifyChanged(Tile tile){ diff --git a/core/src/io/anuke/mindustry/entities/Player.java b/core/src/io/anuke/mindustry/entities/Player.java index 1740814796..4082129bb2 100644 --- a/core/src/io/anuke/mindustry/entities/Player.java +++ b/core/src/io/anuke/mindustry/entities/Player.java @@ -8,7 +8,6 @@ import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.utils.Queue; import io.anuke.annotations.Annotations.Loc; import io.anuke.annotations.Annotations.Remote; -import io.anuke.mindustry.Vars; import io.anuke.mindustry.content.Mechs; import io.anuke.mindustry.content.fx.UnitFx; import io.anuke.mindustry.entities.effect.ScorchDecal; @@ -727,12 +726,14 @@ public class Player extends Unit implements BuilderTrait, CarryTrait, ShooterTra //region utility methods - public void toggleTeam(){ - team = (team == Team.blue ? Team.red : Team.blue); - } - /** Resets all values of the player.*/ public void reset(){ + resetNoAdd(); + + add(); + } + + public void resetNoAdd(){ status.clear(); team = Team.blue; inventory.clear(); @@ -744,8 +745,6 @@ public class Player extends Unit implements BuilderTrait, CarryTrait, ShooterTra boostHeat = drownTime = hitTime = 0f; mech = (isMobile ? Mechs.starterMobile : Mechs.starterDesktop); placeQueue.clear(); - - add(); } public boolean isShooting(){ diff --git a/core/src/io/anuke/mindustry/game/EventType.java b/core/src/io/anuke/mindustry/game/EventType.java index 7bc5201664..da7c7ebe31 100644 --- a/core/src/io/anuke/mindustry/game/EventType.java +++ b/core/src/io/anuke/mindustry/game/EventType.java @@ -27,7 +27,11 @@ public class EventType{ } public static class GameOverEvent implements Event{ + public final Team winner; + public GameOverEvent(Team winner){ + this.winner = winner; + } } /** diff --git a/core/src/io/anuke/mindustry/game/GameMode.java b/core/src/io/anuke/mindustry/game/GameMode.java index 61575449ce..6d14023da8 100644 --- a/core/src/io/anuke/mindustry/game/GameMode.java +++ b/core/src/io/anuke/mindustry/game/GameMode.java @@ -25,7 +25,6 @@ public enum GameMode{ pvp{{ disableWaves = true; isPvp = true; - hidden = true; enemyCoreBuildRadius = 600f; respawnTime = 60 * 10; }}; diff --git a/core/src/io/anuke/mindustry/game/Team.java b/core/src/io/anuke/mindustry/game/Team.java index 7fce5dc6fb..1fd94ae2d1 100644 --- a/core/src/io/anuke/mindustry/game/Team.java +++ b/core/src/io/anuke/mindustry/game/Team.java @@ -1,6 +1,7 @@ package io.anuke.mindustry.game; import com.badlogic.gdx.graphics.Color; +import io.anuke.ucore.util.Bundles; public enum Team{ none(Color.DARK_GRAY), @@ -18,4 +19,8 @@ public enum Team{ this.color = color; intColor = Color.rgba8888(color); } + + public String localized(){ + return Bundles.get("team." + name() + ".name"); + } } diff --git a/core/src/io/anuke/mindustry/net/NetworkIO.java b/core/src/io/anuke/mindustry/net/NetworkIO.java index a21aa53b1f..b0dbd8beeb 100644 --- a/core/src/io/anuke/mindustry/net/NetworkIO.java +++ b/core/src/io/anuke/mindustry/net/NetworkIO.java @@ -183,6 +183,7 @@ public class NetworkIO{ Entities.clear(); int id = stream.readInt(); + player.resetNoAdd(); player.read(stream, TimeUtils.millis()); player.resetID(id); player.add(); @@ -258,7 +259,6 @@ public class NetworkIO{ i += consecutives; } - player.reset(); state.teams = new Teams(); byte teams = stream.readByte(); diff --git a/core/src/io/anuke/mindustry/ui/dialogs/RestartDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/RestartDialog.java index 1a7707d09b..f61cc21a4d 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/RestartDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/RestartDialog.java @@ -1,12 +1,14 @@ package io.anuke.mindustry.ui.dialogs; import io.anuke.mindustry.core.GameState.State; +import io.anuke.mindustry.game.Team; import io.anuke.mindustry.maps.Sector; import io.anuke.ucore.util.Bundles; import static io.anuke.mindustry.Vars.*; public class RestartDialog extends FloatingDialog{ + private Team winner; public RestartDialog(){ super("$text.gameover"); @@ -14,13 +16,25 @@ public class RestartDialog extends FloatingDialog{ shown(this::rebuild); } + public void show(Team winner){ + this.winner = winner; + show(); + } + void rebuild(){ buttons().clear(); content().clear(); buttons().margin(10); - if(world.getSector() == null){ + if(state.mode.isPvp){ + content().add(Bundles.format("text.gameover.pvp",winner.localized())).pad(6); + buttons().addButton("$text.menu", () -> { + hide(); + state.set(State.menu); + logic.reset(); + }).size(130f, 60f); + }else if(world.getSector() == null){ if(control.isHighScore()){ content().add("$text.highscore").pad(6); content().row(); diff --git a/core/src/io/anuke/mindustry/ui/fragments/HudFragment.java b/core/src/io/anuke/mindustry/ui/fragments/HudFragment.java index fb6d2443d5..fee77eda43 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/HudFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/HudFragment.java @@ -145,6 +145,11 @@ public class HudFragment extends Fragment{ t.table("clear", top -> top.add("[orange]< " + Bundles.get("text.paused") + " >").pad(6).get().setFontScale(fontScale * 1.5f)); }); + parent.fill(t -> { + t.visible(() -> netServer.isWaitingForPlayers() && !state.is(State.menu)); + t.table("clear", c -> c.margin(10).add("$text.waiting.players")); + }); + //'core is under attack' table parent.fill(t -> { float notifDuration = 240f; diff --git a/core/src/io/anuke/mindustry/world/blocks/storage/CoreBlock.java b/core/src/io/anuke/mindustry/world/blocks/storage/CoreBlock.java index 5a7f7c1646..87ea6f3b20 100644 --- a/core/src/io/anuke/mindustry/world/blocks/storage/CoreBlock.java +++ b/core/src/io/anuke/mindustry/world/blocks/storage/CoreBlock.java @@ -36,6 +36,7 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import static io.anuke.mindustry.Vars.netServer; import static io.anuke.mindustry.Vars.state; import static io.anuke.mindustry.Vars.unitGroups; @@ -204,7 +205,7 @@ public class CoreBlock extends StorageBlock{ if(entity.progress >= 1f){ Call.onUnitRespawn(tile, entity.currentUnit); } - }else{ + }else if(!netServer.isWaitingForPlayers()){ entity.warmup += Timers.delta(); if(entity.solid && entity.warmup > 60f && unitGroups[tile.getTeamID()].getByID(entity.droneID) == null && !Net.client()){ @@ -241,7 +242,6 @@ public class CoreBlock extends StorageBlock{ public class CoreEntity extends TileEntity implements SpawnerTrait{ public Unit currentUnit; - public float shieldHeat; int droneID = -1; boolean solid = true; float warmup; @@ -251,7 +251,7 @@ public class CoreBlock extends StorageBlock{ @Override public void updateSpawning(Unit unit){ - if(currentUnit == null){ + if(!netServer.isWaitingForPlayers() && currentUnit == null){ currentUnit = unit; progress = 0f; unit.set(tile.drawx(), tile.drawy()); diff --git a/server/src/io/anuke/mindustry/server/ServerControl.java b/server/src/io/anuke/mindustry/server/ServerControl.java index 5c5485d256..0a4b58a8ac 100644 --- a/server/src/io/anuke/mindustry/server/ServerControl.java +++ b/server/src/io/anuke/mindustry/server/ServerControl.java @@ -43,7 +43,6 @@ public class ServerControl extends Module{ private final CommandHandler handler = new CommandHandler(""); private int gameOvers; private boolean inExtraRound; - private Team winnerTeam; private Task lastTask; public ServerControl(String[] args){ @@ -121,8 +120,8 @@ public class ServerControl extends Module{ while(map == previous) map = maps.random(); } - Call.onInfoMessage((state.mode.isPvp && winnerTeam != null - ? "[YELLOW]The " + winnerTeam.name() + " team is victorious![]" : "[SCARLET]Game over![]") + Call.onInfoMessage((state.mode.isPvp + ? "[YELLOW]The " + event.winner.name() + " team is victorious![]" : "[SCARLET]Game over![]") + "\nNext selected map:[accent] "+map.name+"[]" + (map.meta.author() != null ? " by[accent] " + map.meta.author() + "[]" : "") + "."+ "\nNew game begins in " + roundExtraTime + " seconds."); @@ -650,7 +649,7 @@ public class ServerControl extends Module{ info("&lyCore destroyed."); inExtraRound = false; - Events.fire(new GameOverEvent()); + Events.fire(new GameOverEvent(Team.red)); }); handler.register("traceblock", " ", "Prints debug info about a block", arg -> { @@ -882,29 +881,10 @@ public class ServerControl extends Module{ } } - private void checkPvPGameOver(){ - Team alive = null; - - for(Team team : Team.all){ - if(state.teams.get(team).cores.size > 0){ - if(alive != null){ - return; - } - alive = team; - } - } - - if(alive != null && !state.gameOver){ - state.gameOver = true; - winnerTeam = alive; - Events.fire(new GameOverEvent()); - } - } - @Override public void update(){ if(!inExtraRound && state.mode.isPvp){ - checkPvPGameOver(); + // checkPvPGameOver(); } } }