diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index a9617ea0dd..514f526d6f 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -530,13 +530,13 @@ keybind.drop_unit.name = Drop Unit keybind.zoom_minimap.name = Zoom minimap mode.help.title = Description of modes mode.survival.name = Survival -mode.survival.description = The normal mode. Limited resources and automatic incoming waves. +mode.survival.description = The normal mode. Limited resources and automatic incoming waves.\n[gray]Requires enemy spawns in the map to play. mode.sandbox.name = Sandbox mode.sandbox.description = Infinite resources and no timer for waves. mode.pvp.name = PvP -mode.pvp.description = Fight against other players locally. Requires at least 2 differently-colored cores in the map to play. +mode.pvp.description = Fight against other players locally.\n[gray]Requires at least 2 differently-colored cores in the map to play. mode.attack.name = Attack -mode.attack.description = Destroy the enemy's base. No waves. Requires a red core in the map to play. +mode.attack.description = Destroy the enemy's base. No waves.\n[gray]Requires a red core in the map to play. mode.custom = Custom Rules rules.infiniteresources = Infinite Resources diff --git a/core/assets/sprites/uiskin.json b/core/assets/sprites/uiskin.json index c2188da4d9..5c0e25d6e1 100644 --- a/core/assets/sprites/uiskin.json +++ b/core/assets/sprites/uiskin.json @@ -156,7 +156,7 @@ down: button-down, up: button, over: button-over, - disabled: button, + disabled: button-disabled, disabledFontColor: gray } }, diff --git a/core/src/io/anuke/mindustry/game/Gamemode.java b/core/src/io/anuke/mindustry/game/Gamemode.java index 6118b273ef..ed492b6afb 100644 --- a/core/src/io/anuke/mindustry/game/Gamemode.java +++ b/core/src/io/anuke/mindustry/game/Gamemode.java @@ -1,15 +1,18 @@ package io.anuke.mindustry.game; -import io.anuke.arc.Core; -import io.anuke.arc.function.Consumer; +import io.anuke.arc.*; +import io.anuke.arc.function.*; +import io.anuke.mindustry.maps.*; -/** Defines preset rule sets.. */ +import static io.anuke.mindustry.Vars.waveTeam; + +/** Defines preset rule sets. */ public enum Gamemode{ survival(rules -> { rules.waveTimer = true; rules.waves = true; rules.unitDrops = true; - }), + }, map -> map.spawns > 0), sandbox(rules -> { rules.infiniteResources = true; rules.waves = true; @@ -21,7 +24,7 @@ public enum Gamemode{ rules.unitDrops = true; rules.waves = false; rules.attackMode = true; - }), + }, map -> map.teams.contains(waveTeam.ordinal())), pvp(rules -> { rules.pvp = true; rules.enemyCoreBuildRadius = 600f; @@ -33,7 +36,7 @@ public enum Gamemode{ rules.unitBuildSpeedMultiplier = 3f; rules.unitHealthMultiplier = 3f; rules.attackMode = true; - }), + }, map -> map.teams.size > 1), editor(true, rules -> { rules.infiniteResources = true; rules.editor = true; @@ -44,15 +47,27 @@ public enum Gamemode{ }); private final Consumer rules; + private final Predicate validator; + public final boolean hidden; + public final static Gamemode[] all = values(); Gamemode(Consumer rules){ this(false, rules); } Gamemode(boolean hidden, Consumer rules){ + this(hidden, rules, m -> true); + } + + Gamemode(Consumer rules, Predicate validator){ + this(false, rules, validator); + } + + Gamemode(boolean hidden, Consumer rules, Predicate validator){ this.rules = rules; this.hidden = hidden; + this.validator = validator; } /** Applies this preset to this ruleset. */ @@ -61,6 +76,11 @@ public enum Gamemode{ return in; } + /** @return whether this mode can be played on the specified map. */ + public boolean valid(Map map){ + return validator.test(map); + } + public String description(){ return Core.bundle.get("mode." + name() + ".description"); } diff --git a/core/src/io/anuke/mindustry/io/MapIO.java b/core/src/io/anuke/mindustry/io/MapIO.java index 8546f419bb..066d87d488 100644 --- a/core/src/io/anuke/mindustry/io/MapIO.java +++ b/core/src/io/anuke/mindustry/io/MapIO.java @@ -1,21 +1,18 @@ package io.anuke.mindustry.io; -import io.anuke.arc.collection.IntSet; -import io.anuke.arc.collection.StringMap; -import io.anuke.arc.files.FileHandle; -import io.anuke.arc.graphics.Color; -import io.anuke.arc.graphics.Pixmap; -import io.anuke.arc.graphics.Pixmap.Format; -import io.anuke.arc.util.io.CounterInputStream; -import io.anuke.mindustry.content.Blocks; -import io.anuke.mindustry.game.Team; -import io.anuke.mindustry.game.Version; -import io.anuke.mindustry.maps.Map; +import io.anuke.arc.collection.*; +import io.anuke.arc.files.*; +import io.anuke.arc.graphics.*; +import io.anuke.arc.graphics.Pixmap.*; +import io.anuke.arc.util.io.*; +import io.anuke.mindustry.content.*; +import io.anuke.mindustry.game.*; +import io.anuke.mindustry.maps.*; import io.anuke.mindustry.world.*; -import io.anuke.mindustry.world.blocks.storage.CoreBlock; +import io.anuke.mindustry.world.blocks.storage.*; import java.io.*; -import java.util.zip.InflaterInputStream; +import java.util.zip.*; import static io.anuke.mindustry.Vars.*; @@ -65,9 +62,8 @@ public class MapIO{ } public static Pixmap generatePreview(Map map) throws IOException{ - //by default, it does not have an enemy core or any other cores - map.tags.put("enemycore", "false"); - map.tags.put("othercore", "false"); + map.spawns = 0; + map.teams.clear(); try(InputStream is = new InflaterInputStream(map.file.read(bufferSize)); CounterInputStream counter = new CounterInputStream(is); DataInputStream stream = new DataInputStream(counter)){ SaveIO.readHeader(stream); @@ -79,7 +75,6 @@ public class MapIO{ Pixmap walls = new Pixmap(map.width, map.height, Format.RGBA8888); int black = Color.rgba8888(Color.BLACK); int shade = Color.rgba8888(0f, 0f, 0f, 0.5f); - IntSet teams = new IntSet(); CachedTile tile = new CachedTile(){ @Override public void setBlock(Block type){ @@ -95,7 +90,7 @@ public class MapIO{ public void setTeam(Team team){ super.setTeam(team); if(block instanceof CoreBlock){ - teams.add(team.ordinal()); + map.teams.add(team.ordinal()); } } }; @@ -121,20 +116,13 @@ public class MapIO{ }else{ floors.drawPixel(x, floors.getHeight() - 1 - y, colorFor(content.block(floorID), Blocks.air, Blocks.air, Team.none)); } + if(content.block(overlayID) == Blocks.spawn){ + map.spawns ++; + } return tile; } })); - if(teams.size > 1){ - //map must have other team's cores - map.tags.put("othercore", "true"); - } - - if(teams.contains(waveTeam.ordinal())){ - //map must have default enemy team's core - map.tags.put("enemycore", "true"); - } - floors.drawPixmap(walls, 0, 0); walls.dispose(); return floors; diff --git a/core/src/io/anuke/mindustry/maps/Map.java b/core/src/io/anuke/mindustry/maps/Map.java index d7213c8ff8..363e87d2b2 100644 --- a/core/src/io/anuke/mindustry/maps/Map.java +++ b/core/src/io/anuke/mindustry/maps/Map.java @@ -1,11 +1,11 @@ package io.anuke.mindustry.maps; import io.anuke.arc.Core; -import io.anuke.arc.collection.StringMap; +import io.anuke.arc.collection.*; import io.anuke.arc.files.FileHandle; import io.anuke.arc.graphics.Texture; import io.anuke.mindustry.Vars; -import io.anuke.mindustry.game.Rules; +import io.anuke.mindustry.game.*; import io.anuke.mindustry.io.JsonIO; public class Map implements Comparable{ @@ -23,6 +23,10 @@ public class Map implements Comparable{ public Texture texture; /** Build that this map was created in. -1 = unknown or custom build. */ public int build; + /** All teams present on this map.*/ + public IntSet teams = new IntSet(); + /** Number of enemy spawns on this map.*/ + public int spawns = 0; public Map(FileHandle file, int width, int height, StringMap tags, boolean custom, int version, int build){ this.custom = custom; @@ -63,20 +67,16 @@ public class Map implements Comparable{ } /** Whether this map has a core of the enemy 'wave' team. Default: true. - * Used for checking Attack mode validity.*/ + * Used for checking Attack mode validity. public boolean hasEnemyCore(){ return tags.get("enemycore", "true").equals("true"); } /** Whether this map has a core of any team except the default player team. Default: true. - * Used for checking PvP mode validity.*/ + * Used for checking PvP mode validity. public boolean hasOtherCores(){ return tags.get("othercore", "true").equals("true"); - } - - public boolean attribute(MapAttribute attr){ - return tags.getBool(attr.name()); - } + }*/ public String author(){ return tag("author"); diff --git a/core/src/io/anuke/mindustry/maps/MapAttribute.java b/core/src/io/anuke/mindustry/maps/MapAttribute.java deleted file mode 100644 index 3df99af00e..0000000000 --- a/core/src/io/anuke/mindustry/maps/MapAttribute.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.anuke.mindustry.maps; - -import io.anuke.arc.collection.*; -import io.anuke.arc.function.*; - -import static io.anuke.mindustry.Vars.*; - -/** Defines a specific type of attribute for a map, usually whether or not it supports a certain type of mode.*/ -public enum MapAttribute{ - /** Whether a map has a player spawnpoint in it.*/ - spawnpoint(teams -> teams.contains(defaultTeam.ordinal())), - /** Whether a map has a wave team core to attack.*/ - attack(teams -> teams.contains(waveTeam.ordinal())), - /** Whether this map supports PvP.*/ - pvp(teams -> teams.size > 1); - - private final Predicate validator; - - public static final MapAttribute[] all = values(); - - MapAttribute(Predicate set){ - this.validator = set; - } - - //todo also take into account enemy spawnpoints - public boolean validate(IntSet teams){ - return validator.test(teams); - } -} diff --git a/core/src/io/anuke/mindustry/maps/Maps.java b/core/src/io/anuke/mindustry/maps/Maps.java index 08d184b6b9..0dfefd459a 100644 --- a/core/src/io/anuke/mindustry/maps/Maps.java +++ b/core/src/io/anuke/mindustry/maps/Maps.java @@ -1,26 +1,25 @@ package io.anuke.mindustry.maps; -import io.anuke.arc.Core; +import io.anuke.arc.*; import io.anuke.arc.collection.*; -import io.anuke.arc.files.FileHandle; -import io.anuke.arc.function.ExceptionRunnable; -import io.anuke.arc.graphics.Texture; +import io.anuke.arc.files.*; +import io.anuke.arc.function.*; +import io.anuke.arc.graphics.*; import io.anuke.arc.util.*; -import io.anuke.arc.util.serialization.Json; -import io.anuke.mindustry.game.SpawnGroup; -import io.anuke.mindustry.io.LegacyMapIO; -import io.anuke.mindustry.io.MapIO; -import io.anuke.mindustry.world.Tile; -import io.anuke.mindustry.world.blocks.storage.CoreBlock; +import io.anuke.arc.util.serialization.*; +import io.anuke.mindustry.content.*; +import io.anuke.mindustry.game.*; +import io.anuke.mindustry.io.*; +import io.anuke.mindustry.world.*; +import io.anuke.mindustry.world.blocks.storage.*; -import java.io.IOException; -import java.io.StringWriter; +import java.io.*; import static io.anuke.mindustry.Vars.*; public class Maps implements Disposable{ /** List of all built-in maps. Filenames only. */ - private static String[] defaultMapNames = {"fortress", "labyrinth", "islands", "tendrils", "caldera", "glacier", "vein"}; + private static String[] defaultMapNames = {"fortress", "labyrinth", "islands", "tendrils", "caldera", "glacier", "veins"}; /** All maps stored in an ordered array. */ private Array maps = new Array<>(); /** Serializer for meta. */ @@ -110,31 +109,24 @@ public class Maps implements Disposable{ MapIO.writeMap(file, map); if(!headless){ - //by default, it does not have an enemy core or any other cores - map.tags.put("enemycore", "false"); - map.tags.put("othercore", "false"); - IntSet teams = new IntSet(); + //reset attributes + map.teams.clear(); + map.spawns = 0; for(int x = 0; x < map.width; x++){ for(int y = 0; y < map.height; y++){ Tile tile = world.getTiles()[x][y]; if(tile.block() instanceof CoreBlock){ - teams.add(tile.getTeamID()); + map.teams.add(tile.getTeamID()); + } + + if(tile.overlay() == Blocks.spawn){ + map.spawns ++; } } } - if(teams.size > 1){ - //map must have other team's cores - map.tags.put("othercore", "true"); - } - - if(teams.contains(waveTeam.ordinal())){ - //map must have default enemy team's core - map.tags.put("enemycore", "true"); - } - map.texture = new Texture(MapIO.generatePreview(world.getTiles())); } maps.add(map); diff --git a/core/src/io/anuke/mindustry/ui/dialogs/MapPlayDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/MapPlayDialog.java index bafcbbafc0..692a83486d 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/MapPlayDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/MapPlayDialog.java @@ -3,7 +3,7 @@ package io.anuke.mindustry.ui.dialogs; import io.anuke.arc.Core; import io.anuke.arc.scene.ui.ScrollPane; import io.anuke.arc.scene.ui.layout.Table; -import io.anuke.arc.util.Scaling; +import io.anuke.arc.util.*; import io.anuke.mindustry.game.*; import io.anuke.mindustry.maps.Map; import io.anuke.mindustry.ui.BorderImage; @@ -32,9 +32,9 @@ public class MapPlayDialog extends FloatingDialog{ title.setText(map.name()); cont.clearChildren(); - //reset to a valid mode after switching to attack - if((selectedGamemode == Gamemode.attack && !map.hasEnemyCore()) || (selectedGamemode == Gamemode.pvp && !map.hasOtherCores())){ - selectedGamemode = Gamemode.survival; + //reset to any valid mode after switching to attack (one must exist) + if(!selectedGamemode.valid(map)){ + selectedGamemode = Structs.find(Gamemode.all, m -> m.valid(map)); } rules = map.rules(); @@ -50,14 +50,10 @@ public class MapPlayDialog extends FloatingDialog{ for(Gamemode mode : Gamemode.values()){ if(mode.hidden) continue; - if((mode == Gamemode.attack && !map.hasEnemyCore()) || (mode == Gamemode.pvp && !map.hasOtherCores())){ - continue; - } - modes.addButton(mode.toString(), "toggle", () -> { selectedGamemode = mode; rules = mode.apply(map.rules()); - }).update(b -> b.setChecked(selectedGamemode == mode)).size(140f, 54f); + }).update(b -> b.setChecked(selectedGamemode == mode)).size(140f, 54f).disabled(!mode.valid(map)); if(i++ % 2 == 1) modes.row(); } selmode.add(modes);