# Conflicts:
#	core/assets/bundles/bundle_ko.properties
#	core/src/io/anuke/mindustry/Vars.java
#	core/src/io/anuke/mindustry/core/UI.java
This commit is contained in:
Anuken
2018-07-14 09:32:55 -04:00
1239 changed files with 52453 additions and 30445 deletions

View File

@@ -1,42 +1,42 @@
package io.anuke.mindustry;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Filter;
import com.badlogic.gdx.graphics.PixmapIO;
import io.anuke.mindustry.core.*;
import io.anuke.mindustry.io.BlockLoader;
import io.anuke.mindustry.io.BundleLoader;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.modules.ModuleCore;
import io.anuke.ucore.util.Log;
import static io.anuke.mindustry.Vars.*;
public class Mindustry extends ModuleCore {
public class Mindustry extends ModuleCore{
@Override
public void init(){
debug = Platform.instance.isDebug();
@Override
public void init(){
Timers.mark();
Log.setUseColors(false);
BundleLoader.load();
BlockLoader.load();
Vars.init();
module(logic = new Logic());
module(world = new World());
module(control = new Control());
module(renderer = new Renderer());
module(ui = new UI());
module(netServer = new NetServer());
module(netClient = new NetClient());
module(netCommon = new NetCommon());
}
debug = Platform.instance.isDebug();
@Override
public void render(){
super.render();
threads.handleRender();
}
Log.setUseColors(false);
BundleLoader.load();
ContentLoader.load();
module(logic = new Logic());
module(world = new World());
module(control = new Control());
module(renderer = new Renderer());
module(ui = new UI());
module(netServer = new NetServer());
module(netClient = new NetClient());
Log.info("Time to load [total]: {0}", Timers.elapsed());
}
@Override
public void render(){
super.render();
threads.handleRender();
}
}

View File

@@ -4,154 +4,175 @@ import com.badlogic.gdx.Application.ApplicationType;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.IntMap;
import io.anuke.mindustry.core.*;
import io.anuke.mindustry.entities.Bullet;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.entities.bullet.Bullet;
import io.anuke.mindustry.entities.effect.Fire;
import io.anuke.mindustry.entities.effect.ItemDrop;
import io.anuke.mindustry.entities.effect.Puddle;
import io.anuke.mindustry.entities.effect.Shield;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.mindustry.core.Platform;
import io.anuke.mindustry.net.EditLog;
import io.anuke.mindustry.net.ClientDebug;
import io.anuke.mindustry.net.ServerDebug;
import io.anuke.ucore.UCore;
import io.anuke.ucore.entities.EffectEntity;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.entities.units.BaseUnit;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.io.Version;
import io.anuke.mindustry.net.Net;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.impl.EffectEntity;
import io.anuke.ucore.entities.trait.DrawTrait;
import io.anuke.ucore.scene.ui.layout.Unit;
import io.anuke.ucore.util.OS;
import io.anuke.ucore.util.Translator;
import java.util.Locale;
public class Vars{
public static final boolean testMobile = false;
//shorthand for whether or not this is running on android
public static final boolean mobile = (Gdx.app.getType() == ApplicationType.Android) ||
Gdx.app.getType() == ApplicationType.iOS || testMobile;
public static final boolean ios = Gdx.app.getType() == ApplicationType.iOS;
public static final boolean android = Gdx.app.getType() == ApplicationType.Android;
//shorthand for whether or not this is running on GWT
public static final boolean gwt = (Gdx.app.getType() == ApplicationType.WebGL);
//whether to send block state change events to players
public static final boolean syncBlockState = false;
//how far away from the player blocks can be placed
public static final float placerange = 66;
//respawn time in frames
public static final float respawnduration = 60*4;
//time between waves in frames (on normal mode)
public static final float wavespace = 60*60*(mobile ? 1 : 1);
//waves can last no longer than 3 minutes, otherwise the next one spawns
public static final float maxwavespace = 60*60*4f;
//advance time the pathfinding starts at
public static final float aheadPathfinding = 60*15;
//how far away from spawn points the player can't place blocks
public static final float enemyspawnspace = 65;
//discord group URL
public static final String discordURL = "https://discord.gg/BKADYds";
public static final String releasesURL = "https://api.github.com/repos/Anuken/Mindustry/releases";
public static final String macAppDir = UCore.getProperty("user.home") + "/Library/Application Support/";
//directory for user-created map data
public static final FileHandle customMapDirectory = gwt ? null : UCore.isAssets() ?
Gdx.files.local("../../desktop/mindustry-maps") :
OS.isMac ? (Gdx.files.absolute(macAppDir).child("maps/")) :
Gdx.files.local("mindustry-maps/");
//save file directory
public static final FileHandle saveDirectory = gwt ? null : UCore.isAssets() ?
Gdx.files.local("../../desktop/mindustry-saves") :
OS.isMac ? (Gdx.files.absolute(macAppDir).child("saves/")) :
Gdx.files.local("mindustry-saves/");
//scale of the font
public static float fontscale = Math.max(Unit.dp.scl(1f)/2f, 0.5f);
//camera zoom displayed on startup
public static final int baseCameraScale = Math.round(Unit.dp.scl(4));
//how much the zoom changes every zoom button press (unused?)
public static final int zoomScale = Math.round(Unit.dp.scl(1));
//if true, player speed will be increased, massive amounts of resources will be given on start, and other debug options will be available
public static boolean debug = false;
public static boolean debugNet = true;
public static boolean console = false;
//whether the player can clip through walls
public static boolean noclip = false;
//whether to draw chunk borders
public static boolean debugChunks = false;
//whether turrets have infinite ammo (only with debug)
public static boolean infiniteAmmo = true;
//whether to show paths of enemies
public static boolean showPaths = false;
//if false, player is always hidden
public static boolean showPlayer = true;
//whether to hide ui, only on debug
public static boolean showUI = true;
//respawn time in frames
public static final float respawnduration = 60 * 4;
//time between waves in frames (on normal mode)
public static final float wavespace = 60 * 60 * 2f;
//waves can last no longer than 3 minutes, otherwise the next one spawns
public static final float maxwavespace = 60 * 60 * 4f;
//set ridiculously high for now
public static final float coreBuildRange = 800999f;
//discord group URL
public static final String discordURL = "https://discord.gg/BKADYds";
public static final String releasesURL = "https://api.github.com/repos/Anuken/Mindustry/releases";
public static final int maxTextLength = 150;
public static final int maxNameLength = 40;
public static final int maxCharNameLength = 20;
public static final int saveSlots = 64;
public static final float itemSize = 5f;
public static final int tilesize = 8;
public static final Locale[] locales = {new Locale("en"), new Locale("fr"), new Locale("ru"), new Locale("uk", "UA"), new Locale("pl"),
new Locale("de"), new Locale("pt", "BR"), new Locale("ko"), new Locale("in", "ID"),
new Locale("ita"), new Locale("es"), new Locale("zh","TW")};
public static final Color[] playerColors = {
Color.valueOf("82759a"),
Color.valueOf("c0c1c5"),
Color.valueOf("fff0e7"),
Color.valueOf("7d2953"),
Color.valueOf("ff074e"),
Color.valueOf("ff072a"),
Color.valueOf("ff76a6"),
Color.valueOf("a95238"),
Color.valueOf("ffa108"),
Color.valueOf("feeb2c"),
Color.valueOf("ffcaa8"),
Color.valueOf("008551"),
Color.valueOf("00e339"),
Color.valueOf("423c7b"),
Color.valueOf("4b5ef1"),
Color.valueOf("2cabfe"),
};
//server port
public static final int port = 6567;
public static final int webPort = 6568;
public static boolean testMobile;
//shorthand for whether or not this is running on android or ios
public static boolean mobile;
public static boolean ios;
public static boolean android;
//shorthand for whether or not this is running on GWT
public static boolean gwt;
//directory for user-created map data
public static FileHandle customMapDirectory;
//save file directory
public static FileHandle saveDirectory;
public static String mapExtension = "mmap";
public static String saveExtension = "msav";
//scale of the font
public static float fontScale;
//camera zoom displayed on startup
public static int baseCameraScale;
//if true, player speed will be increased, massive amounts of resources will be given on start, and other debug options will be available
public static boolean debug = false;
public static boolean console = false;
//whether the player can clip through walls
public static boolean noclip = false;
//whether turrets have infinite ammo (only with debug)
public static boolean infiniteAmmo = true;
//whether to show paths of enemies
public static boolean showPaths = false;
//if false, player is always hidden
public static boolean showPlayer = true;
//whether to hide ui, only on debug
public static boolean showUI = true;
//whether to show block debug
public static boolean showBlockDebug = false;
public static boolean showFog = true;
public static boolean headless = false;
public static float controllerMin = 0.25f;
public static float baseControllerSpeed = 11f;
//only if smoothCamera
public static boolean snapCamera = true;
public static GameState state;
public static ThreadHandler threads;
public static boolean headless = false;
public static Control control;
public static Logic logic;
public static Renderer renderer;
public static UI ui;
public static World world;
public static NetServer netServer;
public static NetClient netClient;
public static float controllerMin = 0.25f;
public static Player[] players = {};
public static float baseControllerSpeed = 11f;
public static EntityGroup<Player> playerGroup;
public static EntityGroup<TileEntity> tileGroup;
public static EntityGroup<Bullet> bulletGroup;
public static EntityGroup<Shield> shieldGroup;
public static EntityGroup<EffectEntity> effectGroup;
public static EntityGroup<DrawTrait> groundEffectGroup;
public static EntityGroup<ItemDrop> itemGroup;
public static final int saveSlots = 64;
//amount of drops that are left when breaking a block
public static final float breakDropAmount = 0.5f;
public static Array<EditLog> currentEditLogs = new Array<>();
//only if smoothCamera
public static boolean snapCamera = true;
public static final int tilesize = 8;
public static EntityGroup<Puddle> puddleGroup;
public static EntityGroup<Fire> fireGroup;
public static EntityGroup<BaseUnit>[] unitGroups;
public static final Locale[] locales = {new Locale("en"), new Locale("fr"), new Locale("ru"), new Locale("uk", "UA"), new Locale("pl"),
new Locale("de"), new Locale("pt", "BR"), new Locale("ko"), new Locale("in", "ID"), new Locale("ita"), new Locale("es") , new Locale("zh","TW")};
public static final Translator[] tmptr = new Translator[]{new Translator(), new Translator(), new Translator(), new Translator()};
public static final Color[] playerColors = {
Color.valueOf("82759a"),
Color.valueOf("c0c1c5"),
Color.valueOf("fff0e7"),
Color.valueOf("7d2953"),
Color.valueOf("ff074e"),
Color.valueOf("ff072a"),
Color.valueOf("ff76a6"),
Color.valueOf("a95238"),
Color.valueOf("ffa108"),
Color.valueOf("feeb2c"),
Color.valueOf("ffcaa8"),
Color.valueOf("008551"),
Color.valueOf("00e339"),
Color.valueOf("423c7b"),
Color.valueOf("4b5ef1"),
Color.valueOf("2cabfe"),
};
public static void init(){
Version.init();
//server port
public static final int port = 6567;
public static final int webPort = 6568;
playerGroup = Entities.addGroup(Player.class).enableMapping();
tileGroup = Entities.addGroup(TileEntity.class, false);
bulletGroup = Entities.addGroup(Bullet.class).enableMapping();
shieldGroup = Entities.addGroup(Shield.class, false);
effectGroup = Entities.addGroup(EffectEntity.class, false);
groundEffectGroup = Entities.addGroup(DrawTrait.class, false);
puddleGroup = Entities.addGroup(Puddle.class, false).enableMapping();
itemGroup = Entities.addGroup(ItemDrop.class).enableMapping();
fireGroup = Entities.addGroup(Fire.class, false).enableMapping();
unitGroups = new EntityGroup[Team.all.length];
public static final GameState state = new GameState();
public static final ThreadHandler threads = new ThreadHandler(Platform.instance.getThreadProvider());
for(Team team : Team.all){
unitGroups[team.ordinal()] = Entities.addGroup(BaseUnit.class).enableMapping();
}
public static final ServerDebug serverDebug = new ServerDebug();
public static final ClientDebug clientDebug = new ClientDebug();
for(EntityGroup<?> group : Entities.getAllGroups()){
group.setRemoveListener(entity -> {
if(entity instanceof SyncTrait && Net.client()){
netClient.addRemovedEntity((entity).getID());
}
});
}
public static Control control;
public static Logic logic;
public static Renderer renderer;
public static UI ui;
public static World world;
public static NetCommon netCommon;
public static NetServer netServer;
public static NetClient netClient;
public static Player player;
threads = new ThreadHandler(Platform.instance.getThreadProvider());
public static final EntityGroup<Player> playerGroup = Entities.addGroup(Player.class).enableMapping();
public static final EntityGroup<Enemy> enemyGroup = Entities.addGroup(Enemy.class).enableMapping();
public static final EntityGroup<TileEntity> tileGroup = Entities.addGroup(TileEntity.class, false);
public static final EntityGroup<Bullet> bulletGroup = Entities.addGroup(Bullet.class);
public static final EntityGroup<Shield> shieldGroup = Entities.addGroup(Shield.class, false);
public static final EntityGroup<EffectEntity> effectGroup = Entities.addGroup(EffectEntity.class, false);
mobile = Gdx.app.getType() == ApplicationType.Android || Gdx.app.getType() == ApplicationType.iOS || testMobile;
ios = Gdx.app.getType() == ApplicationType.iOS;
android = Gdx.app.getType() == ApplicationType.Android;
gwt = Gdx.app.getType() == ApplicationType.WebGL;
if(!gwt){
customMapDirectory = OS.getAppDataDirectory("Mindustry").child("maps/");
saveDirectory = OS.getAppDataDirectory("Mindustry").child("saves/");
}
fontScale = Math.max(Unit.dp.scl(1f) / 2f, 0.5f);
baseCameraScale = Math.round(Unit.dp.scl(4));
}
}

View File

@@ -0,0 +1,328 @@
package io.anuke.mindustry.ai;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Bits;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.ObjectSet;
import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.content.blocks.Blocks;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.game.EventType.TileChangeEvent;
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.game.TeamInfo.TeamData;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.meta.BlockFlag;
import io.anuke.ucore.core.Events;
import io.anuke.ucore.entities.trait.Entity;
import io.anuke.ucore.function.Predicate;
import io.anuke.ucore.util.EnumSet;
import io.anuke.ucore.util.Geometry;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.*;
//TODO consider using quadtrees for finding specific types of blocks within an area
//TODO maybe use Arrays instead of ObjectSets?
/**
* Class used for indexing special target blocks for AI.
*/
public class BlockIndexer{
/**
* Size of one ore quadrant.
*/
private final static int oreQuadrantSize = 20;
/**
* Size of one structure quadrant.
*/
private final static int structQuadrantSize = 12;
/**
* Set of all ores that are being scanned.
*/
private final ObjectSet<Item> scanOres = ObjectSet.with(Items.tungsten, Items.coal, Items.lead, Items.thorium, Items.titanium);
private final ObjectSet<Item> itemSet = new ObjectSet<>();
/**
* Stores all ore quadtrants on the map.
*/
private ObjectMap<Item, ObjectSet<Tile>> ores;
/**
* Tags all quadrants.
*/
private Bits[] structQuadrants;
/**
* Maps teams to a map of flagged tiles by type.
*/
private ObjectMap<BlockFlag, ObjectSet<Tile>> enemyMap = new ObjectMap<>();
/**
* Maps teams to a map of flagged tiles by type.
*/
private ObjectMap<BlockFlag, ObjectSet<Tile>> allyMap = new ObjectMap<>();
/**
* Empty map for invalid teams.
*/
private ObjectMap<BlockFlag, ObjectSet<Tile>> emptyMap = new ObjectMap<>();
/**
* Maps tile positions to their last known tile index data.
*/
private IntMap<TileIndex> typeMap = new IntMap<>();
/**
* Empty array used for returning.
*/
private ObjectSet<Tile> emptyArray = new ObjectSet<>();
public BlockIndexer(){
Events.on(TileChangeEvent.class, tile -> {
if(typeMap.get(tile.packedPosition()) != null){
TileIndex index = typeMap.get(tile.packedPosition());
for(BlockFlag flag : index.flags){
getMap(index.team).get(flag).remove(tile);
}
}
process(tile);
updateQuadrant(tile);
});
Events.on(WorldLoadEvent.class, () -> {
enemyMap.clear();
allyMap.clear();
typeMap.clear();
ores = null;
//create bitset for each team type that contains each quadrant
structQuadrants = new Bits[Team.all.length];
for(int i = 0; i < Team.all.length; i++){
structQuadrants[i] = new Bits(Mathf.ceil(world.width() / (float) structQuadrantSize) * Mathf.ceil(world.height() / (float) structQuadrantSize));
}
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
process(world.tile(x, y));
}
}
for(int x = 0; x < quadWidth(); x++){
for(int y = 0; y < quadHeight(); y++){
updateQuadrant(world.tile(x * structQuadrantSize, y * structQuadrantSize));
}
}
scanOres();
});
}
/**
* Get all allied blocks with a flag.
*/
public ObjectSet<Tile> getAllied(Team team, BlockFlag type){
return (state.teams.get(team).ally ? allyMap : enemyMap).get(type, emptyArray);
}
/**
* Get all enemy blocks with a flag.
*/
public ObjectSet<Tile> getEnemy(Team team, BlockFlag type){
return (!state.teams.get(team).ally ? allyMap : enemyMap).get(type, emptyArray);
}
public TileEntity findTile(Team team, float x, float y, float range, Predicate<Tile> pred){
Entity closest = null;
float dst = 0;
for(int rx = Math.max((int) ((x - range) / tilesize / structQuadrantSize), 0); rx <= (int) ((x + range) / tilesize / structQuadrantSize) && rx < quadWidth(); rx++){
for(int ry = Math.max((int) ((y - range) / tilesize / structQuadrantSize), 0); ry <= (int) ((y + range) / tilesize / structQuadrantSize) && ry < quadHeight(); ry++){
if(!getQuad(team, rx, ry)) continue;
for(int tx = rx * structQuadrantSize; tx < (rx + 1) * structQuadrantSize && tx < world.width(); tx++){
for(int ty = ry * structQuadrantSize; ty < (ry + 1) * structQuadrantSize && ty < world.height(); ty++){
Tile other = world.tile(tx, ty);
if(other == null || other.entity == null || !pred.test(other)) continue;
TileEntity e = other.entity;
float ndst = Vector2.dst(x, y, e.x, e.y);
if(ndst < range && (closest == null || ndst < dst)){
dst = ndst;
closest = e;
}
}
}
}
}
return (TileEntity) closest;
}
/**
* Returns a set of tiles that have ores of the specified type nearby.
* While each tile in the set is not guaranteed to have an ore directly on it,
* each tile will at least have an ore within {@link #oreQuadrantSize} / 2 blocks of it.
* Only specific ore types are scanned. See {@link #scanOres}.
*/
public ObjectSet<Tile> getOrePositions(Item item){
return ores.get(item, emptyArray);
}
/**
* Find the closest ore block relative to a position.
*/
public Tile findClosestOre(float xp, float yp, Item item){
Tile tile = Geometry.findClosest(xp, yp, world.indexer().getOrePositions(item));
if(tile == null) return null;
for(int x = Math.max(0, tile.x - oreQuadrantSize / 2); x < tile.x + oreQuadrantSize / 2 && x < world.width(); x++){
for(int y = Math.max(0, tile.y - oreQuadrantSize / 2); y < tile.y + oreQuadrantSize / 2 && y < world.height(); y++){
Tile res = world.tile(x, y);
if(res.block() == Blocks.air && res.floor().drops != null && res.floor().drops.item == item){
return res;
}
}
}
return null;
}
private void process(Tile tile){
if(tile.block().flags != null &&
tile.getTeam() != Team.none){
ObjectMap<BlockFlag, ObjectSet<Tile>> map = getMap(tile.getTeam());
for(BlockFlag flag : tile.block().flags){
ObjectSet<Tile> arr = map.get(flag);
if(arr == null){
arr = new ObjectSet<>();
map.put(flag, arr);
}
arr.add(tile);
map.put(flag, arr);
}
typeMap.put(tile.packedPosition(), new TileIndex(tile.block().flags, tile.getTeam()));
}
if(ores == null) return;
int quadrantX = tile.x / oreQuadrantSize;
int quadrantY = tile.y / oreQuadrantSize;
itemSet.clear();
Tile rounded = world.tile(Mathf.clamp(quadrantX * oreQuadrantSize + oreQuadrantSize / 2, 0, world.width() - 1),
Mathf.clamp(quadrantY * oreQuadrantSize + oreQuadrantSize / 2, 0, world.height() - 1));
//find all items that this quadrant contains
for(int x = quadrantX * structQuadrantSize; x < world.width() && x < (quadrantX + 1) * structQuadrantSize; x++){
for(int y = quadrantY * structQuadrantSize; y < world.height() && y < (quadrantY + 1) * structQuadrantSize; y++){
Tile result = world.tile(x, y);
if(result.block().drops == null || !scanOres.contains(result.block().drops.item)) continue;
itemSet.add(result.block().drops.item);
}
}
//update quadrant at this position
for(Item item : scanOres){
ObjectSet<Tile> set = ores.get(item);
//update quadrant status depending on whether the item is in it
if(!itemSet.contains(item)){
set.remove(rounded);
}else{
set.add(rounded);
}
}
}
private void updateQuadrant(Tile tile){
//this quadrant is now 'dirty', re-scan the whole thing
int quadrantX = tile.x / structQuadrantSize;
int quadrantY = tile.y / structQuadrantSize;
int index = quadrantX + quadrantY * quadWidth();
//Log.info("Updating quadrant: {0} {1}", quadrantX, quadrantY);
for(TeamData data : state.teams.getTeams()){
//fast-set this quadrant to 'occupied' if the tile just placed is already of this team
if(tile.getTeam() == data.team && tile.entity != null){
structQuadrants[data.team.ordinal()].set(index);
continue; //no need to process futher
}
structQuadrants[data.team.ordinal()].clear(index);
outer:
for(int x = quadrantX * structQuadrantSize; x < world.width() && x < (quadrantX + 1) * structQuadrantSize; x++){
for(int y = quadrantY * structQuadrantSize; y < world.height() && y < (quadrantY + 1) * structQuadrantSize; y++){
Tile result = world.tile(x, y);
//when a targetable block is found, mark this quadrant as occupied and stop searching
if(result.entity != null && result.getTeam() == data.team){
structQuadrants[data.team.ordinal()].set(index);
break outer;
}
}
}
}
}
private boolean getQuad(Team team, int quadrantX, int quadrantY){
int index = quadrantX + quadrantY * Mathf.ceil(world.width() / (float) structQuadrantSize);
return structQuadrants[team.ordinal()].get(index);
}
private int quadWidth(){
return Mathf.ceil(world.width() / (float) structQuadrantSize);
}
private int quadHeight(){
return Mathf.ceil(world.height() / (float) structQuadrantSize);
}
private ObjectMap<BlockFlag, ObjectSet<Tile>> getMap(Team team){
if(!state.teams.has(team)) return emptyMap;
return state.teams.get(team).ally ? allyMap : enemyMap;
}
private void scanOres(){
ores = new ObjectMap<>();
//initialize ore map with empty sets
for(Item item : scanOres){
ores.put(item, new ObjectSet<>());
}
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
int qx = (x / oreQuadrantSize);
int qy = (y / oreQuadrantSize);
Tile tile = world.tile(x, y);
//add position of quadrant to list when an ore is found
if(tile.floor().drops != null && scanOres.contains(tile.floor().drops.item) && tile.block() == Blocks.air){
ores.get(tile.floor().drops.item).add(world.tile(
//make sure to clamp quadrant middle position, since it might go off bounds
Mathf.clamp(qx * oreQuadrantSize + oreQuadrantSize / 2, 0, world.width() - 1),
Mathf.clamp(qy * oreQuadrantSize + oreQuadrantSize / 2, 0, world.height() - 1)));
}
}
}
}
private class TileIndex{
public final EnumSet<BlockFlag> flags;
public final Team team;
public TileIndex(EnumSet<BlockFlag> flags, Team team){
this.flags = flags;
this.team = team;
}
}
}

View File

@@ -1,69 +0,0 @@
package io.anuke.mindustry.ai;
import com.badlogic.gdx.ai.pfa.Heuristic;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.function.Predicate;
import static io.anuke.mindustry.Vars.tilesize;
public class Heuristics {
/**How many times more it costs to go through a destructible block than an empty block.*/
static final float solidMultiplier = 5f;
/**How many times more it costs to go through a tile that touches a solid block.*/
static final float occludedMultiplier = 5f;
/**Calculates the fastest path. No priorities, just avoids solid blocks.*/
public static class FastestHeuristic implements Heuristic<Tile> {
@Override
public float estimate(Tile node, Tile other){
//Get Manhattan distance cost
float cost = Math.abs(node.worldx() - other.worldx()) + Math.abs(node.worldy() - other.worldy());
//If either one of the tiles is a breakable solid block (that is, it's player-made),
//increase the cost by the tilesize times the solid block multiplier
//Also add the block health, so blocks with more health cost more to traverse
if(node.breakable() && node.block().solid) cost += tilesize* solidMultiplier + node.block().health;
if(other.breakable() && other.block().solid) cost += tilesize* solidMultiplier + other.block().health;
//if this block has solid blocks near it, increase the cost, as we don't want enemies hugging walls
if(node.occluded) cost += tilesize*occludedMultiplier;
return cost;
}
}
/**Calculates the fastest and most destructive path based on a block predicate.*/
public static class DestrutiveHeuristic implements Heuristic<Tile> {
/**Should return whether a block if "free", e.g. whether it's an important target*/
private final Predicate<Block> frees;
public DestrutiveHeuristic(Predicate<Block> frees){
this.frees = frees;
}
@Override
public float estimate(Tile node, Tile other){
//Get Manhattan distance cost
float cost = Math.abs(node.worldx() - other.worldx()) + Math.abs(node.worldy() - other.worldy());
//If either one of the tiles is a breakable solid block (that is, it's player-made),
//increase the cost by the tilesize times the solid block multiplier
//Also add the block health, so blocks with more health cost more to traverse
if(node.breakable() && node.block().solid) cost += tilesize* solidMultiplier + node.block().health;
if(other.breakable() && other.block().solid) cost += tilesize* solidMultiplier + other.block().health;
//if this block has solid blocks near it, increase the cost, as we don't want enemies hugging walls
if(node.occluded) cost += tilesize*occludedMultiplier;
if(other.getLinked() != null) other = other.getLinked();
if(node.getLinked() != null) node = node.getLinked();
//check if it's free
if(frees.test(other.block()) || frees.test(node.block())) cost = 0;
return cost;
}
}
}

View File

@@ -1,12 +0,0 @@
package io.anuke.mindustry.ai;
/**An interface for an indexed graph that doesn't use allocations for connections.*/
public interface OptimizedGraph<N>{
/**This is used in the same way as getConnections(), but does not use Connection objects.*/
N[] connectionsOf(N node);
/** Returns the unique index of the given node.
* @param node the node whose index will be returned
* @return the unique index of the given node. */
int getIndex (N node);
}

View File

@@ -1,268 +0,0 @@
package io.anuke.mindustry.ai;
import com.badlogic.gdx.ai.pfa.*;
import com.badlogic.gdx.utils.BinaryHeap;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.TimeUtils;
/**An IndexedAStarPathfinder that uses an OptimizedGraph, and therefore has less allocations.*/
public class OptimizedPathFinder<N> implements PathFinder<N> {
OptimizedGraph<N> graph;
IntMap<NodeRecord<N>> records = new IntMap<>();
BinaryHeap<NodeRecord<N>> openList;
NodeRecord<N> current;
/**
* The unique ID for each search run. Used to mark nodes.
*/
private int searchId;
private static final byte UNVISITED = 0;
private static final byte OPEN = 1;
private static final byte CLOSED = 2;
@SuppressWarnings("unchecked")
public OptimizedPathFinder(OptimizedGraph<N> graph) {
this.graph = graph;
this.openList = new BinaryHeap<>();
}
@Override
public boolean searchConnectionPath(N startNode, N endNode, Heuristic<N> heuristic, GraphPath<Connection<N>> outPath) {
return false;
}
@Override
public boolean searchNodePath(N startNode, N endNode, Heuristic<N> heuristic, GraphPath<N> outPath) {
// Perform AStar
boolean found = search(startNode, endNode, heuristic);
if (found) {
// Create a path made of nodes
generateNodePath(startNode, outPath);
}
return found;
}
protected boolean search(N startNode, N endNode, Heuristic<N> heuristic) {
initSearch(startNode, endNode, heuristic);
// Iterate through processing each node
do {
// Retrieve the node with smallest estimated total cost from the open list
current = openList.pop();
current.category = CLOSED;
// Terminate if we reached the goal node
if (current.node == endNode) return true;
visitChildren(endNode, heuristic);
} while (openList.size > 0);
// We've run out of nodes without finding the goal, so there's no solution
return false;
}
@Override
public boolean search(PathFinderRequest<N> request, long timeToRun) {
long lastTime = TimeUtils.nanoTime();
// We have to initialize the search if the status has just changed
if (request.statusChanged) {
initSearch(request.startNode, request.endNode, request.heuristic);
request.statusChanged = false;
}
// Iterate through processing each node
do {
// Check the available time
long currentTime = TimeUtils.nanoTime();
timeToRun -= currentTime - lastTime;
if (timeToRun <= PathFinderQueue.TIME_TOLERANCE) return false;
// Retrieve the node with smallest estimated total cost from the open list
current = openList.pop();
current.category = CLOSED;
// Terminate if we reached the goal node; we've found a path.
if (current.node == request.endNode) {
request.pathFound = true;
generateNodePath(request.startNode, request.resultPath);
return true;
}
// Visit current node's children
visitChildren(request.endNode, request.heuristic);
// Store the current time
lastTime = currentTime;
} while (openList.size > 0);
// The open list is empty and we've not found a path.
request.pathFound = false;
return true;
}
protected void initSearch(N startNode, N endNode, Heuristic<N> heuristic) {
// Increment the search id
if (++searchId < 0) searchId = 1;
// Initialize the open list
openList.clear();
// Initialize the record for the start node and add it to the open list
NodeRecord<N> startRecord = getNodeRecord(startNode);
startRecord.node = startNode;
//startRecord.connection = null;
startRecord.costSoFar = 0;
addToOpenList(startRecord, heuristic.estimate(startNode, endNode));
current = null;
}
protected void visitChildren(N endNode, Heuristic<N> heuristic) {
// Get current node's outgoing connections
//Array<Connection<N>> connections = graph.getConnections(current.node);
N[] conn = graph.connectionsOf(current.node);
// Loop through each connection in turn
for (int i = 0; i < conn.length; i++) {
//Connection<N> connection = connections.get(i)
// Get the cost estimate for the node
N node = conn[i];
if(node == null) continue;
float addCost = heuristic.estimate(current.node, node);
float nodeCost = current.costSoFar + addCost;
float nodeHeuristic;
NodeRecord<N> nodeRecord = getNodeRecord(node);
if (nodeRecord.category == CLOSED) { // The node is closed
// If we didn't find a shorter route, skip
if (nodeRecord.costSoFar <= nodeCost) continue;
// We can use the node's old cost values to calculate its heuristic
// without calling the possibly expensive heuristic function
nodeHeuristic = nodeRecord.getEstimatedTotalCost() - nodeRecord.costSoFar;
} else if (nodeRecord.category == OPEN) { // The node is open
// If our route is no better, then skip
if (nodeRecord.costSoFar <= nodeCost) continue;
// Remove it from the open list (it will be re-added with the new cost)
openList.remove(nodeRecord);
// We can use the node's old cost values to calculate its heuristic
// without calling the possibly expensive heuristic function
nodeHeuristic = nodeRecord.getEstimatedTotalCost() - nodeRecord.costSoFar;
} else { // the node is unvisited
// We'll need to calculate the heuristic value using the function,
// since we don't have a node record with a previously calculated value
nodeHeuristic = heuristic.estimate(node, endNode);
}
// Update node record's cost and connection
nodeRecord.costSoFar = nodeCost;
nodeRecord.from = current.node; //TODO ???
// Add it to the open list with the estimated total cost
addToOpenList(nodeRecord, nodeCost + nodeHeuristic);
}
}
protected void generateNodePath(N startNode, GraphPath<N> outPath) {
// Work back along the path, accumulating nodes
// outPath.clear();
while (current.from != null) {
outPath.add(current.node);
current = records.get(graph.getIndex(current.from));
}
outPath.add(startNode);
// Reverse the path
outPath.reverse();
}
protected void addToOpenList(NodeRecord<N> nodeRecord, float estimatedTotalCost) {
openList.add(nodeRecord, estimatedTotalCost);
nodeRecord.category = OPEN;
}
protected NodeRecord<N> getNodeRecord(N node) {
if(!records.containsKey(graph.getIndex(node))){
NodeRecord<N> record = new NodeRecord<>();
record.node = node;
record.searchId = searchId;
records.put(graph.getIndex(node), record);
return record;
}else{
return records.get(graph.getIndex(node));
}
}
/**
* This nested class is used to keep track of the information we need for each node during the search.
*
* @param <N> Type of node
* @author davebaol
*/
static class NodeRecord<N> extends BinaryHeap.Node {
/**
* The reference to the node.
*/
N node;
N from;
/**
* The incoming connection to the node
*/
//Connection<N> connection;
/**
* The actual cost from the start node.
*/
float costSoFar;
/**
* The node category: {@link #UNVISITED}, {@link #OPEN} or {@link #CLOSED}.
*/
byte category;
/**
* ID of the current search.
*/
int searchId;
/**
* Creates a {@code NodeRecord}.
*/
public NodeRecord() {
super(0);
}
/**
* Returns the estimated total cost.
*/
public float getEstimatedTotalCost() {
return getValue();
}
}
}

View File

@@ -1,248 +0,0 @@
package io.anuke.mindustry.ai;
import com.badlogic.gdx.ai.pfa.PathFinderRequest;
import com.badlogic.gdx.ai.pfa.PathSmoother;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.mindustry.game.SpawnPoint;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Log;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.*;
public class Pathfind{
/**Maximum time taken per frame on pathfinding for a single path.*/
private static final long maxTime = 1000000 * 5;
/**Tile graph, for determining conenctions between two tiles*/
TileGraph graph = new TileGraph();
/**Smoother that removes extra nodes from a path.*/
PathSmoother<Tile, Vector2> smoother = new PathSmoother<Tile, Vector2>(new Raycaster());
/**temporary vector2 for calculations*/
Vector2 vector = new Vector2();
Vector2 v1 = new Vector2();
Vector2 v2 = new Vector2();
Vector2 v3 = new Vector2();
/**Finds the position on the path an enemy should move to.
* If the path is not yet calculated, this returns the enemy's position (i. e. "don't move")
* @param enemy The enemy to find a path for
* @return The position the enemy should move to.*/
public Vector2 find(Enemy enemy){
//TODO fix -1/-2 node usage
if(enemy.node == -1 || enemy.node == -2){
findNode(enemy);
}
if(enemy.node == -2){
enemy.node = -1;
}
if(enemy.node < 0 || world.getSpawns().get(enemy.lane).pathTiles == null){
return vector.set(enemy.x, enemy.y);
}
Tile[] path = world.getSpawns().get(enemy.lane).pathTiles;
if(enemy.node >= path.length){
enemy.node = -1;
return vector.set(enemy.x, enemy.y);
}
if(enemy.node <= -1){
return vector.set(enemy.x, enemy.y);
}
//TODO documentation on what this does
Tile prev = path[enemy.node - 1];
Tile target = path[enemy.node];
//a bridge has been broken, re-path
if(!world.passable(target.x, target.y)){
remakePath();
return vector.set(enemy.x, enemy.y);
}
float projectLen = Vector2.dst(prev.worldx(), prev.worldy(), target.worldx(), target.worldy()) / 6f;
Vector2 projection = projectPoint(prev.worldx(), prev.worldy(),
target.worldx(), target.worldy(), enemy.x, enemy.y);
boolean canProject = true;
if(projectLen < 8 || !onLine(projection, prev.worldx(), prev.worldy(), target.worldx(), target.worldy())){
canProject = false;
}else{
projection.add(v1.set(projectLen, 0).rotate(Angles.angle(prev.worldx(), prev.worldy(),
target.worldx(), target.worldy())));
}
float dst = Vector2.dst(enemy.x, enemy.y, target.worldx(), target.worldy());
float nlinedist = enemy.node >= path.length - 1 ? 9999 :
pointLineDist(path[enemy.node].worldx(), path[enemy.node].worldy(),
path[enemy.node + 1].worldx(), path[enemy.node + 1].worldy(), enemy.x, enemy.y);
if(dst < 8 || nlinedist < 8){
if(enemy.node <= path.length-2)
enemy.node ++;
target = path[enemy.node];
}
if(canProject && projection.dst(enemy.x, enemy.y) < Vector2.dst(target.x, target.y, enemy.x, enemy.y)){
vector.set(projection);
}else{
vector.set(target.worldx(), target.worldy());
}
//near the core, stop
if(enemy.node == path.length - 1){
vector.set(target.worldx(), target.worldy());
}
return vector;
}
/**Re-calculate paths for all enemies. Runs when a path changes while moving.*/
private void remakePath(){
for(int i = 0; i < enemyGroup.size(); i ++){
Enemy enemy = enemyGroup.all().get(i);
enemy.node = -1;
}
resetPaths();
}
/**Update the pathfinders and continue calculating the path if it hasn't been calculated yet.
* This method is run each frame.*/
public void update(){
//go through each spawnpoint, and if it's not found a path yet, update it
for(int i = 0; i < world.getSpawns().size; i ++){
SpawnPoint point = world.getSpawns().get(i);
if(point.request == null || point.finder == null){
continue;
}
if(!point.request.pathFound){
try{
if(point.finder.search(point.request, maxTime)){
smoother.smoothPath(point.path);
point.pathTiles = point.path.nodes.toArray(Tile.class);
point.finder = null;
}
}catch (ArrayIndexOutOfBoundsException e){
//no path
point.request.pathFound = true;
}
}
}
}
//1300-1500ms, usually 1400 unoptimized on Caldera
/**Benchmark pathfinding speed. Debugging stuff.*/
public void benchmark(){
SpawnPoint point = world.getSpawns().first();
int amount = 100;
//warmup
for(int i = 0; i < 100; i ++){
point.finder.searchNodePath(point.start, world.getCore(), state.difficulty.heuristic, point.path);
point.path.clear();
}
Timers.mark();
for(int i = 0; i < amount; i ++){
point.finder.searchNodePath(point.start, world.getCore(), state.difficulty.heuristic, point.path);
point.path.clear();
}
Log.info("Time elapsed: {0}ms\nAverage MS per path: {1}", Timers.elapsed(), Timers.elapsed()/amount);
}
/**Reset and clear the paths.*/
public void resetPaths(){
for(int i = 0; i < world.getSpawns().size; i ++){
resetPathFor(world.getSpawns().get(i));
}
}
private void resetPathFor(SpawnPoint point){
point.finder = new OptimizedPathFinder<>(graph);
point.path.clear();
point.pathTiles = null;
point.request = new PathFinderRequest<>(point.start, world.getCore(), state.difficulty.heuristic, point.path);
point.request.statusChanged = true; //IMPORTANT!
}
/**For an enemy that was just loaded from a save, find the node in the path it should be following.*/
void findNode(Enemy enemy){
if(enemy.lane >= world.getSpawns().size || enemy.lane < 0){
enemy.lane = 0;
}
if(world.getSpawns().get(enemy.lane).pathTiles == null){
return;
}
Tile[] path = world.getSpawns().get(enemy.lane).pathTiles;
int closest = findClosest(path, enemy.x, enemy.y);
closest = Mathf.clamp(closest, 1, path.length-1);
if(closest == -1){
return;
}
enemy.node = closest;
}
/**Finds the closest tile to a position, in an array of tiles.*/
private int findClosest(Tile[] tiles, float x, float y){
int cindex = -2;
float dst = Float.MAX_VALUE;
for(int i = 0; i < tiles.length - 1; i ++){
Tile tile = tiles[i];
Tile next = tiles[i + 1];
float d = pointLineDist(tile.worldx(), tile.worldy(), next.worldx(), next.worldy(), x, y);
if(d < dst){
dst = d;
cindex = i;
}
}
return cindex + 1;
}
/**Returns whether a point is on a line.*/
private boolean onLine(Vector2 vector, float x1, float y1, float x2, float y2){
return MathUtils.isEqual(vector.dst(x1, y1) + vector.dst(x2, y2), Vector2.dst(x1, y1, x2, y2), 0.01f);
}
/**Returns distance from a point to a line segment.*/
private float pointLineDist(float x, float y, float x2, float y2, float px, float py){
float l2 = Vector2.dst2(x, y, x2, y2);
float t = Math.max(0, Math.min(1, Vector2.dot(px - x, py - y, x2 - x, y2 - y) / l2));
Vector2 projection = v1.set(x, y).add(v2.set(x2, y2).sub(x, y).scl(t)); // Projection falls on the segment
return projection.dst(px, py);
}
//TODO documentation
private Vector2 projectPoint(float x1, float y1, float x2, float y2, float pointx, float pointy){
float px = x2-x1, py = y2-y1, dAB = px*px + py*py;
float u = ((pointx - x1) * px + (pointy - y1) * py) / dAB;
float x = x1 + u * px, y = y1 + u * py;
return v3.set(x, y); //this is D
}
}

View File

@@ -0,0 +1,197 @@
package io.anuke.mindustry.ai;
import com.badlogic.gdx.math.GridPoint2;
import com.badlogic.gdx.utils.IntArray;
import com.badlogic.gdx.utils.ObjectSet;
import com.badlogic.gdx.utils.ObjectSet.ObjectSetIterator;
import com.badlogic.gdx.utils.Queue;
import com.badlogic.gdx.utils.TimeUtils;
import io.anuke.mindustry.game.EventType.TileChangeEvent;
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.game.TeamInfo.TeamData;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.meta.BlockFlag;
import io.anuke.ucore.core.Events;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.util.Geometry;
import io.anuke.ucore.util.Log;
import static io.anuke.mindustry.Vars.state;
import static io.anuke.mindustry.Vars.world;
public class Pathfinder{
private long maxUpdate = TimeUtils.millisToNanos(4);
private PathData[] paths;
private IntArray blocked = new IntArray();
public Pathfinder(){
Events.on(WorldLoadEvent.class, this::clear);
Events.on(TileChangeEvent.class, tile -> {
for(TeamData data : state.teams.getTeams()){
if(data.team != tile.getTeam() && paths[data.team.ordinal()].weights[tile.x][tile.y] >= Float.MAX_VALUE){
update(tile, data.team);
}
}
update(tile, tile.getTeam());
});
}
public void update(){
ObjectSetIterator<TeamData> iterator = new ObjectSetIterator<>(state.teams.getTeams());
for(TeamData team : iterator){
updateFrontier(team.team, maxUpdate);
}
}
public Tile getTargetTile(Team team, Tile tile){
float[][] values = paths[team.ordinal()].weights;
if(values == null) return tile;
float value = values[tile.x][tile.y];
Tile target = null;
float tl = 0f;
for(GridPoint2 point : Geometry.d8){
int dx = tile.x + point.x, dy = tile.y + point.y;
Tile other = world.tile(dx, dy);
if(other == null) continue;
if(values[dx][dy] < value && (target == null || values[dx][dy] < tl) &&
!other.solid() &&
!(point.x != 0 && point.y != 0 && (world.solid(tile.x + point.x, tile.y) || world.solid(tile.x, tile.y + point.y)))){ //diagonal corner trap
target = other;
tl = values[dx][dy];
}
}
if(target == null || tl == Float.MAX_VALUE) return tile;
return target;
}
public float getDebugValue(int x, int y){
return paths[Team.red.ordinal()].weights[x][y];
}
public float getValueforTeam(Team team, int x, int y){
return paths == null ? 0 : paths[team.ordinal()].weights[x][y];
}
private boolean passable(Tile tile, Team team){
return (tile.getWallID() == 0 && !tile.floor().isLiquid && tile.cliffs == 0 && !tile.floor().solid && !(tile.floor().isLiquid && (tile.floor().damageTaken > 0 || tile.floor().drownTime > 0)))
|| (tile.breakable() && (tile.getTeam() != team)) || !tile.solid();
}
private void update(Tile tile, Team team){
if(paths[team.ordinal()] != null){
PathData path = paths[team.ordinal()];
if(!passable(tile, team)){
path.weights[tile.x][tile.y] = Float.MAX_VALUE;
}
path.search++;
if(path.lastSearchTime + 1000 / 60 * 3 > TimeUtils.millis()){
path.frontier.clear();
}
path.lastSearchTime = TimeUtils.millis();
ObjectSet<Tile> set = world.indexer().getEnemy(team, BlockFlag.target);
for(Tile other : set){
path.weights[other.x][other.y] = 0;
path.searches[other.x][other.y] = path.search;
path.frontier.addFirst(other);
}
}
}
private void createFor(Team team){
PathData path = new PathData();
path.search++;
path.frontier.ensureCapacity((world.width() + world.height()) * 3);
paths[team.ordinal()] = path;
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
Tile tile = world.tile(x, y);
if(tile.block().flags != null && state.teams.areEnemies(tile.getTeam(), team)
&& tile.block().flags.contains(BlockFlag.target)){
path.frontier.addFirst(tile);
path.weights[x][y] = 0;
path.searches[x][y] = path.search;
}else{
path.weights[x][y] = Float.MAX_VALUE;
}
}
}
updateFrontier(team, -1);
}
private void updateFrontier(Team team, long nsToRun){
PathData path = paths[team.ordinal()];
long start = TimeUtils.nanoTime();
while(path.frontier.size > 0 && (nsToRun < 0 || TimeUtils.timeSinceNanos(start) <= nsToRun)){
Tile tile = path.frontier.removeLast();
float cost = path.weights[tile.x][tile.y];
if(cost < Float.MAX_VALUE){
for(GridPoint2 point : Geometry.d4){
int dx = tile.x + point.x, dy = tile.y + point.y;
Tile other = world.tile(dx, dy);
if(other != null && (path.weights[dx][dy] > cost + 1 || path.searches[dx][dy] < path.search)
&& passable(other, team)){
path.frontier.addFirst(world.tile(dx, dy));
path.weights[dx][dy] = cost + other.cost / 2f;
path.searches[dx][dy] = path.search;
}
}
}
}
}
private void clear(){
Timers.mark();
paths = new PathData[Team.all.length];
blocked.clear();
for(TeamData data : state.teams.getTeams()){
PathData path = new PathData();
paths[data.team.ordinal()] = path;
createFor(data.team);
}
state.spawner.checkAllQuadrants();
Log.info("Elapsed calculation time: {0}", Timers.elapsed());
}
class PathData{
float[][] weights;
int[][] searches;
int search = 0;
long lastSearchTime;
Queue<Tile> frontier = new Queue<>();
PathData(){
weights = new float[world.width()][world.height()];
searches = new int[world.width()][world.height()];
}
}
}

View File

@@ -1,87 +0,0 @@
package io.anuke.mindustry.ai;
import com.badlogic.gdx.ai.utils.Collision;
import com.badlogic.gdx.ai.utils.Ray;
import com.badlogic.gdx.ai.utils.RaycastCollisionDetector;
import com.badlogic.gdx.math.Vector2;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.util.Geometry;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.tilesize;
import static io.anuke.mindustry.Vars.world;
public class Raycaster implements RaycastCollisionDetector<Vector2>{
private boolean found = false;
@Override
public boolean collides(Ray<Vector2> ray){
found = false;
Geometry.iterateLine(0f, ray.start.x, ray.start.y, ray.end.x, ray.end.y, tilesize, (x, y)->{
if(solid(x, y)){
found = true;
return;
}
});
return found;
}
@Override
public boolean findCollision(Collision<Vector2> collision, Ray<Vector2> ray){
Vector2 v = vectorCast(ray.start.x, ray.start.y, ray.end.x, ray.end.y);
if(v == null) return false;
collision.point = v;
collision.normal = v.nor();
return true;
}
Vector2 vectorCast(float x0f, float y0f, float x1f, float y1f){
int x0 = (int)x0f;
int y0 = (int)y0f;
int x1 = (int)x1f;
int y1 = (int)y1f;
int dx = Math.abs(x1 - x0);
int dy = Math.abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx - dy;
int e2;
while(true){
if(solid(x0, y0)){
return new Vector2(x0, y0);
}
if(x0 == x1 && y0 == y1) break;
e2 = 2 * err;
if(e2 > -dy){
err = err - dy;
x0 = x0 + sx;
}
if(e2 < dx){
err = err + dx;
y0 = y0 + sy;
}
}
return null;
}
private boolean solid(float x, float y){
Tile tile = world.tile(Mathf.scl2(x, tilesize), Mathf.scl2(y, tilesize));
if(tile == null || tile.solid()) return true;
for(int i = 0; i < 4; i ++){
Tile near = tile.getNearby(i);
if(near == null || near.solid()) return true;
}
return false;
}
}

View File

@@ -1,32 +0,0 @@
package io.anuke.mindustry.ai;
import com.badlogic.gdx.ai.pfa.DefaultGraphPath;
import com.badlogic.gdx.ai.pfa.SmoothableGraphPath;
import com.badlogic.gdx.math.Vector2;
import io.anuke.mindustry.world.Tile;
public class SmoothGraphPath extends DefaultGraphPath<Tile> implements SmoothableGraphPath<Tile, Vector2>{
private Vector2 vector = new Vector2();
@Override
public Vector2 getNodePosition(int index){
Tile tile = nodes.get(index);
return vector.set(tile.worldx(), tile.worldy());
}
@Override
public void swapNodes(int index1, int index2){
nodes.swap(index1, index2);
}
@Override
public void truncatePath(int newLength){
nodes.truncate(newLength);
}
@Override
public void add (Tile node) {
nodes.add(node);
}
}

View File

@@ -1,25 +0,0 @@
package io.anuke.mindustry.ai;
import io.anuke.mindustry.world.Tile;
/**Tilegraph that ignores player-made tiles.*/
public class TileGraph implements OptimizedGraph<Tile> {
private Tile[] tiles = new Tile[4];
/**Used for the OptimizedPathFinder implementation.*/
@Override
public Tile[] connectionsOf(Tile node){
Tile[] nodes = node.getNearby(tiles);
for(int i = 0; i < 4; i ++){
if(nodes[i] != null && !nodes[i].passable()){
nodes[i] = null;
}
}
return nodes;
}
@Override
public int getIndex(Tile node){
return node.packedPosition();
}
}

View File

@@ -0,0 +1,229 @@
package io.anuke.mindustry.ai;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Bits;
import io.anuke.mindustry.entities.units.BaseUnit;
import io.anuke.mindustry.entities.units.Squad;
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
import io.anuke.mindustry.game.SpawnGroup;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.game.WaveCreator;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Events;
import io.anuke.ucore.util.Mathf;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import static io.anuke.mindustry.Vars.*;
public class WaveSpawner{
private static final int quadsize = 4;
private Bits quadrants;
private Array<SpawnGroup> groups;
private Array<FlyerSpawn> flySpawns = new Array<>();
private Array<GroundSpawn> groundSpawns = new Array<>();
public WaveSpawner(){
Events.on(WorldLoadEvent.class, this::reset);
}
public void write(DataOutput output) throws IOException{
output.writeShort(flySpawns.size);
for(FlyerSpawn spawn : flySpawns){
output.writeFloat(spawn.angle);
}
output.writeShort(groundSpawns.size);
for(GroundSpawn spawn : groundSpawns){
output.writeShort((short) spawn.x);
output.writeShort((short) spawn.y);
}
}
public void read(DataInput input) throws IOException{
short flya = input.readShort();
for(int i = 0; i < flya; i++){
FlyerSpawn spawn = new FlyerSpawn();
spawn.angle = input.readFloat();
flySpawns.add(spawn);
}
short grounda = input.readShort();
for(int i = 0; i < grounda; i++){
GroundSpawn spawn = new GroundSpawn();
spawn.x = input.readShort();
spawn.y = input.readShort();
groundSpawns.add(spawn);
}
}
public void spawnEnemies(){
int flyGroups = 0;
int groundGroups = 0;
//count total subgroups spawned by flying/group types
for(SpawnGroup group : groups){
int amount = group.getGroupsSpawned(state.wave);
if(group.type.isFlying){
flyGroups += amount;
}else{
groundGroups += amount;
}
}
int addGround = groundGroups - groundSpawns.size, addFly = flyGroups - flySpawns.size;
//add extra groups if the total exceeds it
for(int i = 0; i < addGround; i++){
GroundSpawn spawn = new GroundSpawn();
findLocation(spawn);
groundSpawns.add(spawn);
}
for(int i = 0; i < addFly; i++){
FlyerSpawn spawn = new FlyerSpawn();
findLocation(spawn);
flySpawns.add(spawn);
}
//store index of last used fly/ground spawn locations
int flyCount = 0, groundCount = 0;
for(SpawnGroup group : groups){
int groups = group.getGroupsSpawned(state.wave);
int spawned = group.getUnitsSpawned(state.wave);
for(int i = 0; i < groups; i++){
Squad squad = new Squad();
float spawnX, spawnY;
float spread;
if(group.type.isFlying){
FlyerSpawn spawn = flySpawns.get(flyCount);
//TODO verify flyer spawn
float margin = 40f; //how far away from the edge flying units spawn
spawnX = world.width() * tilesize / 2f + Mathf.sqrwavex(spawn.angle) * (world.width() / 2f * tilesize + margin);
spawnY = world.height() * tilesize / 2f + Mathf.sqrwavey(spawn.angle) * (world.height() / 2f * tilesize + margin);
spread = margin / 1.5f;
flyCount++;
}else{
GroundSpawn spawn = groundSpawns.get(groundCount);
checkQuadrant(spawn.x, spawn.y);
if(!getQuad(spawn.x, spawn.y)){
findLocation(spawn);
}
spawnX = spawn.x * quadsize * tilesize + quadsize * tilesize / 2f;
spawnY = spawn.y * quadsize * tilesize + quadsize * tilesize / 2f;
spread = quadsize * tilesize / 3f;
groundCount++;
}
for(int j = 0; j < spawned; j++){
BaseUnit unit = group.createUnit(Team.red);
unit.setWave();
unit.setSquad(squad);
unit.set(spawnX + Mathf.range(spread), spawnY + Mathf.range(spread));
unit.add();
}
}
}
}
public void checkAllQuadrants(){
for(int x = 0; x < quadWidth(); x++){
for(int y = 0; y < quadHeight(); y++){
checkQuadrant(x, y);
}
}
}
private void checkQuadrant(int quadx, int quady){
setQuad(quadx, quady, true);
outer:
for(int x = quadx * quadsize; x < world.width() && x < (quadx + 1) * quadsize; x++){
for(int y = quady * quadsize; y < world.height() && y < (quady + 1) * quadsize; y++){
Tile tile = world.tile(x, y);
if(tile == null || tile.solid() || world.pathfinder().getValueforTeam(Team.red, x, y) == Float.MAX_VALUE){
setQuad(quadx, quady, false);
break outer;
}
}
}
}
private void reset(){
flySpawns.clear();
groundSpawns.clear();
quadrants = new Bits(quadWidth() * quadHeight());
if(groups == null){
groups = WaveCreator.getSpawns();
}
}
private boolean getQuad(int quadx, int quady){
return quadrants.get(quadx + quady * quadWidth());
}
private void setQuad(int quadx, int quady, boolean valid){
if(valid){
quadrants.set(quadx + quady * quadWidth());
}else{
quadrants.clear(quadx + quady * quadWidth());
}
}
//TODO instead of randomly scattering locations around the map, find spawns close to each other
private void findLocation(GroundSpawn spawn){
spawn.x = -1;
spawn.y = -1;
int shellWidth = quadWidth() * 2 + quadHeight() * 2 * 6;
shellWidth = Math.min(quadWidth() * quadHeight() / 4, shellWidth);
Mathf.traverseSpiral(quadWidth(), quadHeight(), Mathf.random(shellWidth), (x, y) -> {
if(getQuad(x, y)){
spawn.x = x;
spawn.y = y;
return true;
}
return false;
});
}
//TODO instead of randomly scattering locations around the map, find spawns close to each other
private void findLocation(FlyerSpawn spawn){
spawn.angle = Mathf.random(360f);
}
private int quadWidth(){
return Mathf.ceil(world.width() / (float) quadsize);
}
private int quadHeight(){
return Mathf.ceil(world.height() / (float) quadsize);
}
private class FlyerSpawn{
//square angle
float angle;
}
private class GroundSpawn{
//quadrant spawn coordinates
int x, y;
}
}

View File

@@ -0,0 +1,204 @@
package io.anuke.mindustry.content;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.content.bullets.*;
import io.anuke.mindustry.content.fx.BulletFx;
import io.anuke.mindustry.content.fx.Fx;
import io.anuke.mindustry.content.fx.ShootFx;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.AmmoType;
import io.anuke.mindustry.type.ContentList;
public class AmmoTypes implements ContentList{
public static AmmoType bulletTungsten, bulletLead, bulletCarbide, bulletThorium, bulletSilicon, bulletPyratite,
shotgunTungsten, bombExplosive, bombIncendiary, bombOil, shellCarbide, flamerThermite, weaponMissile,
flakLead, flakExplosive, flakPlastic, flakSurge, missileExplosive, missileIncindiary, missileSurge,
artilleryCarbide, artilleryPlastic, artilleryHoming, artilleryIncindiary, artilleryExplosive,
basicFlame, lancerLaser, lightning, spectreLaser, meltdownLaser, fuseShotgun, oil, water, lava, cryofluid;
@Override
public void load(){
//weapon specific
shotgunTungsten = new AmmoType(Items.tungsten, WeaponBullets.tungstenShotgun, 2){{
shootEffect = ShootFx.shootBig;
smokeEffect = ShootFx.shootBigSmoke;
recoil = 1f;
}};
shellCarbide = new AmmoType(Items.carbide, WeaponBullets.shellCarbide, 2){{
shootEffect = ShootFx.shootBig;
smokeEffect = ShootFx.shootBigSmoke;
}};
bombExplosive = new AmmoType(Items.blastCompound, WeaponBullets.bombExplosive, 3){{
shootEffect = Fx.none;
smokeEffect = Fx.none;
}};
bombIncendiary = new AmmoType(Items.pyratite, WeaponBullets.bombIncendiary, 3){{
shootEffect = Fx.none;
smokeEffect = Fx.none;
}};
bombOil = new AmmoType(Items.coal, WeaponBullets.bombOil, 3){{
shootEffect = Fx.none;
smokeEffect = Fx.none;
}};
flamerThermite = new AmmoType(Items.pyratite, TurretBullets.basicFlame, 3){{
shootEffect = ShootFx.shootSmallFlame;
}};
weaponMissile = new AmmoType(Items.carbide, MissileBullets.javelin, 2){{
shootEffect = BulletFx.hitBulletSmall;
smokeEffect = Fx.none;
reloadMultiplier = 1.2f;
}};
//bullets
bulletLead = new AmmoType(Items.lead, StandardBullets.lead, 5){{
shootEffect = ShootFx.shootSmall;
smokeEffect = ShootFx.shootSmallSmoke;
reloadMultiplier = 1.6f;
inaccuracy = 5f;
}};
bulletTungsten = new AmmoType(Items.tungsten, StandardBullets.tungsten, 2){{
shootEffect = ShootFx.shootSmall;
smokeEffect = ShootFx.shootSmallSmoke;
reloadMultiplier = 0.8f;
}};
bulletCarbide = new AmmoType(Items.carbide, StandardBullets.carbide, 2){{
shootEffect = ShootFx.shootSmall;
smokeEffect = ShootFx.shootSmallSmoke;
reloadMultiplier = 0.6f;
}};
bulletThorium = new AmmoType(Items.thorium, StandardBullets.thorium, 2){{
shootEffect = ShootFx.shootBig;
smokeEffect = ShootFx.shootBigSmoke;
}};
bulletSilicon = new AmmoType(Items.silicon, StandardBullets.homing, 5){{
shootEffect = ShootFx.shootSmall;
smokeEffect = ShootFx.shootSmallSmoke;
reloadMultiplier = 1.4f;
}};
bulletPyratite = new AmmoType(Items.pyratite, StandardBullets.tracer, 3){{
shootEffect = ShootFx.shootSmall;
smokeEffect = ShootFx.shootSmallSmoke;
inaccuracy = 3f;
}};
//flak
flakLead = new AmmoType(Items.lead, FlakBullets.lead, 5){{
shootEffect = ShootFx.shootSmall;
smokeEffect = ShootFx.shootSmallSmoke;
}};
flakExplosive = new AmmoType(Items.blastCompound, FlakBullets.explosive, 5){{
shootEffect = ShootFx.shootSmall;
smokeEffect = ShootFx.shootSmallSmoke;
}};
flakPlastic = new AmmoType(Items.plastanium, FlakBullets.plastic, 5){{
shootEffect = ShootFx.shootSmall;
smokeEffect = ShootFx.shootSmallSmoke;
}};
flakSurge = new AmmoType(Items.surgealloy, FlakBullets.surge, 5){{
shootEffect = ShootFx.shootSmall;
smokeEffect = ShootFx.shootSmallSmoke;
}};
//missiles
missileExplosive = new AmmoType(Items.blastCompound, MissileBullets.explosive, 1){{
shootEffect = ShootFx.shootBig2;
smokeEffect = ShootFx.shootBigSmoke2;
reloadMultiplier = 1.2f;
}};
missileIncindiary = new AmmoType(Items.pyratite, MissileBullets.incindiary, 1){{
shootEffect = ShootFx.shootBig2;
smokeEffect = ShootFx.shootBigSmoke2;
reloadMultiplier = 1.0f;
}};
missileSurge = new AmmoType(Items.surgealloy, MissileBullets.surge, 1){{
shootEffect = ShootFx.shootBig2;
smokeEffect = ShootFx.shootBigSmoke2;
}};
//artillery
artilleryCarbide = new AmmoType(Items.carbide, ArtilleryBullets.carbide, 2){{
shootEffect = ShootFx.shootBig2;
smokeEffect = ShootFx.shootBigSmoke2;
}};
artilleryPlastic = new AmmoType(Items.plastanium, ArtilleryBullets.plastic, 2){{
shootEffect = ShootFx.shootBig2;
smokeEffect = ShootFx.shootBigSmoke2;
reloadMultiplier = 1.4f;
}};
artilleryHoming = new AmmoType(Items.silicon, ArtilleryBullets.homing, 1){{
shootEffect = ShootFx.shootBig2;
smokeEffect = ShootFx.shootBigSmoke2;
reloadMultiplier = 0.9f;
}};
artilleryIncindiary = new AmmoType(Items.pyratite, ArtilleryBullets.incindiary, 2){{
shootEffect = ShootFx.shootBig2;
smokeEffect = ShootFx.shootBigSmoke2;
reloadMultiplier = 1.2f;
}};
artilleryExplosive = new AmmoType(Items.blastCompound, ArtilleryBullets.explosive, 1){{
shootEffect = ShootFx.shootBig2;
smokeEffect = ShootFx.shootBigSmoke2;
reloadMultiplier = 1.6f;
}};
//flame
basicFlame = new AmmoType(Liquids.oil, TurretBullets.basicFlame, 0.3f){{
shootEffect = ShootFx.shootSmallFlame;
}};
//power
lancerLaser = new AmmoType(TurretBullets.lancerLaser);
lightning = new AmmoType(TurretBullets.lightning);
spectreLaser = new AmmoType(TurretBullets.lancerLaser);
meltdownLaser = new AmmoType(TurretBullets.lancerLaser);
fuseShotgun = new AmmoType(Items.tungsten, TurretBullets.fuseShot, 0.1f);
//liquid
oil = new AmmoType(Liquids.oil, TurretBullets.oilShot, 0.3f);
water = new AmmoType(Liquids.water, TurretBullets.waterShot, 0.3f);
lava = new AmmoType(Liquids.lava, TurretBullets.lavaShot, 0.3f);
cryofluid = new AmmoType(Liquids.cryofluid, TurretBullets.cryoShot, 0.3f);
}
@Override
public Array<? extends Content> getAll(){
return AmmoType.all();
}
}

View File

@@ -0,0 +1,102 @@
package io.anuke.mindustry.content;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.ItemType;
public class Items implements ContentList{
public static Item stone, tungsten, lead, coal, carbide, titanium, thorium, silicon, plastanium, phasematter, surgealloy,
biomatter, sand, blastCompound, pyratite;
@Override
public void load(){
stone = new Item("stone", Color.valueOf("777777")){{
hardness = 3;
}};
tungsten = new Item("tungsten", Color.valueOf("a0b0c8")){{
type = ItemType.material;
hardness = 1;
cost = 0.75f;
}};
lead = new Item("lead", Color.valueOf("8e85a2")){{
type = ItemType.material;
hardness = 1;
cost = 0.6f;
}};
coal = new Item("coal", Color.valueOf("272727")){{
explosiveness = 0.2f;
flammability = 0.5f;
hardness = 2;
}};
carbide = new Item("carbide", Color.valueOf("e2e2e2")){{
type = ItemType.material;
}};
titanium = new Item("titanium", Color.valueOf("8da1e3")){{
type = ItemType.material;
hardness = 3;
cost = 1.1f;
}};
thorium = new Item("thorium", Color.valueOf("f9a3c7")){{
type = ItemType.material;
explosiveness = 0.1f;
hardness = 4;
radioactivity = 0.5f;
cost = 1.2f;
}};
silicon = new Item("silicon", Color.valueOf("53565c")){{
type = ItemType.material;
cost = 0.9f;
}};
plastanium = new Item("plastanium", Color.valueOf("e9ead3")){{
type = ItemType.material;
flammability = 0.1f;
explosiveness = 0.1f;
cost = 1.5f;
}};
phasematter = new Item("phase-matter", Color.valueOf("f4ba6e")){{
type = ItemType.material;
cost = 1.5f;
}};
surgealloy = new Item("surge-alloy", Color.valueOf("b4d5c7")){{
type = ItemType.material;
}};
biomatter = new Item("biomatter", Color.valueOf("648b55")){{
flammability = 0.4f;
fluxiness = 0.2f;
}};
sand = new Item("sand", Color.valueOf("e3d39e")){{
fluxiness = 0.5f;
}};
blastCompound = new Item("blast-compound", Color.valueOf("ff795e")){{
flammability = 0.2f;
explosiveness = 0.6f;
}};
pyratite = new Item("pyratite", Color.valueOf("ffaa5f")){{
flammability = 0.7f;
explosiveness = 0.2f;
}};
}
@Override
public Array<? extends Content> getAll(){
return Item.all();
}
}

View File

@@ -0,0 +1,56 @@
package io.anuke.mindustry.content;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.type.Liquid;
public class Liquids implements ContentList{
public static Liquid water, lava, oil, cryofluid;
@Override
public void load(){
water = new Liquid("water", Color.valueOf("486acd")){
{
heatCapacity = 0.4f;
tier = 0;
effect = StatusEffects.wet;
}
};
lava = new Liquid("lava", Color.valueOf("e37341")){
{
temperature = 0.8f;
viscosity = 0.8f;
tier = 2;
effect = StatusEffects.melting;
}
};
oil = new Liquid("oil", Color.valueOf("313131")){
{
viscosity = 0.7f;
flammability = 0.6f;
explosiveness = 0.6f;
tier = 1;
effect = StatusEffects.tarred;
}
};
cryofluid = new Liquid("cryofluid", Color.SKY){
{
heatCapacity = 0.9f;
temperature = 0.25f;
tier = 1;
effect = StatusEffects.freezing;
}
};
}
@Override
public Array<? extends Content> getAll(){
return Liquid.all();
}
}

View File

@@ -0,0 +1,93 @@
package io.anuke.mindustry.content;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.type.Mech;
import io.anuke.mindustry.type.Upgrade;
public class Mechs implements ContentList{
public static Mech alpha, delta, tau, omega, dart, javelin, trident, halberd;
/**
* These are not new mechs, just re-assignments for convenience.
*/
public static Mech starterDesktop, starterMobile;
@Override
public void load(){
alpha = new Mech("alpha-mech", false){{
drillPower = 1;
speed = 0.5f;
weapon = Weapons.blaster;
trailColor = Palette.lightTrail;
}};
delta = new Mech("delta-mech", false){{
drillPower = -1;
speed = 0.63f;
boostSpeed = 0.86f;
itemCapacity = 15;
armor = 30f;
weaponOffsetX = -1;
weaponOffsetY = -1;
weapon = Weapons.shockgun;
ammoCapacity = 50;
trailColor = Color.valueOf("d3ddff");
}};
tau = new Mech("tau-mech", false){{
drillPower = 2;
speed = 0.5f;
}};
omega = new Mech("omega-mech", false){{
drillPower = 1;
speed = 0.4f;
}};
dart = new Mech("dart-ship", true){{
drillPower = 1;
speed = 0.4f;
maxSpeed = 3f;
drag = 0.1f;
weaponOffsetX = -1;
weaponOffsetY = -1;
trailColor = Palette.lightTrail;
}};
javelin = new Mech("javelin-ship", true){{
drillPower = -1;
speed = 0.4f;
maxSpeed = 3.6f;
drag = 0.09f;
weapon = Weapons.missiles;
trailColor = Color.valueOf("d3ddff");
}};
trident = new Mech("trident-ship", true){{
drillPower = 1;
speed = 0.4f;
maxSpeed = 3f;
drag = 0.1f;
}};
halberd = new Mech("halberd-ship", true){{
drillPower = 2;
speed = 0.4f;
maxSpeed = 3f;
drag = 0.1f;
}};
starterDesktop = alpha;
starterMobile = dart;
}
@Override
public Array<? extends Content> getAll(){
return Upgrade.all();
}
}

View File

@@ -0,0 +1,287 @@
package io.anuke.mindustry.content;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.content.blocks.*;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.type.ItemStack;
import io.anuke.mindustry.type.Recipe;
import static io.anuke.mindustry.type.Category.*;
public class Recipes implements ContentList{
@Override
public void load(){
//WALLS
new Recipe(defense, DefenseBlocks.tungstenWall, new ItemStack(Items.tungsten, 12));
new Recipe(defense, DefenseBlocks.tungstenWallLarge, new ItemStack(Items.tungsten, 12 * 4));
new Recipe(defense, DefenseBlocks.carbideWall, new ItemStack(Items.carbide, 12));
new Recipe(defense, DefenseBlocks.carbideWallLarge, new ItemStack(Items.carbide, 12 * 4));
new Recipe(defense, DefenseBlocks.thoriumWall, new ItemStack(Items.thorium, 12));
new Recipe(defense, DefenseBlocks.thoriumWallLarge, new ItemStack(Items.thorium, 12 * 4));
new Recipe(defense, DefenseBlocks.door, new ItemStack(Items.carbide, 12), new ItemStack(Items.silicon, 8));
new Recipe(defense, DefenseBlocks.doorLarge, new ItemStack(Items.carbide, 12 * 4), new ItemStack(Items.silicon, 8 * 4));
//TURRETS
new Recipe(weapon, TurretBlocks.duo, new ItemStack(Items.tungsten, 40));
new Recipe(weapon, TurretBlocks.scorch, new ItemStack(Items.tungsten, 50), new ItemStack(Items.carbide, 20));
new Recipe(weapon, TurretBlocks.hail, new ItemStack(Items.tungsten, 60), new ItemStack(Items.carbide, 35));
new Recipe(weapon, TurretBlocks.lancer, new ItemStack(Items.tungsten, 50), new ItemStack(Items.lead, 100), new ItemStack(Items.silicon, 90));
new Recipe(weapon, TurretBlocks.wave, new ItemStack(Items.carbide, 60), new ItemStack(Items.titanium, 70), new ItemStack(Items.lead, 150));
new Recipe(weapon, TurretBlocks.swarmer, new ItemStack(Items.carbide, 70), new ItemStack(Items.titanium, 70), new ItemStack(Items.plastanium, 90), new ItemStack(Items.silicon, 60));
new Recipe(weapon, TurretBlocks.salvo, new ItemStack(Items.tungsten, 210), new ItemStack(Items.carbide, 190), new ItemStack(Items.thorium, 130));
new Recipe(weapon, TurretBlocks.ripple, new ItemStack(Items.tungsten, 300), new ItemStack(Items.carbide, 220), new ItemStack(Items.thorium, 120));
//DISTRIBUTION
new Recipe(distribution, DistributionBlocks.conveyor, new ItemStack(Items.lead, 1));
new Recipe(distribution, DistributionBlocks.titaniumconveyor, new ItemStack(Items.lead, 2), new ItemStack(Items.titanium, 1));
new Recipe(distribution, DistributionBlocks.phaseConveyor, new ItemStack(Items.phasematter, 10), new ItemStack(Items.silicon, 15), new ItemStack(Items.lead, 20), new ItemStack(Items.carbide, 20));
//starter lead transporation
new Recipe(distribution, DistributionBlocks.junction, new ItemStack(Items.lead, 2));
new Recipe(distribution, DistributionBlocks.splitter, new ItemStack(Items.lead, 6));
//advanced carbide transporation
//new Recipe(distribution, DistributionBlocks.splitter, new ItemStack(Items.carbide, 2), new ItemStack(Items.tungsten, 2));
new Recipe(distribution, DistributionBlocks.distributor, new ItemStack(Items.carbide, 8), new ItemStack(Items.tungsten, 8));
new Recipe(distribution, DistributionBlocks.sorter, new ItemStack(Items.carbide, 4), new ItemStack(Items.tungsten, 4));
new Recipe(distribution, DistributionBlocks.overflowGate, new ItemStack(Items.carbide, 4), new ItemStack(Items.tungsten, 8));
new Recipe(distribution, DistributionBlocks.bridgeConveyor, new ItemStack(Items.carbide, 8), new ItemStack(Items.tungsten, 8));
//CRAFTING
//smelting
new Recipe(crafting, CraftingBlocks.smelter, new ItemStack(Items.tungsten, 70));
new Recipe(crafting, CraftingBlocks.arcsmelter, new ItemStack(Items.tungsten, 90), new ItemStack(Items.carbide, 60), new ItemStack(Items.lead, 50));
new Recipe(crafting, CraftingBlocks.siliconsmelter, new ItemStack(Items.tungsten, 60), new ItemStack(Items.lead, 50));
//advanced fabrication
new Recipe(crafting, CraftingBlocks.plastaniumCompressor, new ItemStack(Items.silicon, 160), new ItemStack(Items.lead, 230), new ItemStack(Items.carbide, 120), new ItemStack(Items.titanium, 160));
new Recipe(crafting, CraftingBlocks.phaseWeaver, new ItemStack(Items.silicon, 260), new ItemStack(Items.lead, 240), new ItemStack(Items.thorium, 150));
//TODO implement alloy smelter
//new Recipe(crafting, CraftingBlocks.alloySmelter, new ItemStack(Items.silicon, 160), new ItemStack(Items.lead, 160), new ItemStack(Items.thorium, 140));
//misc
new Recipe(crafting, CraftingBlocks.pulverizer, new ItemStack(Items.tungsten, 60), new ItemStack(Items.lead, 50));
new Recipe(crafting, CraftingBlocks.pyratiteMixer, new ItemStack(Items.tungsten, 100), new ItemStack(Items.lead, 50));
new Recipe(crafting, CraftingBlocks.blastMixer, new ItemStack(Items.lead, 60), new ItemStack(Items.carbide, 40));
new Recipe(crafting, CraftingBlocks.cryofluidmixer, new ItemStack(Items.lead, 130), new ItemStack(Items.silicon, 80), new ItemStack(Items.thorium, 90));
new Recipe(crafting, CraftingBlocks.solidifier, new ItemStack(Items.carbide, 30), new ItemStack(Items.tungsten, 20));
new Recipe(crafting, CraftingBlocks.melter, new ItemStack(Items.tungsten, 60), new ItemStack(Items.lead, 70), new ItemStack(Items.carbide, 90));
new Recipe(crafting, CraftingBlocks.incinerator, new ItemStack(Items.carbide, 10), new ItemStack(Items.lead, 30));
//processing
new Recipe(crafting, CraftingBlocks.biomatterCompressor, new ItemStack(Items.lead, 70), new ItemStack(Items.silicon, 60));
new Recipe(crafting, CraftingBlocks.separator, new ItemStack(Items.tungsten, 60), new ItemStack(Items.carbide, 50));
new Recipe(crafting, CraftingBlocks.centrifuge, new ItemStack(Items.tungsten, 130), new ItemStack(Items.carbide, 130), new ItemStack(Items.silicon, 60), new ItemStack(Items.titanium, 50));
//POWER
new Recipe(power, PowerBlocks.powerNode, new ItemStack(Items.tungsten, 2), new ItemStack(Items.lead, 6))
.setDependencies(PowerBlocks.combustionGenerator);
new Recipe(power, PowerBlocks.powerNodeLarge, new ItemStack(Items.carbide, 10), new ItemStack(Items.lead, 20), new ItemStack(Items.silicon, 6))
.setDependencies(PowerBlocks.powerNode);
new Recipe(power, PowerBlocks.battery, new ItemStack(Items.tungsten, 8), new ItemStack(Items.lead, 30), new ItemStack(Items.silicon, 4))
.setDependencies(PowerBlocks.powerNode);
new Recipe(power, PowerBlocks.batteryLarge, new ItemStack(Items.carbide, 40), new ItemStack(Items.lead, 80), new ItemStack(Items.silicon, 30))
.setDependencies(PowerBlocks.powerNode);
//generators - combustion
new Recipe(power, PowerBlocks.combustionGenerator, new ItemStack(Items.tungsten, 50), new ItemStack(Items.lead, 30));
new Recipe(power, PowerBlocks.turbineGenerator, new ItemStack(Items.tungsten, 70), new ItemStack(Items.carbide, 50), new ItemStack(Items.lead, 80), new ItemStack(Items.silicon, 60));
//generators - solar
new Recipe(power, PowerBlocks.solarPanel, new ItemStack(Items.lead, 20), new ItemStack(Items.silicon, 30));
new Recipe(power, PowerBlocks.largeSolarPanel, new ItemStack(Items.lead, 200), new ItemStack(Items.silicon, 290), new ItemStack(Items.phasematter, 30));
//generators - other
new Recipe(power, PowerBlocks.nuclearReactor, new ItemStack(Items.lead, 600), new ItemStack(Items.silicon, 400), new ItemStack(Items.carbide, 300), new ItemStack(Items.thorium, 300));
//new Recipe(distribution, StorageBlocks.core, new ItemStack(Items.carbide, 50));
new Recipe(distribution, StorageBlocks.unloader, new ItemStack(Items.carbide, 40), new ItemStack(Items.silicon, 50));
new Recipe(distribution, StorageBlocks.sortedunloader, new ItemStack(Items.carbide, 40), new ItemStack(Items.silicon, 70));
new Recipe(distribution, StorageBlocks.vault, new ItemStack(Items.carbide, 500), new ItemStack(Items.thorium, 350));
//DRILLS, PRODUCERS
new Recipe(production, ProductionBlocks.tungstenDrill, new ItemStack(Items.tungsten, 25));
new Recipe(production, ProductionBlocks.carbideDrill, new ItemStack(Items.tungsten, 50), new ItemStack(Items.carbide, 60));
new Recipe(production, ProductionBlocks.laserdrill, new ItemStack(Items.tungsten, 90), new ItemStack(Items.carbide, 110), new ItemStack(Items.silicon, 70), new ItemStack(Items.titanium, 80));
new Recipe(production, ProductionBlocks.waterextractor, new ItemStack(Items.tungsten, 50), new ItemStack(Items.carbide, 50), new ItemStack(Items.lead, 40));
new Recipe(production, ProductionBlocks.cultivator, new ItemStack(Items.tungsten, 20), new ItemStack(Items.lead, 50), new ItemStack(Items.silicon, 20));
new Recipe(production, ProductionBlocks.oilextractor, new ItemStack(Items.tungsten, 300), new ItemStack(Items.carbide, 350), new ItemStack(Items.lead, 230), new ItemStack(Items.thorium, 230), new ItemStack(Items.silicon, 150));
//UNITS
//bodies
new Recipe(units, UpgradeBlocks.dartFactory, new ItemStack(Items.lead, 150), new ItemStack(Items.silicon, 200), new ItemStack(Items.titanium, 240))
.setDesktop(); //dart is desktop only, because it's the starter mobile ship
new Recipe(units, UpgradeBlocks.javelinFactory, new ItemStack(Items.lead, 200), new ItemStack(Items.silicon, 250), new ItemStack(Items.titanium, 300), new ItemStack(Items.plastanium, 200));
new Recipe(units, UpgradeBlocks.deltaFactory, new ItemStack(Items.carbide, 160), new ItemStack(Items.silicon, 220), new ItemStack(Items.titanium, 250))
.setDesktop();
//new Recipe(units, UpgradeBlocks.deltaFactory, new ItemStack(Items.tungsten, 30), new ItemStack(Items.lead, 50), new ItemStack(Items.silicon, 30));
//actual unit related stuff
new Recipe(units, UnitBlocks.droneFactory, new ItemStack(Items.tungsten, 50), new ItemStack(Items.lead, 90), new ItemStack(Items.silicon, 130));
new Recipe(units, UnitBlocks.fabricatorFactory, new ItemStack(Items.carbide, 70), new ItemStack(Items.thorium, 100), new ItemStack(Items.lead, 150), new ItemStack(Items.silicon, 300));
new Recipe(units, UnitBlocks.repairPoint, new ItemStack(Items.lead, 30), new ItemStack(Items.tungsten, 30), new ItemStack(Items.silicon, 30));
new Recipe(units, UnitBlocks.resupplyPoint, new ItemStack(Items.lead, 30), new ItemStack(Items.tungsten, 30), new ItemStack(Items.silicon, 30));
//LIQUIDS
new Recipe(liquid, LiquidBlocks.conduit, new ItemStack(Items.lead, 1))
.setDependencies(CraftingBlocks.smelter);
new Recipe(liquid, LiquidBlocks.pulseConduit, new ItemStack(Items.titanium, 1), new ItemStack(Items.lead, 1));
new Recipe(liquid, LiquidBlocks.phaseConduit, new ItemStack(Items.phasematter, 10), new ItemStack(Items.silicon, 15), new ItemStack(Items.lead, 20), new ItemStack(Items.titanium, 20));
new Recipe(liquid, LiquidBlocks.liquidRouter, new ItemStack(Items.carbide, 4), new ItemStack(Items.lead, 4));
new Recipe(liquid, LiquidBlocks.liquidtank, new ItemStack(Items.titanium, 50), new ItemStack(Items.lead, 50), new ItemStack(Items.carbide, 20));
new Recipe(liquid, LiquidBlocks.liquidJunction, new ItemStack(Items.carbide, 4), new ItemStack(Items.lead, 4));
new Recipe(liquid, LiquidBlocks.bridgeConduit, new ItemStack(Items.carbide, 8), new ItemStack(Items.lead, 8));
new Recipe(liquid, LiquidBlocks.mechanicalPump, new ItemStack(Items.tungsten, 30), new ItemStack(Items.lead, 20))
.setDependencies(CraftingBlocks.smelter);
new Recipe(liquid, LiquidBlocks.rotaryPump, new ItemStack(Items.tungsten, 140), new ItemStack(Items.lead, 100), new ItemStack(Items.silicon, 40), new ItemStack(Items.titanium, 70));
//DEBUG
new Recipe(units, DebugBlocks.itemSource, new ItemStack(Items.carbide, 10)).setDebug();
new Recipe(units, DebugBlocks.itemVoid, new ItemStack(Items.carbide, 10)).setDebug();
new Recipe(units, DebugBlocks.liquidSource, new ItemStack(Items.carbide, 10)).setDebug();
new Recipe(units, DebugBlocks.powerVoid, new ItemStack(Items.carbide, 10)).setDebug();
new Recipe(units, DebugBlocks.powerInfinite, new ItemStack(Items.carbide, 10), new ItemStack(Items.surgealloy, 5)).setDebug();
//new Recipe(liquid, LiquidBlocks.thermalPump, new ItemStack(Items.carbide, 10), new ItemStack(Items.surgealloy, 5));
/*
new Recipe(production, ProductionBlocks.nucleardrill, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40));
new Recipe(production, ProductionBlocks.plasmadrill, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40));
new Recipe(production, ProductionBlocks.cultivator, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40));
new Recipe(production, ProductionBlocks.waterextractor, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40));
new Recipe(production, ProductionBlocks.oilextractor, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40));*/
//new Recipe(distribution, DistributionBlocks.massDriver, new ItemStack(Items.carbide, 1));
/*
new Recipe(defense, DefenseBlocks.steelwall, new ItemStack(Items.carbide, 12));
new Recipe(defense, DefenseBlocks.titaniumwall, new ItemStack(Items.titanium, 12));
new Recipe(defense, DefenseBlocks.diriumwall, new ItemStack(Items.surgealloy, 12));
new Recipe(defense, DefenseBlocks.steelwalllarge, new ItemStack(Items.carbide, 12 * 4));
new Recipe(defense, DefenseBlocks.titaniumwalllarge, new ItemStack(Items.titanium, 12 * 4));
new Recipe(defense, DefenseBlocks.diriumwall, new ItemStack(Items.surgealloy, 12 * 4));
new Recipe(defense, DefenseBlocks.door, new ItemStack(Items.carbide, 3), new ItemStack(Items.tungsten, 3 * 4));
new Recipe(defense, DefenseBlocks.largedoor, new ItemStack(Items.carbide, 3 * 4), new ItemStack(Items.tungsten, 3 * 4 * 4));
new Recipe(defense, DefenseBlocks.deflectorwall, new ItemStack(Items.titanium, 1));
new Recipe(defense, DefenseBlocks.deflectorwalllarge, new ItemStack(Items.titanium, 1));
new Recipe(defense, DefenseBlocks.phasewall, new ItemStack(Items.titanium, 1));
new Recipe(defense, DefenseBlocks.phasewalllarge, new ItemStack(Items.titanium, 1));
new Recipe(weapon, TurretBlocks.wave, new ItemStack(Items.tungsten, 1));
new Recipe(weapon, TurretBlocks.lancer, new ItemStack(Items.tungsten, 1));
new Recipe(weapon, TurretBlocks.arc, new ItemStack(Items.tungsten, 1));
new Recipe(weapon, TurretBlocks.swarmer, new ItemStack(Items.tungsten, 1));
new Recipe(weapon, TurretBlocks.ripple, new ItemStack(Items.tungsten, 1));
new Recipe(weapon, TurretBlocks.fuse, new ItemStack(Items.tungsten, 1));
new Recipe(weapon, TurretBlocks.ripple, new ItemStack(Items.tungsten, 1));
new Recipe(weapon, TurretBlocks.cyclone, new ItemStack(Items.tungsten, 1));
new Recipe(weapon, TurretBlocks.spectre, new ItemStack(Items.tungsten, 1));
new Recipe(weapon, TurretBlocks.meltdown, new ItemStack(Items.tungsten, 1));
new Recipe(crafting, CraftingBlocks.alloysmelter, new ItemStack(Items.titanium, 50), new ItemStack(Items.carbide, 50));
new Recipe(crafting, CraftingBlocks.alloyfuser, new ItemStack(Items.carbide, 30), new ItemStack(Items.tungsten, 30));
new Recipe(crafting, CraftingBlocks.phaseWeaver, new ItemStack(Items.carbide, 30), new ItemStack(Items.tungsten, 30));
new Recipe(crafting, CraftingBlocks.separator, new ItemStack(Items.carbide, 30), new ItemStack(Items.tungsten, 30));
new Recipe(crafting, CraftingBlocks.centrifuge, new ItemStack(Items.carbide, 30), new ItemStack(Items.tungsten, 30));
new Recipe(crafting, CraftingBlocks.siliconsmelter, new ItemStack(Items.carbide, 30), new ItemStack(Items.tungsten, 30));
new Recipe(crafting, CraftingBlocks.oilRefinery, new ItemStack(Items.carbide, 15), new ItemStack(Items.tungsten, 15));
new Recipe(crafting, CraftingBlocks.biomatterCompressor, new ItemStack(Items.carbide, 15), new ItemStack(Items.tungsten, 15));
new Recipe(crafting, CraftingBlocks.plastaniumCompressor, new ItemStack(Items.carbide, 30), new ItemStack(Items.titanium, 15));
new Recipe(crafting, CraftingBlocks.cryofluidmixer, new ItemStack(Items.carbide, 30), new ItemStack(Items.titanium, 15));
new Recipe(crafting, CraftingBlocks.pulverizer, new ItemStack(Items.carbide, 10), new ItemStack(Items.tungsten, 10));
new Recipe(crafting, CraftingBlocks.stoneFormer, new ItemStack(Items.carbide, 10), new ItemStack(Items.tungsten, 10));
new Recipe(crafting, CraftingBlocks.melter, new ItemStack(Items.carbide, 30), new ItemStack(Items.titanium, 15));
new Recipe(crafting, CraftingBlocks.incinerator, new ItemStack(Items.carbide, 60), new ItemStack(Items.tungsten, 60));
new Recipe(production, ProductionBlocks.tungstenDrill, new ItemStack(Items.tungsten, 25));
new Recipe(production, ProductionBlocks.reinforcedDrill, new ItemStack(Items.tungsten, 25));
new Recipe(production, ProductionBlocks.carbideDrill, new ItemStack(Items.tungsten, 25));
new Recipe(production, ProductionBlocks.titaniumDrill, new ItemStack(Items.tungsten, 25));
new Recipe(production, ProductionBlocks.laserdrill, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40));
new Recipe(production, ProductionBlocks.nucleardrill, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40));
new Recipe(production, ProductionBlocks.plasmadrill, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40));
new Recipe(production, ProductionBlocks.cultivator, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40));
new Recipe(production, ProductionBlocks.waterextractor, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40));
new Recipe(production, ProductionBlocks.oilextractor, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40));
new Recipe(power, PowerBlocks.powerNode, new ItemStack(Items.carbide, 3), new ItemStack(Items.tungsten, 3));
new Recipe(power, PowerBlocks.powerNodeLarge, new ItemStack(Items.carbide, 3), new ItemStack(Items.tungsten, 3));
new Recipe(power, PowerBlocks.battery, new ItemStack(Items.carbide, 5), new ItemStack(Items.tungsten, 5));
new Recipe(power, PowerBlocks.batteryLarge, new ItemStack(Items.carbide, 5), new ItemStack(Items.tungsten, 5));
new Recipe(power, PowerBlocks.combustionGenerator, new ItemStack(Items.tungsten, 1));
new Recipe(power, PowerBlocks.turbineGenerator, new ItemStack(Items.tungsten, 1));
new Recipe(power, PowerBlocks.thermalGenerator, new ItemStack(Items.carbide, 1));
new Recipe(power, PowerBlocks.rtgGenerator, new ItemStack(Items.titanium, 1), new ItemStack(Items.carbide, 1));
new Recipe(power, PowerBlocks.solarPanel, new ItemStack(Items.tungsten, 30), new ItemStack(Items.silicon, 20));
new Recipe(power, PowerBlocks.largeSolarPanel, new ItemStack(Items.tungsten, 30), new ItemStack(Items.silicon, 20));
new Recipe(power, PowerBlocks.nuclearReactor, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40), new ItemStack(Items.carbide, 50));
new Recipe(power, PowerBlocks.fusionReactor, new ItemStack(Items.titanium, 40), new ItemStack(Items.surgealloy, 40), new ItemStack(Items.carbide, 50));
new Recipe(distribution, PowerBlocks.warpGate, new ItemStack(Items.carbide, 1));
new Recipe(liquid, LiquidBlocks.conduit, new ItemStack(Items.carbide, 1));
new Recipe(liquid, LiquidBlocks.pulseConduit, new ItemStack(Items.titanium, 1), new ItemStack(Items.carbide, 1));
new Recipe(liquid, LiquidBlocks.liquidRouter, new ItemStack(Items.carbide, 2));
new Recipe(liquid, LiquidBlocks.liquidtank, new ItemStack(Items.carbide, 2));
new Recipe(liquid, LiquidBlocks.liquidJunction, new ItemStack(Items.carbide, 2));
new Recipe(liquid, LiquidBlocks.bridgeConduit, new ItemStack(Items.titanium, 2), new ItemStack(Items.carbide, 2));
new Recipe(liquid, LiquidBlocks.phaseConduit, new ItemStack(Items.titanium, 2), new ItemStack(Items.carbide, 2));
new Recipe(liquid, LiquidBlocks.mechanicalPump, new ItemStack(Items.carbide, 10));
new Recipe(liquid, LiquidBlocks.rotaryPump, new ItemStack(Items.carbide, 10), new ItemStack(Items.surgealloy, 5));
new Recipe(liquid, LiquidBlocks.thermalPump, new ItemStack(Items.carbide, 10), new ItemStack(Items.surgealloy, 5));
new Recipe(units, UnitBlocks.repairPoint, new ItemStack(Items.carbide, 10));
new Recipe(units, UnitBlocks.dropPoint, new ItemStack(Items.carbide, 10));
new Recipe(units, UnitBlocks.resupplyPoint, new ItemStack(Items.carbide, 10));
new Recipe(units, UnitBlocks.droneFactory, new ItemStack(Items.tungsten, 50));
new Recipe(units, UnitBlocks.reconstructor, new ItemStack(Items.tungsten, 1));
new Recipe(units, UnitBlocks.overdriveProjector, new ItemStack(Items.tungsten, 1));
new Recipe(units, UnitBlocks.shieldProjector, new ItemStack(Items.tungsten, 1));
new Recipe(units, UpgradeBlocks.omegaFactory, new ItemStack(Items.tungsten, 1));
new Recipe(units, UpgradeBlocks.deltaFactory, new ItemStack(Items.tungsten, 1));
new Recipe(units, UpgradeBlocks.tauFactory, new ItemStack(Items.tungsten, 1));
new Recipe(units, UpgradeBlocks.tridentFactory, new ItemStack(Items.tungsten, 1));
new Recipe(units, UpgradeBlocks.javelinFactory, new ItemStack(Items.tungsten, 1));
new Recipe(units, UpgradeBlocks.halberdFactory, new ItemStack(Items.tungsten, 1));
new Recipe(units, DebugBlocks.itemSource, new ItemStack(Items.carbide, 10)).setDebug();
new Recipe(units, DebugBlocks.itemVoid, new ItemStack(Items.carbide, 10)).setDebug();
new Recipe(units, DebugBlocks.liquidSource, new ItemStack(Items.carbide, 10)).setDebug();
new Recipe(units, DebugBlocks.powerVoid, new ItemStack(Items.carbide, 10)).setDebug();
new Recipe(units, DebugBlocks.powerInfinite, new ItemStack(Items.carbide, 10), new ItemStack(Items.surgealloy, 5)).setDebug();*/
}
@Override
public Array<? extends Content> getAll(){
return Recipe.all();
}
}

View File

@@ -0,0 +1,155 @@
package io.anuke.mindustry.content;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.content.fx.EnvironmentFx;
import io.anuke.mindustry.entities.StatusController.StatusEntry;
import io.anuke.mindustry.entities.Unit;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.type.StatusEffect;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.util.Mathf;
public class StatusEffects implements ContentList{
public static StatusEffect none, burning, freezing, wet, melting, tarred, overdrive, shielded;
@Override
public void load(){
none = new StatusEffect(0);
burning = new StatusEffect(4 * 60f){
{
oppositeScale = 0.5f;
}
@Override
public StatusEntry getTransition(Unit unit, StatusEffect to, float time, float newTime, StatusEntry result){
if(to == tarred){
unit.damage(1f);
Effects.effect(EnvironmentFx.burning, unit.x + Mathf.range(unit.getSize() / 2f), unit.y + Mathf.range(unit.getSize() / 2f));
return result.set(this, Math.min(time + newTime, baseDuration + tarred.baseDuration));
}
return super.getTransition(unit, to, time, newTime, result);
}
@Override
public void update(Unit unit, float time){
unit.damagePeriodic(0.04f);
if(Mathf.chance(Timers.delta() * 0.2f)){
Effects.effect(EnvironmentFx.burning, unit.x + Mathf.range(unit.getSize() / 2f), unit.y + Mathf.range(unit.getSize() / 2f));
}
}
};
freezing = new StatusEffect(5 * 60f){
{
oppositeScale = 0.4f;
speedMultiplier = 0.7f;
}
@Override
public void update(Unit unit, float time){
if(Mathf.chance(Timers.delta() * 0.15f)){
Effects.effect(EnvironmentFx.freezing, unit.x + Mathf.range(unit.getSize() / 2f), unit.y + Mathf.range(unit.getSize() / 2f));
}
}
};
wet = new StatusEffect(3 * 60f){
{
oppositeScale = 0.5f;
speedMultiplier = 0.999f;
}
@Override
public void update(Unit unit, float time){
if(Mathf.chance(Timers.delta() * 0.15f)){
Effects.effect(EnvironmentFx.wet, unit.x + Mathf.range(unit.getSize() / 2f), unit.y + Mathf.range(unit.getSize() / 2f));
}
}
};
melting = new StatusEffect(5 * 60f){
{
oppositeScale = 0.2f;
speedMultiplier = 0.8f;
armorMultiplier = 0.8f;
}
@Override
public StatusEntry getTransition(Unit unit, StatusEffect to, float time, float newTime, StatusEntry result){
if(to == tarred){
return result.set(this, Math.min(time + newTime / 2f, baseDuration));
}
return super.getTransition(unit, to, time, newTime, result);
}
@Override
public void update(Unit unit, float time){
unit.damagePeriodic(0.3f);
if(Mathf.chance(Timers.delta() * 0.2f)){
Effects.effect(EnvironmentFx.melting, unit.x + Mathf.range(unit.getSize() / 2f), unit.y + Mathf.range(unit.getSize() / 2f));
}
}
};
tarred = new StatusEffect(4 * 60f){
{
speedMultiplier = 0.6f;
}
@Override
public void update(Unit unit, float time){
if(Mathf.chance(Timers.delta() * 0.15f)){
Effects.effect(EnvironmentFx.oily, unit.x + Mathf.range(unit.getSize() / 2f), unit.y + Mathf.range(unit.getSize() / 2f));
}
}
@Override
public StatusEntry getTransition(Unit unit, StatusEffect to, float time, float newTime, StatusEntry result){
if(to == melting || to == burning){
return result.set(to, newTime + time);
}
return result.set(to, newTime);
}
};
overdrive = new StatusEffect(6f){
{
armorMultiplier = 0.95f;
speedMultiplier = 1.05f;
damageMultiplier = 1.4f;
}
@Override
public void update(Unit unit, float time){
//idle regen boosted
unit.health += 0.01f * Timers.delta();
}
};
shielded = new StatusEffect(6f){
{
armorMultiplier = 3f;
}
};
melting.setOpposites(wet, freezing);
wet.setOpposites(burning);
freezing.setOpposites(burning, melting);
burning.setOpposites(wet, freezing);
}
@Override
public Array<? extends Content> getAll(){
return StatusEffect.all();
}
}

View File

@@ -0,0 +1,79 @@
package io.anuke.mindustry.content;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.entities.units.UnitType;
import io.anuke.mindustry.entities.units.types.*;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.ContentList;
public class UnitTypes implements ContentList{
public static UnitType drone, scout, vtol, monsoon, titan, fabricator;
@Override
public void load(){
drone = new UnitType("drone", Drone.class, Drone::new){{
isFlying = true;
drag = 0.01f;
speed = 0.2f;
maxVelocity = 0.8f;
ammoCapacity = 0;
range = 50f;
healSpeed = 0.05f;
health = 45;
}};
scout = new UnitType("scout", Scout.class, Scout::new){{
maxVelocity = 1.1f;
speed = 0.2f;
drag = 0.4f;
range = 40f;
weapon = Weapons.chainBlaster;
health = 70;
}};
titan = new UnitType("titan", Titan.class, Titan::new){{
maxVelocity = 0.8f;
speed = 0.18f;
drag = 0.4f;
range = 10f;
weapon = Weapons.chainBlaster;
health = 260;
}};
vtol = new UnitType("vtol", Vtol.class, Vtol::new){{
speed = 0.3f;
maxVelocity = 1.9f;
drag = 0.01f;
isFlying = true;
}};
monsoon = new UnitType("monsoon", Monsoon.class, Monsoon::new){{
health = 230;
speed = 0.2f;
maxVelocity = 1.4f;
drag = 0.01f;
isFlying = true;
weapon = Weapons.bomber;
}};
fabricator = new UnitType("fabricator", Fabricator.class, Fabricator::new){{
isFlying = true;
drag = 0.01f;
speed = 0.2f;
maxVelocity = 0.6f;
ammoCapacity = 0;
range = 70f;
itemCapacity = 70;
health = 120;
health = 45;
buildPower = 0.9f;
minePower = 1.1f;
healSpeed = 0.09f;
}};
}
@Override
public Array<? extends Content> getAll(){
return UnitType.all();
}
}

View File

@@ -0,0 +1,109 @@
package io.anuke.mindustry.content;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.content.fx.Fx;
import io.anuke.mindustry.content.fx.ShootFx;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.type.Upgrade;
import io.anuke.mindustry.type.Weapon;
public class Weapons implements ContentList{
public static Weapon blaster, chainBlaster, shockgun, sapper, swarmer, bomber, flakgun, flamethrower, missiles;
@Override
public void load(){
blaster = new Weapon("blaster"){{
length = 1.5f;
reload = 15f;
roundrobin = true;
ejectEffect = ShootFx.shellEjectSmall;
setAmmo(AmmoTypes.bulletLead);
}};
missiles = new Weapon("missiles"){{
length = 1.5f;
reload = 40f;
shots = 2;
inaccuracy = 10f;
roundrobin = false;
roundrobin = true;
ejectEffect = Fx.none;
setAmmo(AmmoTypes.weaponMissile);
}};
chainBlaster = new Weapon("chain-blaster"){{
length = 1.5f;
reload = 30f;
roundrobin = true;
ejectEffect = ShootFx.shellEjectSmall;
setAmmo(AmmoTypes.bulletLead, AmmoTypes.bulletCarbide, AmmoTypes.bulletTungsten, AmmoTypes.bulletSilicon, AmmoTypes.bulletThorium);
}};
shockgun = new Weapon("shockgun"){{
length = 1f;
reload = 50f;
roundrobin = true;
shots = 6;
inaccuracy = 15f;
recoil = 2f;
velocityRnd = 0.7f;
ejectEffect = ShootFx.shellEjectSmall;
setAmmo(AmmoTypes.shotgunTungsten);
}};
flakgun = new Weapon("flakgun"){{
length = 1f;
reload = 70f;
roundrobin = true;
shots = 1;
inaccuracy = 3f;
recoil = 3f;
velocityRnd = 0.1f;
ejectEffect = ShootFx.shellEjectMedium;
setAmmo(AmmoTypes.shellCarbide);
}};
flamethrower = new Weapon("flamethrower"){{
length = 1f;
reload = 14f;
roundrobin = true;
recoil = 1f;
ejectEffect = Fx.none;
setAmmo(AmmoTypes.flamerThermite);
}};
sapper = new Weapon("sapper"){{
length = 1.5f;
reload = 12f;
roundrobin = true;
ejectEffect = ShootFx.shellEjectSmall;
setAmmo(AmmoTypes.bulletCarbide);
}};
swarmer = new Weapon("swarmer"){{
length = 1.5f;
reload = 10f;
roundrobin = true;
ejectEffect = ShootFx.shellEjectSmall;
setAmmo(AmmoTypes.bulletPyratite);
}};
bomber = new Weapon("bomber"){{
length = 0f;
width = 2f;
reload = 5f;
roundrobin = true;
ejectEffect = Fx.none;
velocityRnd = 1f;
inaccuracy = 40f;
setAmmo(AmmoTypes.bombExplosive, AmmoTypes.bombIncendiary, AmmoTypes.bombOil);
}};
}
@Override
public Array<? extends Content> getAll(){
return Upgrade.all();
}
}

View File

@@ -0,0 +1,14 @@
package io.anuke.mindustry.content.blocks;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.world.Block;
public abstract class BlockList implements ContentList{
@Override
public Array<? extends Content> getAll(){
return Block.all();
}
}

View File

@@ -0,0 +1,163 @@
package io.anuke.mindustry.content.blocks;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.content.StatusEffects;
import io.anuke.mindustry.graphics.CacheLayer;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.type.ItemStack;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.*;
public class Blocks extends BlockList implements ContentList{
public static Block air, spawn, blockpart, space, metalfloor, deepwater, water, lava, oil, stone, blackstone, dirt, sand, ice, snow, grass, shrub, rock, icerock, blackrock;
@Override
public void load(){
air = new Floor("air"){
{
blend = false;
}
//don't draw
public void draw(Tile tile){
}
public void load(){
}
public void init(){
}
};
blockpart = new BlockPart();
for(int i = 1; i <= 6; i++){
new BuildBlock("build" + i);
}
space = new Floor("space"){{
placeableOn = false;
variants = 0;
cacheLayer = CacheLayer.space;
solid = true;
blend = false;
minimapColor = Color.valueOf("000001");
}};
metalfloor = new Floor("metalfloor"){{
variants = 6;
}};
deepwater = new Floor("deepwater"){{
liquidColor = Color.valueOf("546bb3");
speedMultiplier = 0.2f;
variants = 0;
liquidDrop = Liquids.water;
isLiquid = true;
status = StatusEffects.wet;
statusIntensity = 1f;
drownTime = 140f;
cacheLayer = CacheLayer.water;
minimapColor = Color.valueOf("465a96");
}};
water = new Floor("water"){{
liquidColor = Color.valueOf("546bb3");
speedMultiplier = 0.5f;
variants = 0;
status = StatusEffects.wet;
statusIntensity = 0.9f;
liquidDrop = Liquids.water;
isLiquid = true;
cacheLayer = CacheLayer.water;
minimapColor = Color.valueOf("506eb4");
}};
lava = new Floor("lava"){{
liquidColor = Color.valueOf("ed5334");
speedMultiplier = 0.2f;
damageTaken = 0.5f;
status = StatusEffects.melting;
statusIntensity = 0.8f;
variants = 0;
liquidDrop = Liquids.lava;
isLiquid = true;
cacheLayer = CacheLayer.lava;
minimapColor = Color.valueOf("ed5334");
}};
oil = new Floor("oil"){{
liquidColor = Color.valueOf("292929");
status = StatusEffects.tarred;
statusIntensity = 1f;
speedMultiplier = 0.2f;
variants = 0;
liquidDrop = Liquids.oil;
isLiquid = true;
cacheLayer = CacheLayer.oil;
minimapColor = Color.valueOf("292929");
}};
stone = new Floor("stone"){{
hasOres = true;
drops = new ItemStack(Items.stone, 1);
blends = block -> block != this && !(block instanceof Ore);
minimapColor = Color.valueOf("323232");
playerUnmineable = true;
}};
blackstone = new Floor("blackstone"){{
drops = new ItemStack(Items.stone, 1);
minimapColor = Color.valueOf("252525");
playerUnmineable = true;
}};
dirt = new Floor("dirt"){{
minimapColor = Color.valueOf("6e501e");
}};
sand = new Floor("sand"){{
drops = new ItemStack(Items.sand, 1);
minimapColor = Color.valueOf("988a67");
hasOres = true;
playerUnmineable = true;
}};
ice = new Floor("ice"){{
dragMultiplier = 0.3f;
speedMultiplier = 0.4f;
minimapColor = Color.valueOf("c4e3e7");
hasOres = true;
}};
snow = new Floor("snow"){{
minimapColor = Color.valueOf("c2d1d2");
hasOres = true;
}};
grass = new Floor("grass"){{
hasOres = true;
minimapColor = Color.valueOf("549d5b");
}};
shrub = new Rock("shrub"){{
shadow = "shrubshadow";
}};
rock = new Rock("rock"){{
variants = 2;
}};
icerock = new Rock("icerock"){{
variants = 2;
}};
blackrock = new Rock("blackrock"){{
variants = 1;
}};
}
}

View File

@@ -0,0 +1,253 @@
package io.anuke.mindustry.content.blocks;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.ItemStack;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.production.*;
public class CraftingBlocks extends BlockList implements ContentList{
public static Block smelter, arcsmelter, siliconsmelter, plastaniumCompressor, phaseWeaver, alloysmelter, alloyfuser,
pyratiteMixer, blastMixer,
cryofluidmixer, melter, separator, centrifuge, biomatterCompressor, pulverizer, solidifier, incinerator;
@Override
public void load(){
smelter = new Smelter("smelter"){{
health = 70;
result = Items.carbide;
craftTime = 45f;
burnDuration = 35f;
useFlux = true;
consumes.items(new ItemStack[]{new ItemStack(Items.tungsten, 3)});
consumes.item(Items.coal);
}};
arcsmelter = new PowerSmelter("arc-smelter"){{
health = 90;
craftEffect = BlockFx.smeltsmoke;
result = Items.carbide;
craftTime = 30f;
size = 2;
useFlux = true;
fluxNeeded = 2;
consumes.items(new ItemStack[]{new ItemStack(Items.coal, 1), new ItemStack(Items.tungsten, 2)});
consumes.power(0.1f);
}};
siliconsmelter = new PowerSmelter("silicon-smelter"){{
health = 90;
craftEffect = BlockFx.smeltsmoke;
result = Items.silicon;
craftTime = 40f;
size = 2;
hasLiquids = false;
flameColor = Color.valueOf("ffef99");
consumes.items(new ItemStack[]{new ItemStack(Items.coal, 1), new ItemStack(Items.sand, 2)});
consumes.power(0.05f);
}};
plastaniumCompressor = new PlastaniumCompressor("plastanium-compressor"){{
hasItems = true;
liquidCapacity = 60f;
craftTime = 80f;
output = Items.plastanium;
itemCapacity = 30;
size = 2;
health = 320;
hasPower = hasLiquids = true;
craftEffect = BlockFx.formsmoke;
updateEffect = BlockFx.plasticburn;
consumes.liquid(Liquids.oil, 0.3f);
consumes.power(0.4f);
consumes.item(Items.titanium, 2);
}};
phaseWeaver = new PhaseWeaver("phase-weaver"){{
health = 90;
craftEffect = BlockFx.smeltsmoke;
result = Items.phasematter;
craftTime = 120f;
size = 2;
consumes.items(new ItemStack[]{new ItemStack(Items.thorium, 4), new ItemStack(Items.sand, 10)});
consumes.power(0.5f);
}};
alloysmelter = new PowerSmelter("alloy-smelter"){{
health = 90;
craftEffect = BlockFx.smeltsmoke;
result = Items.surgealloy;
craftTime = 50f;
size = 2;
useFlux = true;
fluxNeeded = 4;
consumes.power(0.3f);
consumes.items(new ItemStack[]{new ItemStack(Items.titanium, 2), new ItemStack(Items.lead, 4), new ItemStack(Items.silicon, 3), new ItemStack(Items.plastanium, 2)});
}};
alloyfuser = new PowerSmelter("alloy-fuser"){{
health = 90;
craftEffect = BlockFx.smeltsmoke;
result = Items.surgealloy;
craftTime = 30f;
size = 3;
useFlux = true;
fluxNeeded = 4;
consumes.items(new ItemStack[]{new ItemStack(Items.titanium, 3), new ItemStack(Items.lead, 4), new ItemStack(Items.silicon, 3), new ItemStack(Items.plastanium, 2)});
consumes.power(0.4f);
}};
cryofluidmixer = new LiquidMixer("cryofluidmixer"){{
health = 200;
outputLiquid = Liquids.cryofluid;
liquidPerItem = 50f;
itemCapacity = 50;
size = 2;
hasPower = true;
consumes.power(0.1f);
consumes.item(Items.titanium);
consumes.liquid(Liquids.water, 0.3f);
}};
blastMixer = new GenericCrafter("blast-mixer"){{
itemCapacity = 20;
hasItems = true;
hasPower = true;
hasLiquids = true;
output = Items.blastCompound;
size = 2;
consumes.liquid(Liquids.oil, 0.05f);
consumes.item(Items.pyratite, 1);
consumes.power(0.04f);
}};
pyratiteMixer = new PowerSmelter("pyratite-mixer"){{
flameColor = Color.CLEAR;
itemCapacity = 20;
hasItems = true;
hasPower = true;
result = Items.pyratite;
size = 2;
consumes.power(0.02f);
consumes.items(new ItemStack[]{new ItemStack(Items.coal, 1), new ItemStack(Items.lead, 2), new ItemStack(Items.sand, 2)});
}};
melter = new PowerCrafter("melter"){{
health = 200;
outputLiquid = Liquids.lava;
outputLiquidAmount = 0.75f;
itemCapacity = 50;
craftTime = 10f;
hasLiquids = hasPower = true;
consumes.power(0.1f);
consumes.item(Items.stone, 2);
}};
separator = new Separator("separator"){{
results = new Item[]{
null, null, null, null, null, null, null, null, null, null,
Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand,
Items.stone, Items.stone, Items.stone, Items.stone, Items.stone, Items.stone, Items.stone, Items.stone, Items.stone,
Items.tungsten, Items.tungsten, Items.tungsten, Items.tungsten,
Items.lead, Items.lead,
Items.coal, Items.coal,
Items.titanium
};
filterTime = 40f;
itemCapacity = 40;
health = 50;
consumes.item(Items.stone, 2);
consumes.liquid(Liquids.water, 0.3f);
}};
centrifuge = new Separator("centrifuge"){{
results = new Item[]{
null, null, null, null, null, null, null, null, null, null, null, null, null,
Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand, Items.sand,
Items.stone, Items.stone, Items.stone, Items.stone, Items.stone, Items.stone, Items.stone, Items.stone, Items.stone, Items.stone, Items.stone,
Items.tungsten, Items.tungsten, Items.tungsten, Items.tungsten, Items.tungsten,
Items.lead, Items.lead, Items.lead,
Items.coal, Items.coal, Items.coal,
Items.titanium, Items.titanium,
Items.thorium,
};
hasPower = true;
filterTime = 15f;
itemCapacity = 60;
health = 50 * 4;
spinnerLength = 1.5f;
spinnerRadius = 3.5f;
spinnerThickness = 1.5f;
spinnerSpeed = 3f;
size = 2;
consumes.item(Items.stone, 2);
consumes.power(0.2f);
consumes.liquid(Liquids.water, 0.5f);
}};
biomatterCompressor = new Compressor("biomattercompressor"){{
liquidCapacity = 60f;
itemCapacity = 50;
craftTime = 25f;
outputLiquid = Liquids.oil;
outputLiquidAmount = 0.14f;
size = 2;
health = 320;
hasLiquids = true;
consumes.item(Items.biomatter, 1);
consumes.power(0.06f);
}};
pulverizer = new Pulverizer("pulverizer"){{
itemCapacity = 40;
output = Items.sand;
health = 80;
craftEffect = BlockFx.pulverize;
craftTime = 60f;
updateEffect = BlockFx.pulverizeSmall;
hasItems = hasPower = true;
consumes.item(Items.stone, 2);
consumes.power(0.2f);
}};
solidifier = new GenericCrafter("solidifer"){{
liquidCapacity = 21f;
craftTime = 14;
output = Items.stone;
itemCapacity = 20;
health = 80;
craftEffect = BlockFx.purifystone;
hasLiquids = hasItems = true;
consumes.liquid(Liquids.lava, 1f);
}};
incinerator = new Incinerator("incinerator"){{
health = 90;
}};
}
}

View File

@@ -0,0 +1,165 @@
package io.anuke.mindustry.content.blocks;
import com.badlogic.gdx.utils.Array;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.gen.CallBlocks;
import io.anuke.mindustry.net.In;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.Liquid;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.PowerBlock;
import io.anuke.mindustry.world.blocks.distribution.Sorter;
import io.anuke.mindustry.world.blocks.power.PowerNode;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.scene.ui.ButtonGroup;
import io.anuke.ucore.scene.ui.ImageButton;
import io.anuke.ucore.scene.ui.layout.Table;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class DebugBlocks extends BlockList implements ContentList{
public static Block powerVoid, powerInfinite, itemSource, liquidSource, itemVoid;
@Remote(targets = Loc.both, called = Loc.both, in = In.blocks, forward = true)
public static void setLiquidSourceLiquid(Player player, Tile tile, Liquid liquid){
LiquidSourceEntity entity = tile.entity();
entity.source = liquid;
}
@Override
public void load(){
powerVoid = new PowerBlock("powervoid"){
{
powerCapacity = Float.MAX_VALUE;
}
};
powerInfinite = new PowerNode("powerinfinite"){
{
powerCapacity = 10000f;
powerSpeed = 100f;
}
@Override
public void update(Tile tile){
super.update(tile);
tile.entity.power.amount = powerCapacity;
}
};
itemSource = new Sorter("itemsource"){
{
hasItems = true;
}
@Override
public void update(Tile tile){
SorterEntity entity = tile.entity();
entity.items.set(entity.sortItem, 1);
tryDump(tile, entity.sortItem);
}
@Override
public boolean acceptItem(Item item, Tile tile, Tile source){
return false;
}
};
liquidSource = new Block("liquidsource"){
{
update = true;
solid = true;
hasLiquids = true;
liquidCapacity = 100f;
configurable = true;
}
@Override
public void update(Tile tile){
LiquidSourceEntity entity = tile.entity();
tile.entity.liquids.add(entity.source, liquidCapacity);
tryDumpLiquid(tile, entity.source);
}
@Override
public void draw(Tile tile){
super.draw(tile);
LiquidSourceEntity entity = tile.entity();
Draw.color(entity.source.color);
Draw.rect("blank", tile.worldx(), tile.worldy(), 4f, 4f);
Draw.color();
}
@Override
public void buildTable(Tile tile, Table table){
LiquidSourceEntity entity = tile.entity();
Array<Liquid> items = Liquid.all();
ButtonGroup<ImageButton> group = new ButtonGroup<>();
Table cont = new Table();
for(int i = 0; i < items.size; i++){
if(i == 0) continue;
final int f = i;
ImageButton button = cont.addImageButton("white", "toggle", 24, () -> {
CallBlocks.setLiquidSourceLiquid(null, tile, items.get(f));
}).size(38, 42).padBottom(-5.1f).group(group).get();
button.getStyle().imageUpColor = items.get(i).color;
button.setChecked(entity.source.id == f);
if(i % 4 == 3){
cont.row();
}
}
table.add(cont);
}
@Override
public TileEntity getEntity(){
return new LiquidSourceEntity();
}
};
itemVoid = new Block("itemvoid"){
{
update = solid = true;
}
@Override
public void handleItem(Item item, Tile tile, Tile source){
}
@Override
public boolean acceptItem(Item item, Tile tile, Tile source){
return true;
}
};
}
class LiquidSourceEntity extends TileEntity{
public Liquid source = Liquids.water;
@Override
public void write(DataOutputStream stream) throws IOException{
stream.writeByte(source.id);
}
@Override
public void read(DataInputStream stream) throws IOException{
source = Liquid.getByID(stream.readByte());
}
}
}

View File

@@ -0,0 +1,76 @@
package io.anuke.mindustry.content.blocks;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.Wall;
import io.anuke.mindustry.world.blocks.defense.DeflectorWall;
import io.anuke.mindustry.world.blocks.defense.Door;
import io.anuke.mindustry.world.blocks.defense.PhaseWall;
public class DefenseBlocks extends BlockList implements ContentList{
public static Block tungstenWall, tungstenWallLarge, carbideWall, carbideWallLarge, thoriumWall, thoriumWallLarge, door, doorLarge, deflectorwall, deflectorwalllarge,
phasewall, phasewalllarge;
@Override
public void load(){
int wallHealthMultiplier = 4;
tungstenWall = new Wall("tungsten-wall"){{
health = 80 * wallHealthMultiplier;
}};
tungstenWallLarge = new Wall("tungsten-wall-large"){{
health = 80 * 4 * wallHealthMultiplier;
size = 2;
}};
carbideWall = new Wall("carbide-wall"){{
health = 110 * wallHealthMultiplier;
}};
carbideWallLarge = new Wall("carbide-wall-large"){{
health = 110 * wallHealthMultiplier * 4;
size = 2;
}};
thoriumWall = new Wall("thorium-wall"){{
health = 200 * wallHealthMultiplier;
}};
thoriumWallLarge = new Wall("thorium-wall-large"){{
health = 200 * wallHealthMultiplier * 4;
size = 2;
}};
deflectorwall = new DeflectorWall("deflector-wall"){{
health = 150 * wallHealthMultiplier;
}};
deflectorwalllarge = new DeflectorWall("deflector-wall-large"){{
health = 150 * 4 * wallHealthMultiplier;
size = 2;
}};
phasewall = new PhaseWall("phase-wall"){{
health = 150 * wallHealthMultiplier;
}};
phasewalllarge = new PhaseWall("phase-wall-large"){{
health = 150 * 4 * wallHealthMultiplier;
size = 2;
regenSpeed = 0.5f;
}};
door = new Door("door"){{
health = 100 * wallHealthMultiplier;
}};
doorLarge = new Door("door-large"){{
openfx = BlockFx.dooropenlarge;
closefx = BlockFx.doorcloselarge;
health = 100 * 4 * wallHealthMultiplier;
size = 2;
}};
}
}

View File

@@ -0,0 +1,56 @@
package io.anuke.mindustry.content.blocks;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.distribution.*;
public class DistributionBlocks extends BlockList implements ContentList{
public static Block conveyor, titaniumconveyor, distributor, junction,
bridgeConveyor, phaseConveyor, sorter, splitter, overflowGate, massDriver;
@Override
public void load(){
conveyor = new Conveyor("conveyor"){{
health = 45;
speed = 0.03f;
}};
titaniumconveyor = new Conveyor("titanium-conveyor"){{
health = 65;
speed = 0.07f;
}};
junction = new Junction("junction"){{
speed = 26;
capacity = 32;
}};
bridgeConveyor = new BufferedItemBridge("bridge-conveyor"){{
range = 4;
speed = 60f;
}};
phaseConveyor = new ItemBridge("phase-conveyor"){{
range = 11;
hasPower = true;
consumes.power(0.05f);
}};
sorter = new Sorter("sorter");
splitter = new Splitter("splitter");
distributor = new Splitter("distributor"){{
size = 2;
}};
overflowGate = new OverflowGate("overflow-gate");
massDriver = new MassDriver("mass-driver"){{
size = 3;
itemCapacity = 80;
range = 300f;
}};
}
}

View File

@@ -0,0 +1,71 @@
package io.anuke.mindustry.content.blocks;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.distribution.*;
import io.anuke.mindustry.world.blocks.production.Pump;
public class LiquidBlocks extends BlockList implements ContentList{
public static Block mechanicalPump, rotaryPump, thermalPump, conduit, pulseConduit, liquidRouter, liquidtank, liquidJunction, bridgeConduit, phaseConduit;
@Override
public void load(){
mechanicalPump = new Pump("mechanical-pump"){{
shadow = "shadow-round-1";
pumpAmount = 0.1f;
tier = 0;
}};
rotaryPump = new Pump("rotary-pump"){{
shadow = "shadow-rounded-2";
pumpAmount = 0.25f;
consumes.power(0.015f);
liquidCapacity = 30f;
hasPower = true;
size = 2;
tier = 1;
}};
thermalPump = new Pump("thermal-pump"){{
pumpAmount = 0.3f;
consumes.power(0.05f);
liquidCapacity = 40f;
size = 2;
tier = 2;
}};
conduit = new Conduit("conduit"){{
health = 45;
}};
pulseConduit = new Conduit("pulse-conduit"){{
liquidCapacity = 16f;
liquidFlowFactor = 4.9f;
health = 90;
}};
liquidRouter = new LiquidRouter("liquid-router"){{
liquidCapacity = 40f;
}};
liquidtank = new LiquidRouter("liquid-tank"){{
size = 3;
liquidCapacity = 1500f;
health = 500;
}};
liquidJunction = new LiquidJunction("liquid-junction");
bridgeConduit = new LiquidExtendingBridge("bridge-conduit"){{
range = 4;
hasPower = false;
}};
phaseConduit = new LiquidBridge("phase-conduit"){{
range = 11;
hasPower = true;
consumes.power(0.05f);
}};
}
}

View File

@@ -0,0 +1,35 @@
package io.anuke.mindustry.content.blocks;
import com.badlogic.gdx.utils.ObjectMap;
import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.Floor;
import io.anuke.mindustry.world.blocks.OreBlock;
public class OreBlocks extends BlockList{
private static final ObjectMap<Item, ObjectMap<Block, Block>> oreBlockMap = new ObjectMap<>();
public static Block get(Block floor, Item item){
if(!oreBlockMap.containsKey(item)) throw new IllegalArgumentException("Item '" + item + "' is not an ore!");
if(!oreBlockMap.get(item).containsKey(floor))
throw new IllegalArgumentException("Block '" + floor.name + "' does not support ores!");
return oreBlockMap.get(item).get(floor);
}
@Override
public void load(){
Item[] ores = {Items.tungsten, Items.lead, Items.coal, Items.titanium, Items.thorium};
for(Item item : ores){
ObjectMap<Block, Block> map = new ObjectMap<>();
oreBlockMap.put(item, map);
for(Block block : Block.all()){
if(block instanceof Floor && ((Floor) block).hasOres){
map.put(block, new OreBlock(item, (Floor) block));
}
}
}
}
}

View File

@@ -0,0 +1,90 @@
package io.anuke.mindustry.content.blocks;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.distribution.WarpGate;
import io.anuke.mindustry.world.blocks.power.*;
public class PowerBlocks extends BlockList implements ContentList{
public static Block combustionGenerator, thermalGenerator, turbineGenerator, rtgGenerator, solarPanel, largeSolarPanel,
nuclearReactor, fusionReactor, battery, batteryLarge, powerNode, powerNodeLarge, warpGate;
@Override
public void load(){
combustionGenerator = new BurnerGenerator("combustion-generator"){{
powerOutput = 0.09f;
powerCapacity = 40f;
itemDuration = 40f;
}};
thermalGenerator = new LiquidHeatGenerator("thermal-generator"){{
maxLiquidGenerate = 0.5f;
powerPerLiquid = 0.08f;
powerCapacity = 40f;
powerPerLiquid = 0.25f;
generateEffect = BlockFx.redgeneratespark;
size = 2;
}};
turbineGenerator = new TurbineGenerator("turbine-generator"){{
powerOutput = 0.28f;
powerCapacity = 40f;
itemDuration = 30f;
powerPerLiquid = 0.7f;
consumes.liquid(Liquids.water, 0.05f);
size = 2;
}};
rtgGenerator = new DecayGenerator("rtg-generator"){{
powerCapacity = 40f;
powerOutput = 0.02f;
itemDuration = 500f;
}};
solarPanel = new SolarGenerator("solar-panel"){{
generation = 0.0045f;
}};
largeSolarPanel = new SolarGenerator("solar-panel-large"){{
size = 3;
generation = 0.055f;
}};
nuclearReactor = new NuclearReactor("nuclear-reactor"){{
size = 3;
health = 700;
powerMultiplier = 0.8f;
}};
fusionReactor = new FusionReactor("fusion-reactor"){{
size = 4;
health = 600;
}};
battery = new PowerDistributor("battery"){{
powerCapacity = 320f;
}};
batteryLarge = new PowerDistributor("battery-large"){{
size = 3;
powerCapacity = 2000f;
}};
powerNode = new PowerNode("power-node"){{
shadow = "shadow-round-1";
}};
powerNodeLarge = new PowerNode("power-node-large"){{
size = 2;
powerSpeed = 1f;
maxNodes = 5;
laserRange = 7.5f;
shadow = "shadow-round-2";
}};
warpGate = new WarpGate("warp-gate");
}
}

View File

@@ -0,0 +1,108 @@
package io.anuke.mindustry.content.blocks;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.production.Cultivator;
import io.anuke.mindustry.world.blocks.production.Drill;
import io.anuke.mindustry.world.blocks.production.Fracker;
import io.anuke.mindustry.world.blocks.production.SolidPump;
public class ProductionBlocks extends BlockList implements ContentList{
public static Block tungstenDrill, carbideDrill, laserdrill, blastdrill, plasmadrill, waterextractor, oilextractor, cultivator;
@Override
public void load(){
tungstenDrill = new Drill("tungsten-drill"){{
tier = 2;
drillTime = 340;
}};
carbideDrill = new Drill("carbide-drill"){{
tier = 3;
drillTime = 280;
}};
laserdrill = new Drill("laser-drill"){{
drillTime = 180;
size = 2;
hasPower = true;
tier = 4;
updateEffect = BlockFx.pulverizeMedium;
drillEffect = BlockFx.mineBig;
consumes.power(0.2f);
}};
blastdrill = new Drill("blast-drill"){{
drillTime = 120;
size = 3;
drawRim = true;
hasPower = true;
tier = 5;
updateEffect = BlockFx.pulverizeRed;
updateEffectChance = 0.03f;
drillEffect = BlockFx.mineHuge;
rotateSpeed = 6f;
warmupSpeed = 0.01f;
consumes.power(0.5f);
}};
plasmadrill = new Drill("plasma-drill"){{
heatColor = Color.valueOf("ff461b");
drillTime = 90;
size = 4;
hasLiquids = true;
hasPower = true;
tier = 5;
rotateSpeed = 9f;
drawRim = true;
updateEffect = BlockFx.pulverizeRedder;
updateEffectChance = 0.04f;
drillEffect = BlockFx.mineHuge;
warmupSpeed = 0.005f;
consumes.power(0.7f);
}};
waterextractor = new SolidPump("water-extractor"){{
result = Liquids.water;
pumpAmount = 0.1f;
size = 2;
liquidCapacity = 30f;
rotateSpeed = 1.4f;
consumes.power(0.2f);
}};
oilextractor = new Fracker("oil-extractor"){{
result = Liquids.oil;
updateEffect = BlockFx.pulverize;
liquidCapacity = 50f;
updateEffectChance = 0.05f;
pumpAmount = 0.08f;
size = 3;
liquidCapacity = 30f;
consumes.item(Items.sand);
consumes.power(0.5f);
consumes.liquid(Liquids.water, 0.3f);
}};
cultivator = new Cultivator("cultivator"){{
result = Items.biomatter;
drillTime = 260;
size = 2;
hasLiquids = true;
hasPower = true;
consumes.power(0.08f);
consumes.liquid(Liquids.water, 0.2f);
}};
}
}

View File

@@ -0,0 +1,33 @@
package io.anuke.mindustry.content.blocks;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.storage.CoreBlock;
import io.anuke.mindustry.world.blocks.storage.SortedUnloader;
import io.anuke.mindustry.world.blocks.storage.Unloader;
import io.anuke.mindustry.world.blocks.storage.Vault;
public class StorageBlocks extends BlockList implements ContentList{
public static Block core, vault, unloader, sortedunloader;
@Override
public void load(){
core = new CoreBlock("core"){{
health = 800;
}};
vault = new Vault("vault"){{
size = 3;
health = 600;
itemCapacity = 2000;
}};
unloader = new Unloader("unloader"){{
speed = 5;
}};
sortedunloader = new SortedUnloader("sortedunloader"){{
speed = 5;
}};
}
}

View File

@@ -0,0 +1,206 @@
package io.anuke.mindustry.content.blocks;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.content.AmmoTypes;
import io.anuke.mindustry.content.fx.ShootFx;
import io.anuke.mindustry.type.AmmoType;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.defense.turrets.*;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Strings;
public class TurretBlocks extends BlockList implements ContentList{
public static Block duo, /*scatter,*/
scorch, hail, wave, lancer, arc, swarmer, salvo, fuse, ripple, cyclone, spectre, meltdown;
@Override
public void load(){
duo = new DoubleTurret("duo"){{
ammoTypes = new AmmoType[]{AmmoTypes.bulletTungsten, AmmoTypes.bulletLead, AmmoTypes.bulletCarbide, AmmoTypes.bulletPyratite, AmmoTypes.bulletSilicon};
reload = 25f;
restitution = 0.03f;
range = 90f;
shootCone = 15f;
ammoUseEffect = ShootFx.shellEjectSmall;
health = 80;
inaccuracy = 2f;
rotatespeed = 10f;
}};
/*
scatter = new BurstTurret("scatter") {{
ammoTypes = new AmmoType[]{AmmoTypes.flakLead, AmmoTypes.flakExplosive, AmmoTypes.flakPlastic};
ammoPerShot = 1;
shots = 3;
reload = 60f;
restitution = 0.03f;
recoil = 1.5f;
burstSpacing = 1f;
inaccuracy = 7f;
ammoUseEffect = ShootFx.shellEjectSmall;
}};*/
hail = new ArtilleryTurret("hail"){{
ammoTypes = new AmmoType[]{AmmoTypes.artilleryCarbide, AmmoTypes.artilleryHoming, AmmoTypes.artilleryIncindiary};
reload = 100f;
recoil = 2f;
range = 200f;
inaccuracy = 5f;
health = 120;
}};
scorch = new LiquidTurret("scorch"){{
ammoTypes = new AmmoType[]{AmmoTypes.basicFlame};
recoil = 0f;
reload = 4f;
shootCone = 50f;
ammoUseEffect = ShootFx.shellEjectSmall;
health = 160;
drawer = (tile, entity) -> Draw.rect(entity.target != null ? name + "-shoot" : name, tile.drawx() + tr2.x, tile.drawy() + tr2.y, entity.rotation - 90);
}};
wave = new LiquidTurret("wave"){{
ammoTypes = new AmmoType[]{AmmoTypes.water, AmmoTypes.lava, AmmoTypes.cryofluid, AmmoTypes.oil};
size = 2;
recoil = 0f;
reload = 4f;
inaccuracy = 5f;
shootCone = 50f;
shootEffect = ShootFx.shootLiquid;
range = 70f;
health = 360;
drawer = (tile, entity) -> {
Draw.rect(region, tile.drawx() + tr2.x, tile.drawy() + tr2.y, entity.rotation - 90);
Draw.color(entity.liquids.current().color);
Draw.alpha(entity.liquids.total() / liquidCapacity);
Draw.rect(name + "-liquid", tile.drawx() + tr2.x, tile.drawy() + tr2.y, entity.rotation - 90);
Draw.color();
};
}};
lancer = new LaserTurret("lancer"){{
range = 90f;
chargeTime = 60f;
chargeMaxDelay = 30f;
chargeEffects = 7;
shootType = AmmoTypes.lancerLaser;
recoil = 2f;
reload = 100f;
cooldown = 0.03f;
powerUsed = 20f;
powerCapacity = 60f;
shootShake = 2f;
shootEffect = ShootFx.lancerLaserShoot;
smokeEffect = ShootFx.lancerLaserShootSmoke;
chargeEffect = ShootFx.lancerLaserCharge;
chargeBeginEffect = ShootFx.lancerLaserChargeBegin;
heatColor = Color.RED;
size = 2;
health = 320;
}};
arc = new LaserTurret("arc"){{
shootType = AmmoTypes.lightning;
reload = 100f;
chargeTime = 70f;
shootShake = 1f;
chargeMaxDelay = 30f;
chargeEffects = 7;
shootEffect = ShootFx.lightningShoot;
chargeEffect = ShootFx.lightningCharge;
chargeBeginEffect = ShootFx.lancerLaserChargeBegin;
heatColor = Color.RED;
recoil = 3f;
size = 2;
}};
swarmer = new BurstTurret("swarmer"){{
ammoTypes = new AmmoType[]{AmmoTypes.missileExplosive, AmmoTypes.missileIncindiary/*, AmmoTypes.missileSurge*/};
reload = 60f;
shots = 4;
burstSpacing = 5;
inaccuracy = 10f;
range = 140f;
xRand = 6f;
size = 2;
health = 380;
}};
salvo = new BurstTurret("salvo"){{
size = 2;
range = 120f;
ammoTypes = new AmmoType[]{AmmoTypes.bulletTungsten, AmmoTypes.bulletCarbide, AmmoTypes.bulletPyratite, AmmoTypes.bulletThorium, AmmoTypes.bulletSilicon};
reload = 40f;
restitution = 0.03f;
ammoEjectBack = 3f;
cooldown = 0.03f;
recoil = 3f;
shootShake = 2f;
burstSpacing = 4;
shots = 3;
ammoUseEffect = ShootFx.shellEjectBig;
drawer = (tile, entity) -> {
Draw.rect(region, tile.drawx() + tr2.x, tile.drawy() + tr2.y, entity.rotation - 90);
float offsetx = (int) (Mathf.abscurve(Mathf.curve(entity.reload / reload, 0.3f, 0.2f)) * 3f);
float offsety = -(int) (Mathf.abscurve(Mathf.curve(entity.reload / reload, 0.3f, 0.2f)) * 2f);
for(int i : Mathf.signs){
float rot = entity.rotation + 90 * i;
Draw.rect(name + "-panel-" + Strings.dir(i),
tile.drawx() + tr2.x + Angles.trnsx(rot, offsetx, offsety),
tile.drawy() + tr2.y + Angles.trnsy(rot, -offsetx, offsety), entity.rotation - 90);
}
};
health = 360;
}};
ripple = new ArtilleryTurret("ripple"){{
ammoTypes = new AmmoType[]{AmmoTypes.artilleryCarbide, AmmoTypes.artilleryHoming, AmmoTypes.artilleryIncindiary, AmmoTypes.artilleryExplosive, AmmoTypes.artilleryPlastic};
size = 3;
shots = 4;
inaccuracy = 12f;
reload = 60f;
ammoEjectBack = 5f;
ammoUseEffect = ShootFx.shellEjectBig;
cooldown = 0.03f;
velocityInaccuracy = 0.2f;
restitution = 0.02f;
recoil = 6f;
shootShake = 2f;
range = 300f;
health = 550;
}};
cyclone = new ItemTurret("cyclone"){{
ammoTypes = new AmmoType[]{AmmoTypes.flakLead, AmmoTypes.flakExplosive, AmmoTypes.flakPlastic, AmmoTypes.flakSurge};
size = 3;
}};
fuse = new ItemTurret("fuse"){{
//TODO make it use power
ammoTypes = new AmmoType[]{AmmoTypes.fuseShotgun};
size = 3;
}};
spectre = new ItemTurret("spectre"){{
ammoTypes = new AmmoType[]{AmmoTypes.bulletTungsten, AmmoTypes.bulletLead, AmmoTypes.bulletCarbide, AmmoTypes.bulletPyratite, AmmoTypes.bulletThorium, AmmoTypes.bulletSilicon};
reload = 25f;
restitution = 0.03f;
ammoUseEffect = ShootFx.shellEjectSmall;
size = 4;
}};
meltdown = new PowerTurret("meltdown"){{
shootType = AmmoTypes.meltdownLaser;
size = 4;
}};
}
}

View File

@@ -0,0 +1,58 @@
package io.anuke.mindustry.content.blocks;
import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.content.UnitTypes;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.type.ItemStack;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.units.*;
public class UnitBlocks extends BlockList implements ContentList{
public static Block resupplyPoint, repairPoint, droneFactory, fabricatorFactory, dropPoint, reconstructor, overdriveProjector, shieldProjector;
@Override
public void load(){
droneFactory = new UnitFactory("drone-factory"){{
type = UnitTypes.drone;
produceTime = 800;
size = 2;
consumes.power(0.08f);
consumes.items(new ItemStack[]{new ItemStack(Items.silicon, 30), new ItemStack(Items.lead, 30)});
}};
fabricatorFactory = new UnitFactory("fabricator-factory"){{
type = UnitTypes.fabricator;
produceTime = 1600;
size = 2;
consumes.power(0.2f);
consumes.items(new ItemStack[]{new ItemStack(Items.silicon, 70), new ItemStack(Items.lead, 80), new ItemStack(Items.titanium, 80)});
}};
resupplyPoint = new ResupplyPoint("resupply-point"){{
shadow = "shadow-round-1";
itemCapacity = 30;
}};
dropPoint = new DropPoint("drop-point"){{
shadow = "shadow-round-1";
itemCapacity = 40;
}};
repairPoint = new RepairPoint("repair-point"){{
shadow = "shadow-round-1";
repairSpeed = 0.1f;
}};
reconstructor = new Reconstructor("reconstructor"){{
size = 2;
}};
overdriveProjector = new OverdriveProjector("overdrive-projector"){{
size = 2;
}};
shieldProjector = new ShieldProjector("shieldprojector"){{
size = 2;
}};
}
}

View File

@@ -0,0 +1,47 @@
package io.anuke.mindustry.content.blocks;
import io.anuke.mindustry.content.Mechs;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.units.MechFactory;
public class UpgradeBlocks extends BlockList{
public static Block deltaFactory, tauFactory, omegaFactory, dartFactory, javelinFactory, tridentFactory, halberdFactory;
@Override
public void load(){
deltaFactory = new MechFactory("delta-mech-factory"){{
mech = Mechs.delta;
size = 2;
}};
tauFactory = new MechFactory("tau-mech-factory"){{
mech = Mechs.tau;
size = 2;
}};
omegaFactory = new MechFactory("omega-mech-factory"){{
mech = Mechs.omega;
size = 3;
}};
dartFactory = new MechFactory("dart-ship-factory"){{
mech = Mechs.dart;
size = 2;
}};
javelinFactory = new MechFactory("javelin-ship-factory"){{
mech = Mechs.javelin;
size = 2;
}};
tridentFactory = new MechFactory("trident-ship-factory"){{
mech = Mechs.trident;
size = 2;
}};
halberdFactory = new MechFactory("halberd-ship-factory"){{
mech = Mechs.halberd;
size = 3;
}};
}
}

View File

@@ -0,0 +1,106 @@
package io.anuke.mindustry.content.bullets;
import io.anuke.mindustry.content.fx.BulletFx;
import io.anuke.mindustry.entities.bullet.ArtilleryBulletType;
import io.anuke.mindustry.entities.bullet.BasicBulletType;
import io.anuke.mindustry.entities.bullet.BulletType;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
public class ArtilleryBullets extends BulletList implements ContentList{
public static BulletType carbide, plastic, plasticFrag, homing, incindiary, explosive, surge;
@Override
public void load(){
carbide = new ArtilleryBulletType(3f, 0, "shell"){
{
hiteffect = BulletFx.flakExplosion;
knockback = 0.8f;
lifetime = 50f;
bulletWidth = bulletHeight = 11f;
collidesTiles = false;
splashDamageRadius = 25f;
splashDamage = 33f;
}
};
plasticFrag = new BasicBulletType(2.5f, 6, "bullet"){
{
bulletWidth = 10f;
bulletHeight = 12f;
bulletShrink = 1f;
lifetime = 15f;
backColor = Palette.plastaniumBack;
frontColor = Palette.plastaniumFront;
}
};
plastic = new ArtilleryBulletType(3.3f, 0, "shell"){
{
hiteffect = BulletFx.plasticExplosion;
knockback = 1f;
lifetime = 55f;
bulletWidth = bulletHeight = 13f;
collidesTiles = false;
splashDamageRadius = 35f;
splashDamage = 35f;
fragBullet = plasticFrag;
fragBullets = 9;
backColor = Palette.plastaniumBack;
frontColor = Palette.plastaniumFront;
}
};
homing = new ArtilleryBulletType(3f, 0, "shell"){
{
hiteffect = BulletFx.flakExplosion;
knockback = 0.8f;
lifetime = 45f;
bulletWidth = bulletHeight = 11f;
collidesTiles = false;
splashDamageRadius = 25f;
splashDamage = 33f;
homingPower = 2f;
homingRange = 50f;
}
};
incindiary = new ArtilleryBulletType(3f, 0, "shell"){
{
hiteffect = BulletFx.blastExplosion;
knockback = 0.8f;
lifetime = 60f;
bulletWidth = bulletHeight = 13f;
collidesTiles = false;
splashDamageRadius = 25f;
splashDamage = 30f;
incendAmount = 4;
incendSpread = 11f;
frontColor = Palette.lightishOrange;
backColor = Palette.lightOrange;
trailEffect = BulletFx.incendTrail;
}
};
explosive = new ArtilleryBulletType(2f, 0, "shell"){
{
hiteffect = BulletFx.blastExplosion;
knockback = 0.8f;
lifetime = 70f;
bulletWidth = bulletHeight = 14f;
collidesTiles = false;
splashDamageRadius = 45f;
splashDamage = 50f;
backColor = Palette.missileYellowBack;
frontColor = Palette.missileYellow;
}
};
surge = new ArtilleryBulletType(3f, 0, "shell"){
{
//TODO
}
};
}
}

View File

@@ -0,0 +1,14 @@
package io.anuke.mindustry.content.bullets;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.entities.bullet.BulletType;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.ContentList;
public abstract class BulletList implements ContentList{
@Override
public Array<? extends Content> getAll(){
return BulletType.all();
}
}

View File

@@ -0,0 +1,41 @@
package io.anuke.mindustry.content.bullets;
import io.anuke.mindustry.entities.bullet.BasicBulletType;
import io.anuke.mindustry.entities.bullet.BulletType;
import io.anuke.mindustry.type.ContentList;
public class FlakBullets extends BulletList implements ContentList{
public static BulletType lead, plastic, explosive, surge;
@Override
public void load(){
lead = new BasicBulletType(3f, 5, "bullet"){
{
bulletWidth = 7f;
bulletHeight = 9f;
}
};
plastic = new BasicBulletType(3f, 5, "bullet"){
{
bulletWidth = 7f;
bulletHeight = 9f;
}
};
explosive = new BasicBulletType(3f, 5, "bullet"){
{
bulletWidth = 7f;
bulletHeight = 9f;
}
};
surge = new BasicBulletType(3f, 5, "bullet"){
{
bulletWidth = 7f;
bulletHeight = 9f;
}
};
}
}

View File

@@ -0,0 +1,69 @@
package io.anuke.mindustry.content.bullets;
import io.anuke.mindustry.content.fx.BulletFx;
import io.anuke.mindustry.entities.bullet.BulletType;
import io.anuke.mindustry.entities.bullet.MissileBulletType;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
public class MissileBullets extends BulletList implements ContentList{
public static BulletType explosive, incindiary, surge, javelin;
@Override
public void load(){
explosive = new MissileBulletType(1.8f, 10, "missile"){
{
bulletWidth = 8f;
bulletHeight = 8f;
bulletShrink = 0f;
drag = -0.01f;
splashDamageRadius = 30f;
splashDamage = 30f;
lifetime = 150f;
hiteffect = BulletFx.blastExplosion;
despawneffect = BulletFx.blastExplosion;
}
};
incindiary = new MissileBulletType(2f, 12, "missile"){
{
frontColor = Palette.lightishOrange;
backColor = Palette.lightOrange;
bulletWidth = 7f;
bulletHeight = 8f;
bulletShrink = 0f;
drag = -0.01f;
homingPower = 7f;
splashDamageRadius = 10f;
splashDamage = 10f;
lifetime = 160f;
hiteffect = BulletFx.blastExplosion;
incendSpread = 10f;
incendAmount = 3;
}
};
surge = new MissileBulletType(3f, 5, "bullet"){
{
bulletWidth = 7f;
bulletHeight = 9f;
}
};
javelin = new MissileBulletType(2.5f, 10, "missile"){
{
bulletWidth = 8f;
bulletHeight = 8f;
bulletShrink = 0f;
drag = -0.02f;
keepVelocity = false;
splashDamageRadius = 25f;
splashDamage = 15f;
lifetime = 90f;
hiteffect = BulletFx.blastExplosion;
despawneffect = BulletFx.blastExplosion;
}
};
}
}

View File

@@ -0,0 +1,64 @@
package io.anuke.mindustry.content.bullets;
import io.anuke.mindustry.entities.bullet.BasicBulletType;
import io.anuke.mindustry.entities.bullet.BulletType;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
public class StandardBullets extends BulletList implements ContentList{
public static BulletType tungsten, lead, carbide, thorium, homing, tracer;
@Override
public void load(){
tungsten = new BasicBulletType(3.2f, 10, "bullet"){
{
bulletWidth = 9f;
bulletHeight = 11f;
}
};
lead = new BasicBulletType(2.5f, 5, "bullet"){
{
bulletWidth = 7f;
bulletHeight = 9f;
}
};
carbide = new BasicBulletType(3.5f, 18, "bullet"){
{
bulletWidth = 9f;
bulletHeight = 12f;
armorPierce = 0.2f;
}
};
thorium = new BasicBulletType(4f, 29, "bullet"){
{
bulletWidth = 10f;
bulletHeight = 13f;
armorPierce = 0.5f;
}
};
homing = new BasicBulletType(3f, 9, "bullet"){
{
bulletWidth = 7f;
bulletHeight = 9f;
homingPower = 5f;
}
};
tracer = new BasicBulletType(3.2f, 11, "bullet"){
{
bulletWidth = 10f;
bulletHeight = 12f;
frontColor = Palette.lightishOrange;
backColor = Palette.lightOrange;
incendSpread = 3f;
incendAmount = 1;
incendChance = 0.3f;
}
};
}
}

View File

@@ -0,0 +1,273 @@
package io.anuke.mindustry.content.bullets;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.content.StatusEffects;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.content.fx.BulletFx;
import io.anuke.mindustry.content.fx.EnvironmentFx;
import io.anuke.mindustry.content.fx.Fx;
import io.anuke.mindustry.entities.Damage;
import io.anuke.mindustry.entities.bullet.Bullet;
import io.anuke.mindustry.entities.bullet.BulletType;
import io.anuke.mindustry.entities.bullet.LiquidBulletType;
import io.anuke.mindustry.entities.effect.Fire;
import io.anuke.mindustry.entities.effect.ItemDrop;
import io.anuke.mindustry.entities.effect.Lightning;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.distribution.MassDriver.DriverBulletData;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.world;
public class TurretBullets extends BulletList implements ContentList{
public static BulletType fireball, basicFlame, lancerLaser, fuseShot, waterShot, cryoShot, lavaShot, oilShot, lightning, driverBolt;
@Override
public void load(){
fireball = new BulletType(1f, 4){
{
pierce = true;
hitTiles = false;
collides = false;
collidesTiles = false;
drag = 0.03f;
}
@Override
public void init(Bullet b){
b.getVelocity().setLength(0.6f + Mathf.random(2f));
}
@Override
public void draw(Bullet b){
//TODO add color to the bullet depending on the color of the flame it came from
Draw.color(Palette.lightFlame, Palette.darkFlame, Color.GRAY, b.fin());
Fill.circle(b.x, b.y, 3f * b.fout());
Draw.reset();
}
@Override
public void update(Bullet b){
if(Mathf.chance(0.04 * Timers.delta())){
Tile tile = world.tileWorld(b.x, b.y);
if(tile != null){
Fire.create(tile);
}
}
if(Mathf.chance(0.1 * Timers.delta())){
Effects.effect(EnvironmentFx.fireballsmoke, b.x, b.y);
}
if(Mathf.chance(0.1 * Timers.delta())){
Effects.effect(EnvironmentFx.ballfire, b.x, b.y);
}
}
};
basicFlame = new BulletType(2f, 5){
{
hitsize = 7f;
lifetime = 30f;
pierce = true;
drag = 0.07f;
hiteffect = BulletFx.hitFlameSmall;
despawneffect = Fx.none;
status = StatusEffects.burning;
}
@Override
public void draw(Bullet b){
}
};
lancerLaser = new BulletType(0.001f, 110){
Color[] colors = {Palette.lancerLaser.cpy().mul(1f, 1f, 1f, 0.4f), Palette.lancerLaser, Color.WHITE};
float[] tscales = {1f, 0.7f, 0.5f, 0.2f};
float[] lenscales = {1f, 1.1f, 1.13f, 1.14f};
float length = 90f;
{
hiteffect = BulletFx.hitLancer;
despawneffect = Fx.none;
hitsize = 4;
lifetime = 16f;
pierce = true;
}
@Override
public void init(Bullet b){
Damage.collideLine(b, b.getTeam(), hiteffect, b.x, b.y, b.angle(), length);
}
@Override
public void draw(Bullet b){
float f = Mathf.curve(b.fin(), 0f, 0.2f);
float baseLen = length * f;
Lines.lineAngle(b.x, b.y, b.angle(), baseLen);
for(int s = 0; s < 3; s++){
Draw.color(colors[s]);
for(int i = 0; i < tscales.length; i++){
Lines.stroke(7f * b.fout() * (s == 0 ? 1.5f : s == 1 ? 1f : 0.3f) * tscales[i]);
Lines.lineAngle(b.x, b.y, b.angle(), baseLen * lenscales[i]);
}
}
Draw.reset();
}
};
fuseShot = new BulletType(0.01f, 100){
//TODO
};
waterShot = new LiquidBulletType(Liquids.water){
{
status = StatusEffects.wet;
statusIntensity = 0.5f;
knockback = 0.65f;
}
};
cryoShot = new LiquidBulletType(Liquids.cryofluid){
{
status = StatusEffects.freezing;
statusIntensity = 0.5f;
}
};
lavaShot = new LiquidBulletType(Liquids.lava){
{
damage = 4;
speed = 1.9f;
drag = 0.03f;
status = StatusEffects.melting;
statusIntensity = 0.5f;
}
};
oilShot = new LiquidBulletType(Liquids.oil){
{
speed = 2f;
drag = 0.03f;
status = StatusEffects.tarred;
statusIntensity = 0.5f;
}
};
lightning = new BulletType(0.001f, 10){
{
lifetime = 1;
despawneffect = Fx.none;
hiteffect = BulletFx.hitLancer;
}
@Override
public void draw(Bullet b){
}
@Override
public void init(Bullet b){
Lightning.create(b.getTeam(), hiteffect, Palette.lancerLaser, damage, b.x, b.y, b.angle(), 30);
}
};
driverBolt = new BulletType(5f, 20){
{
collidesTiles = false;
lifetime = 200f;
despawneffect = BlockFx.smeltsmoke;
hiteffect = BulletFx.hitBulletBig;
drag = 0.02f;
}
@Override
public void draw(Bullet b){
Draw.color(Color.LIGHT_GRAY);
Fill.square(b.x, b.y, 3f, b.angle());
Draw.color(Palette.lighterOrange);
Fill.square(b.x, b.y, 2f, b.angle());
Draw.reset();
}
@Override
public void update(Bullet b){
//data MUST be an instance of DriverBulletData
if(!(b.getData() instanceof DriverBulletData)){
hit(b);
return;
}
float hitDst = 7f;
DriverBulletData data = (DriverBulletData) b.getData();
//if the target is dead, just keep flying until the bullet explodes
if(data.to.isDead()){
return;
}
float baseDst = data.from.distanceTo(data.to);
float dst1 = b.distanceTo(data.from);
float dst2 = b.distanceTo(data.to);
boolean intersect = false;
//bullet has gone past the destination point: but did it intersect it?
if(dst1 > baseDst){
float angleTo = b.angleTo(data.to);
float baseAngle = data.to.angleTo(data.from);
//if angles are nearby, then yes, it did
if(Mathf.angNear(angleTo, baseAngle, 2f)){
intersect = true;
//snap bullet position back; this is used for low-FPS situations
b.set(data.to.x + Angles.trnsx(baseAngle, hitDst), data.to.y + Angles.trnsy(baseAngle, hitDst));
}
}
//if on course and it's in range of the target
if(Math.abs(dst1 + dst2 - baseDst) < 4f && dst2 <= hitDst){
intersect = true;
} //else, bullet has gone off course, does not get recieved.
if(intersect){
data.to.handlePayload(b, data);
}
}
@Override
public void despawned(Bullet b){
super.despawned(b);
if(!(b.getData() instanceof DriverBulletData)) return;
DriverBulletData data = (DriverBulletData) b.getData();
data.to.isRecieving = false;
for(int i = 0; i < data.items.length; i++){
int amountDropped = Mathf.random(0, data.items[i]);
if(amountDropped > 0){
float angle = b.angle() + Mathf.range(100f);
float vs = Mathf.random(0f, 4f);
ItemDrop.create(Item.getByID(i), amountDropped, b.x, b.y, Angles.trnsx(angle, vs), Angles.trnsy(angle, vs));
}
}
}
@Override
public void hit(Bullet b, float hitx, float hity){
super.hit(b, hitx, hity);
despawned(b);
}
};
}
}

View File

@@ -0,0 +1,99 @@
package io.anuke.mindustry.content.bullets;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.content.fx.BulletFx;
import io.anuke.mindustry.entities.bullet.BasicBulletType;
import io.anuke.mindustry.entities.bullet.BombBulletType;
import io.anuke.mindustry.entities.bullet.Bullet;
import io.anuke.mindustry.entities.bullet.BulletType;
import io.anuke.mindustry.entities.effect.Fire;
import io.anuke.mindustry.entities.effect.Puddle;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.world;
public class WeaponBullets extends BulletList{
public static BulletType tungstenShotgun, bombExplosive, bombIncendiary, bombOil, shellCarbide;
@Override
public void load(){
tungstenShotgun = new BasicBulletType(5f, 8, "bullet"){
{
bulletWidth = 8f;
bulletHeight = 9f;
bulletShrink = 0.6f;
lifetime = 30f;
drag = 0.04f;
}
};
bombExplosive = new BombBulletType(20f, 20f, "shell"){
{
bulletWidth = 9f;
bulletHeight = 13f;
hiteffect = BulletFx.flakExplosion;
}
};
bombIncendiary = new BombBulletType(15f, 10f, "shell"){
{
bulletWidth = 8f;
bulletHeight = 12f;
hiteffect = BulletFx.flakExplosion;
backColor = Palette.lightOrange;
frontColor = Palette.lightishOrange;
}
@Override
public void hit(Bullet b, float x, float y){
super.hit(b, x, y);
for(int i = 0; i < 3; i++){
float cx = x + Mathf.range(10f);
float cy = y + Mathf.range(10f);
Tile tile = world.tileWorld(cx, cy);
if(tile != null){
Fire.create(tile);
}
}
}
};
bombOil = new BombBulletType(3f, 3f, "shell"){
{
bulletWidth = 8f;
bulletHeight = 12f;
hiteffect = BlockFx.pulverize;
backColor = new Color(0x4f4f4fff);
frontColor = Color.GRAY;
}
@Override
public void hit(Bullet b, float x, float y){
super.hit(b, x, y);
for(int i = 0; i < 3; i++){
Tile tile = world.tileWorld(x + Mathf.range(8f), y + Mathf.range(8f));
Puddle.deposit(tile, Liquids.oil, 5f);
}
}
};
shellCarbide = new BasicBulletType(3.4f, 20, "bullet"){
{
bulletWidth = 10f;
bulletHeight = 12f;
bulletShrink = 0.4f;
lifetime = 40f;
drag = 0.025f;
fragBullets = 5;
hiteffect = BulletFx.flakExplosion;
fragBullet = tungstenShotgun;
}
};
}
}

View File

@@ -0,0 +1,278 @@
package io.anuke.mindustry.content.fx;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.entities.effect.GroundEffectEntity.GroundEffect;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.graphics.Hue;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Tmp;
import static io.anuke.mindustry.Vars.tilesize;
public class BlockFx extends FxList implements ContentList{
public static Effect reactorsmoke, nuclearsmoke, nuclearcloud, redgeneratespark, generatespark, fuelburn, plasticburn, pulverize, pulverizeRed, pulverizeRedder, pulverizeSmall, pulverizeMedium, producesmoke, smeltsmoke, formsmoke, blastsmoke, lava, dooropen, doorclose, dooropenlarge, doorcloselarge, purify, purifyoil, purifystone, generate, mine, mineBig, mineHuge, smelt, teleportActivate, teleport, teleportOut, ripple, bubble;
@Override
public void load(){
reactorsmoke = new Effect(17, e -> {
Angles.randLenVectors(e.id, 4, e.fin() * 8f, (x, y) -> {
float size = 1f + e.fout() * 5f;
Draw.color(Color.LIGHT_GRAY, Color.GRAY, e.fin());
Draw.rect("circle", e.x + x, e.y + y, size, size);
Draw.reset();
});
});
nuclearsmoke = new Effect(40, e -> {
Angles.randLenVectors(e.id, 4, e.fin() * 13f, (x, y) -> {
float size = e.fslope() * 4f;
Draw.color(Color.LIGHT_GRAY, Color.GRAY, e.fin());
Draw.rect("circle", e.x + x, e.y + y, size, size);
Draw.reset();
});
});
nuclearcloud = new Effect(90, 200f, e -> {
Angles.randLenVectors(e.id, 10, e.finpow() * 90f, (x, y) -> {
float size = e.fout() * 14f;
Draw.color(Color.LIME, Color.GRAY, e.fin());
Draw.rect("circle", e.x + x, e.y + y, size, size);
Draw.reset();
});
});
redgeneratespark = new Effect(18, e -> {
Angles.randLenVectors(e.id, 5, e.fin() * 8f, (x, y) -> {
float len = e.fout() * 4f;
Draw.color(Palette.redSpark, Color.GRAY, e.fin());
//Draw.alpha(e.fout());
Draw.rect("circle", e.x + x, e.y + y, len, len);
Draw.reset();
});
});
generatespark = new Effect(18, e -> {
Angles.randLenVectors(e.id, 5, e.fin() * 8f, (x, y) -> {
float len = e.fout() * 4f;
Draw.color(Palette.orangeSpark, Color.GRAY, e.fin());
Draw.rect("circle", e.x + x, e.y + y, len, len);
Draw.reset();
});
});
fuelburn = new Effect(23, e -> {
Angles.randLenVectors(e.id, 5, e.fin() * 9f, (x, y) -> {
float len = e.fout() * 4f;
Draw.color(Color.LIGHT_GRAY, Color.GRAY, e.fin());
Draw.rect("circle", e.x + x, e.y + y, len, len);
Draw.reset();
});
});
plasticburn = new Effect(40, e -> {
Angles.randLenVectors(e.id, 5, 3f + e.fin() * 5f, (x, y) -> {
Draw.color(Color.valueOf("e9ead3"), Color.GRAY, e.fin());
Fill.circle(e.x + x, e.y + y, e.fout() * 1f);
Draw.reset();
});
});
pulverize = new Effect(40, e -> {
Angles.randLenVectors(e.id, 5, 3f + e.fin() * 8f, (x, y) -> {
Draw.color(Palette.stoneGray);
Fill.poly(e.x + x, e.y + y, 4, e.fout() * 2f + 0.5f, 45);
Draw.reset();
});
});
pulverizeRed = new Effect(40, e -> {
Angles.randLenVectors(e.id, 5, 3f + e.fin() * 8f, (x, y) -> {
Draw.color(Palette.redDust, Palette.stoneGray, e.fin());
Fill.poly(e.x + x, e.y + y, 4, e.fout() * 2f + 0.5f, 45);
Draw.reset();
});
});
pulverizeRedder = new Effect(40, e -> {
Angles.randLenVectors(e.id, 5, 3f + e.fin() * 9f, (x, y) -> {
Draw.color(Palette.redderDust, Palette.stoneGray, e.fin());
Fill.poly(e.x + x, e.y + y, 4, e.fout() * 2.5f + 0.5f, 45);
Draw.reset();
});
});
pulverizeSmall = new Effect(30, e -> {
Angles.randLenVectors(e.id, 3, e.fin() * 5f, (x, y) -> {
Draw.color(Palette.stoneGray);
Fill.poly(e.x + x, e.y + y, 4, e.fout() * 1f + 0.5f, 45);
Draw.reset();
});
});
pulverizeMedium = new Effect(30, e -> {
Angles.randLenVectors(e.id, 5, 3f + e.fin() * 8f, (x, y) -> {
Draw.color(Palette.stoneGray);
Fill.poly(e.x + x, e.y + y, 4, e.fout() * 1f + 0.5f, 45);
Draw.reset();
});
});
producesmoke = new Effect(12, e -> {
Angles.randLenVectors(e.id, 8, 4f + e.fin() * 18f, (x, y) -> {
Draw.color(Color.WHITE, Palette.accent, e.fin());
Fill.poly(e.x + x, e.y + y, 4, 1f + e.fout() * 3f, 45);
Draw.reset();
});
});
smeltsmoke = new Effect(15, e -> {
Angles.randLenVectors(e.id, 6, 4f + e.fin() * 5f, (x, y) -> {
Draw.color(Color.WHITE, e.color, e.fin());
Fill.poly(e.x + x, e.y + y, 4, 0.5f + e.fout() * 2f, 45);
Draw.reset();
});
});
formsmoke = new Effect(40, e -> {
Angles.randLenVectors(e.id, 6, 5f + e.fin() * 8f, (x, y) -> {
Draw.color(Palette.plasticSmoke, Color.LIGHT_GRAY, e.fin());
Fill.poly(e.x + x, e.y + y, 4, 0.2f + e.fout() * 2f, 45);
Draw.reset();
});
});
blastsmoke = new Effect(26, e -> {
Angles.randLenVectors(e.id, 12, 1f + e.fin() * 23f, (x, y) -> {
float size = 2f + e.fout() * 6f;
Draw.color(Color.LIGHT_GRAY, Color.DARK_GRAY, e.fin());
Draw.rect("circle", e.x + x, e.y + y, size, size);
Draw.reset();
});
});
lava = new Effect(18, e -> {
Angles.randLenVectors(e.id, 3, 1f + e.fin() * 10f, (x, y) -> {
float size = e.fslope() * 4f;
Draw.color(Color.ORANGE, Color.GRAY, e.fin());
Draw.rect("circle", e.x + x, e.y + y, size, size);
Draw.reset();
});
});
dooropen = new Effect(10, e -> {
Lines.stroke(e.fout() * 1.6f);
Lines.square(e.x, e.y, tilesize / 2f + e.fin() * 2f);
Draw.reset();
});
doorclose = new Effect(10, e -> {
Lines.stroke(e.fout() * 1.6f);
Lines.square(e.x, e.y, tilesize / 2f + e.fout() * 2f);
Draw.reset();
});
dooropenlarge = new Effect(10, e -> {
Lines.stroke(e.fout() * 1.6f);
Lines.square(e.x, e.y, tilesize + e.fin() * 2f);
Draw.reset();
});
doorcloselarge = new Effect(10, e -> {
Lines.stroke(e.fout() * 1.6f);
Lines.square(e.x, e.y, tilesize + e.fout() * 2f);
Draw.reset();
});
purify = new Effect(10, e -> {
Draw.color(Color.ROYAL, Color.GRAY, e.fin());
Lines.stroke(2f);
Lines.spikes(e.x, e.y, e.fin() * 4f, 2, 6);
Draw.reset();
});
purifyoil = new Effect(10, e -> {
Draw.color(Color.BLACK, Color.GRAY, e.fin());
Lines.stroke(2f);
Lines.spikes(e.x, e.y, e.fin() * 4f, 2, 6);
Draw.reset();
});
purifystone = new Effect(10, e -> {
Draw.color(Color.ORANGE, Color.GRAY, e.fin());
Lines.stroke(2f);
Lines.spikes(e.x, e.y, e.fin() * 4f, 2, 6);
Draw.reset();
});
generate = new Effect(11, e -> {
Draw.color(Color.ORANGE, Color.YELLOW, e.fin());
Lines.stroke(1f);
Lines.spikes(e.x, e.y, e.fin() * 5f, 2, 8);
Draw.reset();
});
mine = new Effect(20, e -> {
Angles.randLenVectors(e.id, 6, 3f + e.fin() * 6f, (x, y) -> {
Draw.color(e.color, Color.LIGHT_GRAY, e.fin());
Fill.poly(e.x + x, e.y + y, 4, e.fout() * 2f, 45);
Draw.reset();
});
});
mineBig = new Effect(30, e -> {
Angles.randLenVectors(e.id, 6, 4f + e.fin() * 8f, (x, y) -> {
Draw.color(e.color, Color.LIGHT_GRAY, e.fin());
Fill.poly(e.x + x, e.y + y, 4, e.fout() * 2f + 0.2f, 45);
Draw.reset();
});
});
mineHuge = new Effect(40, e -> {
Angles.randLenVectors(e.id, 8, 5f + e.fin() * 10f, (x, y) -> {
Draw.color(e.color, Color.LIGHT_GRAY, e.fin());
Fill.poly(e.x + x, e.y + y, 4, e.fout() * 2f + 0.5f, 45);
Draw.reset();
});
});
smelt = new Effect(20, e -> {
Angles.randLenVectors(e.id, 6, 2f + e.fin() * 5f, (x, y) -> {
Draw.color(Color.WHITE, e.color, e.fin());
Fill.poly(e.x + x, e.y + y, 4, 0.5f + e.fout() * 2f, 45);
Draw.reset();
});
});
teleportActivate = new Effect(50, e -> {
Draw.color(e.color);
e.scaled(8f, e2 -> {
Lines.stroke(e2.fout() * 4f);
Lines.circle(e2.x, e2.y, 4f + e2.fin() * 27f);
});
Lines.stroke(e.fout() * 2f);
Angles.randLenVectors(e.id, 30, 4f + 40f * e.fin(), (x, y) -> {
Lines.lineAngle(e.x + x, e.y + y, Mathf.atan2(x, y), e.fin() * 4f + 1f);
});
Draw.reset();
});
teleport = new Effect(60, e -> {
Draw.color(e.color);
Lines.stroke(e.fin() * 2f);
Lines.circle(e.x, e.y, 7f + e.fout() * 8f);
Angles.randLenVectors(e.id, 20, 6f + 20f * e.fout(), (x, y) -> {
Lines.lineAngle(e.x + x, e.y + y, Mathf.atan2(x, y), e.fin() * 4f + 1f);
});
Draw.reset();
});
teleportOut = new Effect(20, e -> {
Draw.color(e.color);
Lines.stroke(e.fout() * 2f);
Lines.circle(e.x, e.y, 7f + e.fin() * 8f);
Angles.randLenVectors(e.id, 20, 4f + 20f * e.fin(), (x, y) -> {
Lines.lineAngle(e.x + x, e.y + y, Mathf.atan2(x, y), e.fslope() * 4f + 1f);
});
Draw.reset();
});
ripple = new GroundEffect(false, 30, e -> {
Draw.color(Hue.shift(Tmp.c1.set(e.color), 2, 0.1f));
Lines.stroke(e.fout() + 0.4f);
Lines.circle(e.x, e.y, 2f + e.fin() * 4f);
Draw.reset();
});
bubble = new Effect(20, e -> {
Draw.color(Hue.shift(Tmp.c1.set(e.color), 2, 0.1f));
Lines.stroke(e.fout() + 0.2f);
Angles.randLenVectors(e.id, 2, 8f, (x, y) -> {
Lines.circle(e.x + x, e.y + y, 1f + e.fin() * 3f);
});
Draw.reset();
});
}
}

View File

@@ -0,0 +1,181 @@
package io.anuke.mindustry.content.fx;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
public class BulletFx extends FxList implements ContentList{
public static Effect hitBulletSmall, hitBulletBig, hitFlameSmall, hitLiquid, hitLancer, despawn, flakExplosion, blastExplosion, plasticExplosion,
artilleryTrail, incendTrail, missileTrail;
@Override
public void load(){
hitBulletSmall = new Effect(14, e -> {
Draw.color(Color.WHITE, Palette.lightOrange, e.fin());
Lines.stroke(0.5f + e.fout());
Angles.randLenVectors(e.id, 5, e.fin() * 15f, e.rotation, 50f, (x, y) -> {
float ang = Mathf.atan2(x, y);
Lines.lineAngle(e.x + x, e.y + y, ang, e.fout() * 3 + 1f);
});
Draw.reset();
});
hitBulletBig = new Effect(13, e -> {
Draw.color(Color.WHITE, Palette.lightOrange, e.fin());
Lines.stroke(0.5f + e.fout() * 1.5f);
Angles.randLenVectors(e.id, 8, e.finpow() * 30f, e.rotation, 50f, (x, y) -> {
float ang = Mathf.atan2(x, y);
Lines.lineAngle(e.x + x, e.y + y, ang, e.fout() * 4 + 1.5f);
});
Draw.reset();
});
hitFlameSmall = new Effect(14, e -> {
Draw.color(Palette.lightFlame, Palette.darkFlame, e.fin());
Lines.stroke(0.5f + e.fout());
Angles.randLenVectors(e.id, 5, e.fin() * 15f, e.rotation, 50f, (x, y) -> {
float ang = Mathf.atan2(x, y);
Lines.lineAngle(e.x + x, e.y + y, ang, e.fout() * 3 + 1f);
});
Draw.reset();
});
hitLiquid = new Effect(16, e -> {
Draw.color(e.color);
Angles.randLenVectors(e.id, 5, e.fin() * 15f, e.rotation + 180f, 60f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 2f);
});
Draw.reset();
});
hitLancer = new Effect(12, e -> {
Draw.color(Color.WHITE);
Lines.stroke(e.fout() * 1.5f);
Angles.randLenVectors(e.id, 8, e.finpow() * 17f, e.rotation, 360f, (x, y) -> {
float ang = Mathf.atan2(x, y);
Lines.lineAngle(e.x + x, e.y + y, ang, e.fout() * 4 + 1f);
});
Draw.reset();
});
despawn = new Effect(12, e -> {
Draw.color(Palette.lighterOrange, Color.GRAY, e.fin());
Lines.stroke(e.fout());
Angles.randLenVectors(e.id, 7, e.fin() * 7f, e.rotation, 40f, (x, y) -> {
float ang = Mathf.atan2(x, y);
Lines.lineAngle(e.x + x, e.y + y, ang, e.fout() * 2 + 1f);
});
Draw.reset();
});
flakExplosion = new Effect(20, e -> {
Draw.color(Palette.bulletYellow);
e.scaled(6, i -> {
Lines.stroke(3f * i.fout());
Lines.circle(e.x, e.y, 3f + i.fin() * 10f);
});
Draw.color(Color.GRAY);
Angles.randLenVectors(e.id, 5, 2f + 23f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 3f + 0.5f);
});
Draw.color(Palette.lighterOrange);
Lines.stroke(1f * e.fout());
Angles.randLenVectors(e.id + 1, 4, 1f + 23f * e.finpow(), (x, y) -> {
Lines.lineAngle(e.x + x, e.y + y, Mathf.atan2(x, y), 1f + e.fout() * 3f);
});
Draw.reset();
});
plasticExplosion = new Effect(24, e -> {
Draw.color(Palette.plastaniumFront);
e.scaled(7, i -> {
Lines.stroke(3f * i.fout());
Lines.circle(e.x, e.y, 3f + i.fin() * 24f);
});
Draw.color(Color.GRAY);
Angles.randLenVectors(e.id, 7, 2f + 28f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 4f + 0.5f);
});
Draw.color(Palette.plastaniumBack);
Lines.stroke(1f * e.fout());
Angles.randLenVectors(e.id + 1, 4, 1f + 25f * e.finpow(), (x, y) -> {
Lines.lineAngle(e.x + x, e.y + y, Mathf.atan2(x, y), 1f + e.fout() * 3f);
});
Draw.reset();
});
blastExplosion = new Effect(22, e -> {
Draw.color(Palette.missileYellow);
e.scaled(6, i -> {
Lines.stroke(3f * i.fout());
Lines.circle(e.x, e.y, 3f + i.fin() * 15f);
});
Draw.color(Color.GRAY);
Angles.randLenVectors(e.id, 5, 2f + 23f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 4f + 0.5f);
});
Draw.color(Palette.missileYellowBack);
Lines.stroke(1f * e.fout());
Angles.randLenVectors(e.id + 1, 4, 1f + 23f * e.finpow(), (x, y) -> {
Lines.lineAngle(e.x + x, e.y + y, Mathf.atan2(x, y), 1f + e.fout() * 3f);
});
Draw.reset();
});
artilleryTrail = new Effect(50, e -> {
Draw.color(e.color);
Fill.circle(e.x, e.y, e.rotation * e.fout());
Draw.reset();
});
incendTrail = new Effect(50, e -> {
Draw.color(Palette.lightOrange);
Fill.circle(e.x, e.y, e.rotation * e.fout());
Draw.reset();
});
missileTrail = new Effect(50, e -> {
Draw.color(Palette.missileYellowBack);
Fill.circle(e.x, e.y, e.rotation * e.fout());
Draw.reset();
});
}
}

View File

@@ -0,0 +1,119 @@
package io.anuke.mindustry.content.fx;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
public class EnvironmentFx extends FxList implements ContentList{
public static Effect burning, fire, smoke, steam, fireballsmoke, ballfire, freezing, melting, wet, oily;
@Override
public void load(){
burning = new Effect(35f, e -> {
Draw.color(Palette.lightFlame, Palette.darkFlame, e.fin());
Angles.randLenVectors(e.id, 3, 2f + e.fin() * 7f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.1f + e.fout() * 1.4f);
});
Draw.color();
});
fire = new Effect(35f, e -> {
Draw.color(Palette.lightFlame, Palette.darkFlame, e.fin());
Angles.randLenVectors(e.id, 2, 2f + e.fin() * 7f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.2f + e.fslope() * 1.5f);
});
Draw.color();
});
smoke = new Effect(35f, e -> {
Draw.color(Color.GRAY);
Angles.randLenVectors(e.id, 1, 2f + e.fin() * 7f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.2f + e.fslope() * 1.5f);
});
Draw.color();
});
steam = new Effect(35f, e -> {
Draw.color(Color.LIGHT_GRAY);
Angles.randLenVectors(e.id, 2, 2f + e.fin() * 7f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.2f + e.fslope() * 1.5f);
});
Draw.color();
});
fireballsmoke = new Effect(25f, e -> {
Draw.color(Color.GRAY);
Angles.randLenVectors(e.id, 1, 2f + e.fin() * 7f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.2f + e.fout() * 1.5f);
});
Draw.color();
});
ballfire = new Effect(25f, e -> {
Draw.color(Palette.lightFlame, Palette.darkFlame, e.fin());
Angles.randLenVectors(e.id, 2, 2f + e.fin() * 7f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.2f + e.fout() * 1.5f);
});
Draw.color();
});
freezing = new Effect(40f, e -> {
Draw.color(Liquids.cryofluid.color);
Angles.randLenVectors(e.id, 2, 1f + e.fin() * 2f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 1.2f);
});
Draw.color();
});
melting = new Effect(40f, e -> {
Draw.color(Liquids.lava.color, Color.WHITE, e.fout() / 5f + Mathf.randomSeedRange(e.id, 0.12f));
Angles.randLenVectors(e.id, 2, 1f + e.fin() * 3f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, .2f + e.fout() * 1.2f);
});
Draw.color();
});
wet = new Effect(40f, e -> {
Draw.color(Liquids.water.color);
Angles.randLenVectors(e.id, 2, 1f + e.fin() * 2f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 1f);
});
Draw.color();
});
oily = new Effect(42f, e -> {
Draw.color(Liquids.oil.color);
Angles.randLenVectors(e.id, 2, 1f + e.fin() * 2f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 1f);
});
Draw.color();
});
}
}

View File

@@ -0,0 +1,97 @@
package io.anuke.mindustry.content.fx;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
public class ExplosionFx extends FxList implements ContentList{
public static Effect shockwave, bigShockwave, nuclearShockwave, explosion, blockExplosion, blockExplosionSmoke;
@Override
public void load(){
shockwave = new Effect(10f, 80f, e -> {
Draw.color(Color.WHITE, Color.LIGHT_GRAY, e.fin());
Lines.stroke(e.fout() * 2f + 0.2f);
Lines.circle(e.x, e.y, e.fin() * 28f);
Draw.reset();
});
bigShockwave = new Effect(10f, 80f, e -> {
Draw.color(Color.WHITE, Color.LIGHT_GRAY, e.fin());
Lines.stroke(e.fout() * 3f);
Lines.circle(e.x, e.y, e.fin() * 50f);
Draw.reset();
});
nuclearShockwave = new Effect(10f, 200f, e -> {
Draw.color(Color.WHITE, Color.LIGHT_GRAY, e.fin());
Lines.stroke(e.fout() * 3f + 0.2f);
Lines.poly(e.x, e.y, 40, e.fin() * 140f);
Draw.reset();
});
explosion = new Effect(30, e -> {
e.scaled(7, i -> {
Lines.stroke(3f * i.fout());
Lines.circle(e.x, e.y, 3f + i.fin() * 10f);
});
Draw.color(Color.GRAY);
Angles.randLenVectors(e.id, 6, 2f + 19f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 3f + 0.5f);
Fill.circle(e.x + x / 2f, e.y + y / 2f, e.fout() * 1f);
});
Draw.color(Palette.lighterOrange, Palette.lightOrange, Color.GRAY, e.fin());
Lines.stroke(1.5f * e.fout());
Angles.randLenVectors(e.id + 1, 8, 1f + 23f * e.finpow(), (x, y) -> {
Lines.lineAngle(e.x + x, e.y + y, Mathf.atan2(x, y), 1f + e.fout() * 3f);
});
Draw.reset();
});
blockExplosion = new Effect(30, e -> {
e.scaled(7, i -> {
Lines.stroke(3.1f * i.fout());
Lines.circle(e.x, e.y, 3f + i.fin() * 14f);
});
Draw.color(Color.GRAY);
Angles.randLenVectors(e.id, 6, 2f + 19f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 3f + 0.5f);
Fill.circle(e.x + x / 2f, e.y + y / 2f, e.fout() * 1f);
});
Draw.color(Palette.lighterOrange, Palette.lightOrange, Color.GRAY, e.fin());
Lines.stroke(1.7f * e.fout());
Angles.randLenVectors(e.id + 1, 9, 1f + 23f * e.finpow(), (x, y) -> {
Lines.lineAngle(e.x + x, e.y + y, Mathf.atan2(x, y), 1f + e.fout() * 3f);
});
Draw.reset();
});
blockExplosionSmoke = new Effect(30, e -> {
Draw.color(Color.GRAY);
Angles.randLenVectors(e.id, 6, 4f + 30f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 3f);
Fill.circle(e.x + x / 2f, e.y + y / 2f, e.fout() * 1f);
});
Draw.reset();
});
}
}

View File

@@ -0,0 +1,69 @@
package io.anuke.mindustry.content.fx;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Angles;
import static io.anuke.mindustry.Vars.tilesize;
public class Fx extends FxList implements ContentList{
public static Effect none, placeBlock, breakBlock, smoke, spawn, tapBlock, select;
@Override
public void load(){
none = new Effect(0, 0f, e -> {
});
placeBlock = new Effect(16, e -> {
Draw.color(Palette.accent);
Lines.stroke(3f - e.fin() * 2f);
Lines.square(e.x, e.y, tilesize / 2f * e.rotation + e.fin() * 3f);
Draw.reset();
});
tapBlock = new Effect(12, e -> {
Draw.color(Palette.accent);
Lines.stroke(3f - e.fin() * 2f);
Lines.circle(e.x, e.y, 4f + (tilesize / 1.5f * e.rotation) * e.fin());
Draw.reset();
});
breakBlock = new Effect(12, e -> {
Draw.color(Palette.remove);
Lines.stroke(3f - e.fin() * 2f);
Lines.square(e.x, e.y, tilesize / 2f * e.rotation + e.fin() * 3f);
Angles.randLenVectors(e.id, 3 + (int) (e.rotation * 3), e.rotation * 2f + (tilesize * e.rotation) * e.finpow(), (x, y) -> {
Fill.square(e.x + x, e.y + y, 1f + e.fout() * (3f + e.rotation));
});
Draw.reset();
});
select = new Effect(23, e -> {
Draw.color(Palette.accent);
Lines.stroke(e.fout() * 3f);
Lines.circle(e.x, e.y, 3f + e.fin() * 14f);
Draw.reset();
});
smoke = new Effect(100, e -> {
Draw.color(Color.GRAY, Palette.darkishGray, e.fin());
float size = 7f - e.fin() * 7f;
Draw.rect("circle", e.x, e.y, size, size);
Draw.reset();
});
spawn = new Effect(23, e -> {
Lines.stroke(2f * e.fout());
Draw.color(Palette.accent);
Lines.poly(e.x, e.y, 4, 3f + e.fin() * 8f);
Draw.reset();
});
}
}

View File

@@ -0,0 +1,13 @@
package io.anuke.mindustry.content.fx;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.ContentList;
public abstract class FxList implements ContentList{
@Override
public Array<? extends Content> getAll(){
return Array.with();
}
}

View File

@@ -0,0 +1,217 @@
package io.anuke.mindustry.content.fx;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.entities.effect.GroundEffectEntity.GroundEffect;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.graphics.Shapes;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
public class ShootFx extends FxList implements ContentList{
public static Effect shootSmall, shootSmallSmoke, shootBig, shootBig2, shootBigSmoke, shootBigSmoke2, shootSmallFlame, shootLiquid, shellEjectSmall, shellEjectMedium, shellEjectBig, lancerLaserShoot, lancerLaserShootSmoke, lancerLaserCharge, lancerLaserChargeBegin, lightningCharge, lightningShoot;
@Override
public void load(){
shootSmall = new Effect(8, e -> {
Draw.color(Palette.lighterOrange, Palette.lightOrange, e.fin());
float w = 1f + 5 * e.fout();
Shapes.tri(e.x, e.y, w, 15f * e.fout(), e.rotation);
Shapes.tri(e.x, e.y, w, 3f * e.fout(), e.rotation + 180f);
Draw.reset();
});
shootSmallSmoke = new Effect(20f, e -> {
Draw.color(Palette.lighterOrange, Color.LIGHT_GRAY, Color.GRAY, e.fin());
Angles.randLenVectors(e.id, 5, e.finpow() * 6f, e.rotation, 20f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 1.5f);
});
Draw.reset();
});
shootBig = new Effect(9, e -> {
Draw.color(Palette.lighterOrange, Palette.lightOrange, e.fin());
float w = 1.2f + 7 * e.fout();
Shapes.tri(e.x, e.y, w, 25f * e.fout(), e.rotation);
Shapes.tri(e.x, e.y, w, 4f * e.fout(), e.rotation + 180f);
Draw.reset();
});
shootBig2 = new Effect(10, e -> {
Draw.color(Palette.lightOrange, Color.GRAY, e.fin());
float w = 1.2f + 8 * e.fout();
Shapes.tri(e.x, e.y, w, 29f * e.fout(), e.rotation);
Shapes.tri(e.x, e.y, w, 5f * e.fout(), e.rotation + 180f);
Draw.reset();
});
shootBigSmoke = new Effect(17f, e -> {
Draw.color(Palette.lighterOrange, Color.LIGHT_GRAY, Color.GRAY, e.fin());
Angles.randLenVectors(e.id, 8, e.finpow() * 19f, e.rotation, 10f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 2f + 0.2f);
});
Draw.reset();
});
shootBigSmoke2 = new Effect(18f, e -> {
Draw.color(Palette.lightOrange, Color.LIGHT_GRAY, Color.GRAY, e.fin());
Angles.randLenVectors(e.id, 9, e.finpow() * 23f, e.rotation, 20f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 2.4f + 0.2f);
});
Draw.reset();
});
shootSmallFlame = new Effect(30f, e -> {
Draw.color(Palette.lightFlame, Palette.darkFlame, Color.GRAY, e.fin());
Angles.randLenVectors(e.id, 8, e.finpow() * 26f, e.rotation, 10f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.65f + e.fout() * 1.5f);
});
Draw.reset();
});
shootLiquid = new Effect(40f, e -> {
Draw.color(e.color, Color.WHITE, e.fout() / 6f + Mathf.randomSeedRange(e.id, 0.1f));
Angles.randLenVectors(e.id, 6, e.finpow() * 60f, e.rotation, 11f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, 0.5f + e.fout() * 2.5f);
});
Draw.reset();
});
shellEjectSmall = new GroundEffect(30f, 400f, e -> {
Draw.color(Palette.lightOrange, Color.LIGHT_GRAY, Palette.lightishGray, e.fin());
float rot = Math.abs(e.rotation) + 90f;
int i = Mathf.sign(e.rotation);
float len = (2f + e.finpow() * 6f) * i;
float lr = rot + e.fin() * 30f * i;
Draw.rect("white",
e.x + Angles.trnsx(lr, len) + Mathf.randomSeedRange(e.id + i + 7, 3f * e.fin()),
e.y + Angles.trnsy(lr, len) + Mathf.randomSeedRange(e.id + i + 8, 3f * e.fin()),
1f, 2f, rot + e.fin() * 50f * i);
Draw.color();
});
shellEjectMedium = new GroundEffect(34f, 400f, e -> {
Draw.color(Palette.lightOrange, Color.LIGHT_GRAY, Palette.lightishGray, e.fin());
float rot = e.rotation + 90f;
for(int i : Mathf.signs){
float len = (2f + e.finpow() * 10f) * i;
float lr = rot + e.fin() * 20f * i;
Draw.rect("casing",
e.x + Angles.trnsx(lr, len) + Mathf.randomSeedRange(e.id + i + 7, 3f * e.fin()),
e.y + Angles.trnsy(lr, len) + Mathf.randomSeedRange(e.id + i + 8, 3f * e.fin()),
2f, 3f, rot);
}
Draw.color(Color.LIGHT_GRAY, Color.GRAY, e.fin());
for(int i : Mathf.signs){
Angles.randLenVectors(e.id, 4, 1f + e.finpow() * 11f, e.rotation + 90f * i, 20f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 1.5f);
});
}
Draw.color();
});
shellEjectBig = new GroundEffect(22f, 400f, e -> {
Draw.color(Palette.lightOrange, Color.LIGHT_GRAY, Palette.lightishGray, e.fin());
float rot = e.rotation + 90f;
for(int i : Mathf.signs){
float len = (4f + e.finpow() * 8f) * i;
float lr = rot + Mathf.randomSeedRange(e.id + i + 6, 20f * e.fin()) * i;
Draw.rect("casing",
e.x + Angles.trnsx(lr, len) + Mathf.randomSeedRange(e.id + i + 7, 3f * e.fin()),
e.y + Angles.trnsy(lr, len) + Mathf.randomSeedRange(e.id + i + 8, 3f * e.fin()),
2.5f, 4f,
rot + e.fin() * 30f * i + Mathf.randomSeedRange(e.id + i + 9, 40f * e.fin()));
}
Draw.color(Color.LIGHT_GRAY);
for(int i : Mathf.signs){
Angles.randLenVectors(e.id, 4, -e.finpow() * 15f, e.rotation + 90f * i, 25f, (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 2f);
});
}
Draw.color();
});
lancerLaserShoot = new Effect(21f, e -> {
Draw.color(Palette.lancerLaser);
for(int i : Mathf.signs){
Shapes.tri(e.x, e.y, 4f * e.fout(), 29f, e.rotation + 90f * i);
}
Draw.reset();
});
lancerLaserShootSmoke = new Effect(26f, e -> {
Draw.color(Palette.lancerLaser);
Angles.randLenVectors(e.id, 7, 80f, e.rotation, 0f, (x, y) -> {
Lines.lineAngle(e.x + x, e.y + y, Mathf.atan2(x, y), e.fout() * 9f);
});
Draw.reset();
});
lancerLaserCharge = new Effect(38f, e -> {
Draw.color(Palette.lancerLaser);
Angles.randLenVectors(e.id, 2, 1f + 20f * e.fout(), e.rotation, 120f, (x, y) -> {
Lines.lineAngle(e.x + x, e.y + y, Mathf.atan2(x, y), e.fslope() * 3f + 1f);
});
Draw.reset();
});
lancerLaserChargeBegin = new Effect(71f, e -> {
Draw.color(Palette.lancerLaser);
Fill.circle(e.x, e.y, e.fin() * 3f);
Draw.color();
Fill.circle(e.x, e.y, e.fin() * 2f);
});
lightningCharge = new Effect(38f, e -> {
Draw.color(Palette.lancerLaser);
Angles.randLenVectors(e.id, 2, 1f + 20f * e.fout(), e.rotation, 120f, (x, y) -> {
Shapes.tri(e.x + x, e.y + y, e.fslope() * 3f + 1, e.fslope() * 3f + 1, Mathf.atan2(x, y));
});
Draw.reset();
});
lightningShoot = new Effect(12f, e -> {
Draw.color(Color.WHITE, Palette.lancerLaser, e.fin());
Lines.stroke(e.fout() * 1.2f + 0.5f);
Angles.randLenVectors(e.id, 7, 25f * e.finpow(), e.rotation, 50f, (x, y) -> {
Lines.lineAngle(e.x + x, e.y + y, Mathf.atan2(x, y), e.fin() * 5f + 2f);
});
Draw.reset();
});
}
}

View File

@@ -0,0 +1,49 @@
package io.anuke.mindustry.content.fx;
import io.anuke.mindustry.entities.effect.GroundEffectEntity.GroundEffect;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.type.ContentList;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
public class UnitFx extends FxList implements ContentList{
public static Effect vtolHover, unitDrop, unitPickup, pickup;
@Override
public void load(){
vtolHover = new Effect(40f, e -> {
float len = e.finpow() * 10f;
float ang = e.rotation + Mathf.randomSeedRange(e.id, 30f);
Draw.color(Palette.lightFlame, Palette.lightOrange, e.fin());
Fill.circle(e.x + Angles.trnsx(ang, len), e.y + Angles.trnsy(ang, len), 2f * e.fout());
Draw.reset();
});
unitDrop = new GroundEffect(30, e -> {
Draw.color(Palette.lightishGray);
Angles.randLenVectors(e.id, 9, 3 + 20f * e.finpow(), (x, y) -> {
Fill.circle(e.x + x, e.y + y, e.fout() * 4f + 0.4f);
});
Draw.reset();
});
unitPickup = new GroundEffect(18, e -> {
Draw.color(Palette.lightishGray);
Lines.stroke(e.fin() * 2f);
Lines.poly(e.x, e.y, 4, 13f * e.fout());
Draw.reset();
});
pickup = new Effect(18, e -> {
Draw.color(Palette.lightishGray);
Lines.stroke(e.fout() * 2f);
Lines.spikes(e.x, e.y, 1f + e.fin() * 6f, e.fout() * 4f, 6);
Draw.reset();
});
}
}

View File

@@ -0,0 +1,176 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ObjectSet;
import com.badlogic.gdx.utils.OrderedMap;
import com.badlogic.gdx.utils.OrderedSet;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.content.blocks.*;
import io.anuke.mindustry.content.bullets.*;
import io.anuke.mindustry.content.fx.*;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.entities.bullet.Bullet;
import io.anuke.mindustry.entities.bullet.BulletType;
import io.anuke.mindustry.entities.effect.Fire;
import io.anuke.mindustry.entities.effect.ItemDrop;
import io.anuke.mindustry.entities.effect.Lightning;
import io.anuke.mindustry.entities.effect.Puddle;
import io.anuke.mindustry.entities.traits.TypeTrait;
import io.anuke.mindustry.entities.units.UnitType;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.*;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.ColorMapper;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.util.Log;
/**
* Loads all game content.
* Call load() before doing anything with content.
*/
public class ContentLoader{
private static boolean loaded = false;
private static ObjectSet<Array<? extends Content>> contentSet = new OrderedSet<>();
private static OrderedMap<String, Array<Content>> contentMap = new OrderedMap<>();
private static ObjectSet<Consumer<Content>> initialization = new ObjectSet<>();
private static ContentList[] content = {
//effects
new BlockFx(),
new BulletFx(),
new EnvironmentFx(),
new ExplosionFx(),
new Fx(),
new ShootFx(),
new UnitFx(),
//items
new Items(),
//status effects
new StatusEffects(),
//liquids
new Liquids(),
//bullets
new ArtilleryBullets(),
new FlakBullets(),
new MissileBullets(),
new StandardBullets(),
new TurretBullets(),
new WeaponBullets(),
//ammotypes
new AmmoTypes(),
//weapons
new Weapons(),
//mechs
new Mechs(),
//units
new UnitTypes(),
//blocks
new Blocks(),
new DefenseBlocks(),
new DistributionBlocks(),
new ProductionBlocks(),
new TurretBlocks(),
new DebugBlocks(),
new LiquidBlocks(),
new StorageBlocks(),
new UnitBlocks(),
new PowerBlocks(),
new CraftingBlocks(),
new UpgradeBlocks(),
new OreBlocks(),
//not really a content class, but this makes initialization easier
new ColorMapper(),
//recipes
new Recipes(),
};
/**
* Creates all content types.
*/
public static void load(){
if(loaded){
Log.info("Content already loaded, skipping.");
return;
}
registerTypes();
for(ContentList list : content){
list.load();
}
for(ContentList list : content){
if(list.getAll().size != 0){
String type = list.getAll().first().getContentTypeName();
if(!contentMap.containsKey(type)){
contentMap.put(type, new Array<>());
}
contentMap.get(type).addAll(list.getAll());
}
contentSet.add(list.getAll());
}
if(Block.all().size >= 256){
throw new IllegalArgumentException("THE TIME HAS COME. More than 256 blocks have been created.");
}
Log.info("--- CONTENT INFO ---");
Log.info("Blocks loaded: {0}\nItems loaded: {1}\nLiquids loaded: {2}\nUpgrades loaded: {3}\nUnits loaded: {4}\nAmmo types loaded: {5}\nBullet types loaded: {6}\nStatus effects loaded: {7}\nRecipes loaded: {8}\nEffects loaded: {9}\nTotal content classes: {10}",
Block.all().size, Item.all().size, Liquid.all().size, Mech.all().size, UnitType.all().size,
AmmoType.all().size, BulletType.all().size, StatusEffect.all().size, Recipe.all().size, Effects.all().size, content.length);
Log.info("-------------------");
loaded = true;
}
/**
* Initializes all content with the specified function.
*/
public static void initialize(Consumer<Content> callable){
if(initialization.contains(callable)) return;
for(Array<? extends Content> arr : contentSet){
for(Content content : arr){
callable.accept(content);
}
}
initialization.add(callable);
}
public static void dispose(){
//TODO clear all content.
}
public static OrderedMap<String, Array<Content>> getContentMap(){
return contentMap;
}
/**
* Registers sync IDs for all types of sync entities.
* Do not register units here!
*/
private static void registerTypes(){
TypeTrait.registerType(Player.class, Player::new);
TypeTrait.registerType(ItemDrop.class, ItemDrop::new);
TypeTrait.registerType(Fire.class, Fire::new);
TypeTrait.registerType(Puddle.class, Puddle::new);
TypeTrait.registerType(Bullet.class, Bullet::new);
TypeTrait.registerType(Lightning.class, Lightning::new);
}
}

View File

@@ -1,410 +1,409 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Buttons;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.TimeUtils;
import io.anuke.mindustry.content.Mechs;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.game.ContentDatabase;
import io.anuke.mindustry.game.EventType.*;
import io.anuke.mindustry.game.Tutorial;
import io.anuke.mindustry.game.UpgradeInventory;
import io.anuke.mindustry.graphics.Fx;
import io.anuke.mindustry.input.AndroidInput;
import io.anuke.mindustry.input.DefaultKeybinds;
import io.anuke.mindustry.input.DesktopInput;
import io.anuke.mindustry.input.InputHandler;
import io.anuke.mindustry.input.MobileInput;
import io.anuke.mindustry.io.Map;
import io.anuke.mindustry.io.Saves;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.resource.Item;
import io.anuke.mindustry.resource.Weapon;
import io.anuke.mindustry.world.Map;
import io.anuke.ucore.UCore;
import io.anuke.mindustry.type.Recipe;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import io.anuke.ucore.core.*;
import io.anuke.ucore.core.Inputs.DeviceType;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.EntityPhysics;
import io.anuke.ucore.modules.Module;
import io.anuke.ucore.scene.ui.layout.Unit;
import io.anuke.ucore.util.Atlas;
import io.anuke.ucore.util.InputProxy;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.*;
/**Control module.
/**
* Control module.
* Handles all input, saving, keybinds and keybinds.
* Should <i>not</i> handle any game-critical state.
* This class is not created in the headless server.*/
* Should <i>not</i> handle any logic-critical state.
* This class is not created in the headless server.
*/
public class Control extends Module{
private UpgradeInventory upgrades = new UpgradeInventory();
private Tutorial tutorial = new Tutorial();
private boolean hiscore = false;
/** Minimum period of time between the same sound being played.*/
private static final long minSoundPeriod = 100;
private boolean wasPaused = false;
private boolean hiscore = false;
private boolean wasPaused = false;
private Saves saves;
private ContentDatabase db;
private InputHandler[] inputs = {};
private ObjectMap<Sound, Long> soundMap = new ObjectMap<>();
private Saves saves;
private float respawntime;
private InputHandler input;
private InputProxy proxy;
private float controlx, controly;
private boolean controlling;
private Throwable error;
private Input gdxInput;
public Control(){
saves = new Saves();
public Control(){
Inputs.useControllers(!gwt);
saves = new Saves();
db = new ContentDatabase();
Gdx.input.setCatchBackKey(true);
Inputs.useControllers(!gwt);
if(mobile){
input = new AndroidInput();
}else{
input = new DesktopInput();
}
Gdx.input.setCatchBackKey(true);
proxy = new InputProxy(Gdx.input){
@Override
public int getY() {
return controlling ? (int)controly : input.getY();
Effects.setShakeFalloff(10000f);
ContentLoader.initialize(Content::init);
Core.atlas = new Atlas("sprites.atlas");
Core.atlas.setErrorRegion("error");
ContentLoader.initialize(Content::load);
db.load();
gdxInput = Gdx.input;
Sounds.load("shoot.mp3", "place.mp3", "explosion.mp3", "enemyshoot.mp3",
"corexplode.mp3", "break.mp3", "spawn.mp3", "flame.mp3", "die.mp3",
"respawn.mp3", "purchase.mp3", "flame2.mp3", "bigshot.mp3", "laser.mp3", "lasershot.mp3",
"ping.mp3", "tesla.mp3", "waveend.mp3", "railgun.mp3", "blast.mp3", "bang2.mp3");
Sounds.setFalloff(9000f);
Sounds.setPlayer((sound, volume) -> {
long time = TimeUtils.millis();
long value = soundMap.get(sound, 0L);
if(TimeUtils.timeSinceMillis(value) >= minSoundPeriod){
threads.run(() -> sound.play(volume));
soundMap.put(sound, time);
}
@Override
public int getX() {
return controlling ? (int)controlx : input.getX();
}
@Override
public int getY(int pointer) {
return pointer == 0 ? getY() : super.getY(pointer);
}
@Override
public int getX(int pointer) {
return pointer == 0 ? getX() : super.getX(pointer);
}
};
Inputs.addProcessor(input);
Effects.setShakeFalloff(10000f);
Core.atlas = new Atlas("sprites.atlas");
for(Item item : Item.getAllItems()){
item.init();
}
Sounds.load("shoot.mp3", "place.mp3", "explosion.mp3", "enemyshoot.mp3",
"corexplode.mp3", "break.mp3", "spawn.mp3", "flame.mp3", "die.mp3",
"respawn.mp3", "purchase.mp3", "flame2.mp3", "bigshot.mp3", "laser.mp3", "lasershot.mp3",
"ping.mp3", "tesla.mp3", "waveend.mp3", "railgun.mp3", "blast.mp3", "bang2.mp3");
Sounds.setFalloff(9000f);
});
Musics.load("1.mp3", "2.mp3", "3.mp3", "4.mp3", "5.mp3", "6.mp3");
DefaultKeybinds.load();
for(int i = 0; i < saveSlots; i ++){
Settings.defaults("save-" + i + "-autosave", !gwt);
Settings.defaults("save-" + i + "-name", "untitled");
Settings.defaults("save-" + i + "-data", "empty");
}
Settings.defaultList(
"ip", "localhost",
"port", port + "",
"color-0", Color.rgba8888(playerColors[8]),
"color-1", Color.rgba8888(playerColors[11]),
"color-2", Color.rgba8888(playerColors[13]),
"color-3", Color.rgba8888(playerColors[9]),
"name", "player",
"lastBuild", 0
);
Settings.defaultList(
"ip", "localhost",
"port", port+"",
"name", mobile || gwt ? "player" : UCore.getProperty("user.name"),
"servers", "",
"color", Color.rgba8888(playerColors[8]),
"lastVersion", "3.2",
"lastBuild", 0
);
KeyBinds.load();
KeyBinds.load();
addPlayer(0);
for(Map map : world.maps().list()){
Settings.defaults("hiscore" + map.name, 0);
}
saves.load();
player = new Player();
player.name = Settings.getString("name");
player.isAndroid = mobile;
player.color.set(Settings.getInt("color"));
player.isLocal = true;
Events.on(StateChangeEvent.class, (from, to) -> {
if((from == State.playing && to == State.menu) || (from == State.menu && to != State.menu)){
Timers.runTask(5f, Platform.instance::updateRPC);
}
});
saves.load();
Events.on(PlayEvent.class, () -> {
for(Player player : players){
player.add();
}
Events.on(StateChangeEvent.class, (from, to) -> {
if((from == State.playing && to == State.menu) || (from == State.menu && to != State.menu)){
Timers.runTask(5f, Platform.instance::updateRPC);
}
});
Events.on(PlayEvent.class, () -> {
renderer.clearTiles();
player.set(world.getSpawnX(), world.getSpawnY());
Core.camera.position.set(player.x, player.y, 0);
ui.hudfrag.updateItems();
state.set(State.playing);
});
Events.on(ResetEvent.class, () -> {
upgrades.reset();
player.weaponLeft = player.weaponRight = Weapon.blaster;
player.add();
player.heal();
respawntime = -1;
hiscore = false;
ui.hudfrag.updateItems();
ui.hudfrag.updateWeapons();
ui.hudfrag.fadeRespawn(false);
});
Events.on(WaveEvent.class, () -> {
Sounds.play("spawn");
int last = Settings.getInt("hiscore" + world.getMap().name, 0);
if(state.wave > last && !state.mode.infiniteResources && !state.mode.disableWaveTimer){
Settings.putInt("hiscore" + world.getMap().name, state.wave);
Settings.save();
hiscore = true;
}
Platform.instance.updateRPC();
});
Events.on(GameOverEvent.class, () -> {
Effects.shake(5, 6, Core.camera.position.x, Core.camera.position.y);
Sounds.play("corexplode");
for(int i = 0; i < 16; i ++){
Timers.run(i*2, ()-> Effects.effect(Fx.explosion, world.getCore().worldx()+Mathf.range(40), world.getCore().worldy()+Mathf.range(40)));
}
Effects.effect(Fx.coreexplosion, world.getCore().worldx(), world.getCore().worldy());
ui.restart.show();
Timers.runTask(30f, () -> state.set(State.menu));
});
}
//FIXME figure out what's causing this problem in the first place
public void triggerInputUpdate(){
Gdx.input = proxy;
}
public void setError(Throwable error){
this.error = error;
}
public UpgradeInventory upgrades() {
return upgrades;
}
public Saves getSaves(){
return saves;
}
public boolean showCursor(){
return controlling;
}
public InputHandler input(){
return input;
}
public void playMap(Map map){
ui.loadfrag.show();
saves.resetSave();
Timers.runTask(10, () -> {
logic.reset();
world.loadMap(map);
logic.play();
});
Timers.runTask(18, () -> ui.loadfrag.hide());
}
public boolean isHighScore(){
return hiscore;
}
public float getRespawnTime(){
return respawntime;
}
public void setRespawnTime(float respawntime){
this.respawntime = respawntime;
}
public Tutorial tutorial(){
return tutorial;
}
private void checkOldUser(){
boolean hasPlayed = false;
for(Map map : world.maps().getAllMaps()){
if(Settings.getInt("hiscore" + map.name) != 0){
hasPlayed = true;
break;
}
}
if(hasPlayed && Settings.getString("lastVersion").equals("3.2")){
Timers.runTask(1f, () -> ui.showInfo("$text.changes"));
Settings.putString("lastVersion", "3.3");
Settings.save();
}
}
@Override
public void dispose(){
Platform.instance.onGameExit();
Net.dispose();
}
@Override
public void pause(){
wasPaused = state.is(State.paused);
if(state.is(State.playing)) state.set(State.paused);
}
@Override
public void resume(){
if(state.is(State.paused) && !wasPaused){
state.set(State.playing);
}
}
});
@Override
public void init(){
Timers.run(1f, Musics::shuffleAll);
Events.on(WorldLoadGraphicsEvent.class, () -> {
if(mobile){
Core.camera.position.set(players[0].x, players[0].y, 0);
}
});
Entities.initPhysics();
Entities.collisions().setCollider(tilesize, world::solid);
Platform.instance.updateRPC();
checkOldUser();
}
@Override
public void update(){
if(error != null){
throw new RuntimeException(error);
}
Gdx.input = proxy;
if(Inputs.keyTap("console")){
console = !console;
}
if(KeyBinds.getSection("default").device.type == DeviceType.controller){
if(Inputs.keyTap("select")){
Inputs.getProcessor().touchDown(Gdx.input.getX(), Gdx.input.getY(), 0, Buttons.LEFT);
Events.on(ResetEvent.class, () -> {
for(Player player : players){
player.reset();
}
if(Inputs.keyRelease("select")){
Inputs.getProcessor().touchUp(Gdx.input.getX(), Gdx.input.getY(), 0, Buttons.LEFT);
hiscore = false;
saves.resetSave();
});
Events.on(WaveEvent.class, () -> {
int last = Settings.getInt("hiscore" + world.getMap().name, 0);
if(state.wave > last && !state.mode.infiniteResources && !state.mode.disableWaveTimer){
Settings.putInt("hiscore" + world.getMap().name, state.wave);
Settings.save();
hiscore = true;
}
float xa = Inputs.getAxis("cursor_x");
float ya = Inputs.getAxis("cursor_y");
Platform.instance.updateRPC();
});
if(Math.abs(xa) > controllerMin || Math.abs(ya) > controllerMin) {
float scl = Settings.getInt("sensitivity")/100f * Unit.dp.scl(1f);
controlx += xa*baseControllerSpeed*scl;
controly -= ya*baseControllerSpeed*scl;
controlling = true;
Events.on(GameOverEvent.class, () -> {
Effects.shake(5, 6, Core.camera.position.x, Core.camera.position.y);
Gdx.input.setCursorCatched(true);
//TODO game over effect
ui.restart.show();
Inputs.getProcessor().touchDragged(Gdx.input.getX(), Gdx.input.getY(), 0);
}
Timers.runTask(30f, () -> state.set(State.menu));
});
controlx = Mathf.clamp(controlx, 0, Gdx.graphics.getWidth());
controly = Mathf.clamp(controly, 0, Gdx.graphics.getHeight());
Events.on(WorldLoadEvent.class, () -> threads.runGraphics(() -> Events.fire(WorldLoadGraphicsEvent.class)));
}
if(Gdx.input.getDeltaX() > 1 || Gdx.input.getDeltaY() > 1) {
controlling = false;
Gdx.input.setCursorCatched(false);
}
}else{
controlling = false;
Gdx.input.setCursorCatched(false);
public void addPlayer(int index){
if(players.length != index + 1){
Player[] old = players;
players = new Player[index + 1];
System.arraycopy(old, 0, players, 0, old.length);
}
if(!controlling){
controlx = Gdx.input.getX();
controly = Gdx.input.getY();
if(inputs.length != index + 1){
InputHandler[] oldi = inputs;
inputs = new InputHandler[index + 1];
System.arraycopy(oldi, 0, inputs, 0, oldi.length);
}
Player setTo = (index == 0 ? null : players[0]);
Player player = new Player();
player.name = Settings.getString("name");
player.mech = mobile ? Mechs.starterMobile : Mechs.starterDesktop;
player.color.set(Settings.getInt("color-" + index));
player.isLocal = true;
player.playerIndex = index;
player.isMobile = mobile;
players[index] = player;
if(setTo != null){
player.set(setTo.x, setTo.y);
}
if(!state.is(State.menu)){
player.add();
}
InputHandler input;
if(mobile){
input = new MobileInput(player);
}else{
input = new DesktopInput(player);
}
inputs[index] = input;
Inputs.addProcessor(input);
}
public void removePlayer(){
players[players.length - 1].remove();
inputs[inputs.length - 1].remove();
Player[] old = players;
players = new Player[players.length - 1];
System.arraycopy(old, 0, players, 0, players.length);
InputHandler[] oldi = inputs;
inputs = new InputHandler[inputs.length - 1];
System.arraycopy(oldi, 0, inputs, 0, inputs.length);
}
public ContentDatabase database(){
return db;
}
public Input gdxInput(){
return gdxInput;
}
public void setError(Throwable error){
this.error = error;
}
public Saves getSaves(){
return saves;
}
public InputHandler input(int index){
return inputs[index];
}
public void triggerUpdateInput(){
//Gdx.input = proxy;
}
public void playMap(Map map){
ui.loadfrag.show();
Timers.run(5f, () ->
threads.run(() -> {
logic.reset();
world.loadMap(map);
logic.play();
Gdx.app.postRunnable(ui.loadfrag::hide);
}));
}
public boolean isHighScore(){
return hiscore;
}
private void checkUnlockableBlocks(){
TileEntity entity = players[0].getClosestCore();
if(entity == null) return;
entity.items.forEach((item, amount) -> control.database().unlockContent(item));
if(players[0].inventory.hasItem()){
control.database().unlockContent(players[0].inventory.getItem().item);
}
for(int i = 0; i < Recipe.all().size; i++){
Recipe recipe = Recipe.all().get(i);
if(!recipe.debugOnly && entity.items.has(recipe.requirements, 1.4f)){
if(control.database().unlockContent(recipe)){
ui.hudfrag.showUnlock(recipe);
}
}
}
}
@Override
public void dispose(){
Platform.instance.onGameExit();
ContentLoader.dispose();
Net.dispose();
ui.editor.dispose();
inputs = new InputHandler[]{};
players = new Player[]{};
}
@Override
public void pause(){
wasPaused = state.is(State.paused);
if(state.is(State.playing)) state.set(State.paused);
}
@Override
public void resume(){
if(state.is(State.paused) && !wasPaused){
state.set(State.playing);
}
}
@Override
public void init(){
EntityPhysics.initPhysics();
Platform.instance.updateRPC();
if(!Settings.has("4.0-warning")){
Settings.putBool("4.0-warning", true);
Timers.run(5f, () -> {
FloatingDialog dialog = new FloatingDialog("[orange]WARNING![]");
dialog.buttons().addButton("$text.ok", dialog::hide).size(100f, 60f);
dialog.content().add("The beta version you are about to play should be considered very unstable, and is [accent]not representative of the final 4.0 release.[]\n\n " +
"A large portion of content is still unimplemented. \nAll current art and UI is temporary, and will be re-drawn before release. " +
"\n\n[accent]Saves and maps may be corrupted without warning between updates.[] You have been warned!").wrap().width(500f);
dialog.show();
});
}
if(!Settings.has("4.0-no-sound")){
Settings.putBool("4.0-no-sound", true);
Timers.run(4f, () -> {
FloatingDialog dialog = new FloatingDialog("[orange]Attention![]");
dialog.buttons().addButton("$text.ok", dialog::hide).size(100f, 60f);
dialog.content().add("You might have noticed that 4.0 does not have any sound.\nThis is [orange]intentional![] Sound will be added in a later update.\n\n[LIGHT_GRAY](now stop reporting this as a bug)").wrap().width(500f);
dialog.show();
});
}
}
/** Called from main logic thread.*/
public void runUpdateLogic(){
if(!state.is(State.menu)){
renderer.minimap().updateUnitArray();
}
}
@Override
public void update(){
if(error != null){
throw new RuntimeException(error);
}
if(Inputs.keyTap("console")){
console = !console;
}
saves.update();
if(state.inventory.isUpdated() && (Timers.get("updateItems", 8) || state.is(State.paused))){
ui.hudfrag.updateItems();
state.inventory.setUpdated(false);
}
triggerUpdateInput();
if(!state.is(State.menu)){
input.update();
for(InputHandler inputHandler : inputs){
inputHandler.updateController();
}
if(Inputs.keyTap("pause") && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
if(!state.is(State.menu)){
for(InputHandler input : inputs){
input.update();
}
//check unlocks every 2 seconds
if(!state.mode.infiniteResources && Timers.get("timerCheckUnlock", 120)){
checkUnlockableBlocks();
//save if the db changed, but don't save unlocks
if(db.isDirty() && !debug){
db.save();
}
}
if(Inputs.keyTap("pause") && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
state.set(state.is(State.playing) ? State.paused : State.playing);
}
}
if(Inputs.keyTap("menu")){
if(state.is(State.paused)){
ui.paused.hide();
if(Inputs.keyTap("menu")){
if(state.is(State.paused)){
ui.paused.hide();
state.set(State.playing);
}else if (!ui.restart.isShown()){
if(ui.chatfrag.chatOpen()) {
ui.chatfrag.hide();
}else{
ui.paused.show();
}else if(!ui.restart.isShown()){
if(ui.chatfrag.chatOpen()){
ui.chatfrag.hide();
}else{
ui.paused.show();
state.set(State.paused);
}
}
}
}
}
}
if(!state.is(State.paused) || Net.active()){
Entities.update(effectGroup);
if(respawntime > 0){
respawntime -= Timers.delta();
if(respawntime <= 0){
player.set(world.getSpawnX(), world.getSpawnY());
player.heal();
player.add();
Effects.sound("respawn");
ui.hudfrag.fadeRespawn(false);
}
}
if(tutorial.active()){
tutorial.update();
}
}
}else{
if(!state.is(State.paused) || Net.active()){
Timers.update();
}
}
}
if(!state.is(State.paused) || Net.active()){
Entities.update(effectGroup);
Entities.update(groundEffectGroup);
}
}else{
if(!state.is(State.paused) || Net.active()){
Timers.update();
}
}
}
}

View File

@@ -1,40 +1,37 @@
package io.anuke.mindustry.core;
import io.anuke.mindustry.ai.WaveSpawner;
import io.anuke.mindustry.game.Difficulty;
import io.anuke.mindustry.game.EventType.StateChangeEvent;
import io.anuke.mindustry.game.GameMode;
import io.anuke.mindustry.game.Inventory;
import io.anuke.mindustry.game.TeamInfo;
import io.anuke.ucore.core.Events;
public class GameState{
private State state = State.menu;
public int wave = 1;
public float wavetime;
public boolean gameOver = false;
public GameMode mode = GameMode.waves;
public Difficulty difficulty = Difficulty.normal;
public boolean friendlyFire;
public WaveSpawner spawner = new WaveSpawner();
public TeamInfo teams = new TeamInfo();
private State state = State.menu;
public final Inventory inventory = new Inventory();
public void set(State astate){
Events.fire(StateChangeEvent.class, state, astate);
state = astate;
}
public int wave = 1;
public int lastUpdated = -1;
public float wavetime;
public float extrawavetime;
public int enemies = 0;
public boolean gameOver = false;
public GameMode mode = GameMode.waves;
public Difficulty difficulty = Difficulty.normal;
public boolean friendlyFire;
public void set(State astate){
Events.fire(StateChangeEvent.class, state, astate);
state = astate;
}
public boolean is(State astate){
return state == astate;
}
public boolean is(State astate){
return state == astate;
}
public State getState(){
return state;
}
public enum State{
paused, playing, menu
}
public State getState(){
return state;
}
public enum State{
paused, playing, menu
}
}

View File

@@ -1,160 +1,173 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.mindustry.game.EnemySpawn;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.game.EventType.GameOverEvent;
import io.anuke.mindustry.game.EventType.PlayEvent;
import io.anuke.mindustry.game.EventType.ResetEvent;
import io.anuke.mindustry.game.EventType.WaveEvent;
import io.anuke.mindustry.game.SpawnPoint;
import io.anuke.mindustry.game.WaveCreator;
import io.anuke.mindustry.graphics.Fx;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.game.TeamInfo;
import io.anuke.mindustry.game.TeamInfo.TeamData;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.net.NetEvents;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.ItemType;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.ProductionBlocks;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Events;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.EntityPhysics;
import io.anuke.ucore.modules.Module;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.*;
/**Logic module.
/**
* Logic module.
* Handles all logic for entities and waves.
* Handles game state events.
* Does not store any game state itself.
*
* <p>
* This class should <i>not</i> call any outside methods to change state of modules, but instead fire events.
*/
public class Logic extends Module {
private final Array<EnemySpawn> spawns = WaveCreator.getSpawns();
public class Logic extends Module{
public boolean doUpdate = true;
public Logic(){
state = new GameState();
}
@Override
public void init(){
Entities.initPhysics();
Entities.collisions().setCollider(tilesize, world::solid);
EntityPhysics.initPhysics();
EntityPhysics.collisions().setCollider(tilesize, world::solid);
}
public void play(){
state.set(State.playing);
state.wavetime = wavespace * state.difficulty.timeScaling * 2;
if(state.mode.infiniteResources){
state.inventory.fill();
//fill inventory with items for debugging
for(TeamData team : state.teams.getTeams()){
for(Tile tile : team.cores){
if(debug){
for(Item item : Item.all()){
if(item.type == ItemType.material){
tile.entity.items.add(item, 1000);
}
}
}else{
tile.entity.items.add(Items.tungsten, 50);
tile.entity.items.add(Items.lead, 20);
}
}
}
Events.fire(PlayEvent.class);
}
public void reset(){
state.wave = 1;
state.extrawavetime = maxwavespace * state.difficulty.maxTimeScaling;
state.wavetime = wavespace * state.difficulty.timeScaling;
state.enemies = 0;
state.lastUpdated = -1;
state.gameOver = false;
state.inventory.clearItems();
state.teams = new TeamInfo();
state.teams.add(Team.blue, true);
state.teams.add(Team.red, false);
Timers.clear();
Entities.clear();
TileEntity.sleepingEntities = 0;
Events.fire(ResetEvent.class);
}
public void runWave(){
if(state.lastUpdated < state.wave + 1){
world.pathfinder().resetPaths();
state.lastUpdated = state.wave + 1;
}
for(EnemySpawn spawn : spawns){
Array<SpawnPoint> spawns = world.getSpawns();
for(int lane = 0; lane < spawns.size; lane ++){
int fl = lane;
Tile tile = spawns.get(lane).start;
int spawnamount = spawn.evaluate(state.wave, lane);
for(int i = 0; i < spawnamount; i ++){
float range = 12f;
Timers.runTask(i*5f, () -> {
Enemy enemy = new Enemy(spawn.type);
enemy.set(tile.worldx() + Mathf.range(range), tile.worldy() + Mathf.range(range));
enemy.lane = fl;
enemy.tier = spawn.tier(state.wave, fl);
enemy.add();
Effects.effect(Fx.spawn, enemy);
state.enemies ++;
});
}
}
}
state.wave ++;
state.spawner.spawnEnemies();
state.wave++;
state.wavetime = wavespace * state.difficulty.timeScaling;
state.extrawavetime = maxwavespace * state.difficulty.maxTimeScaling;
Events.fire(WaveEvent.class);
}
private void checkGameOver(){
boolean gameOver = true;
for(TeamData data : state.teams.getTeams(true)){
if(data.cores.size > 0){
gameOver = false;
break;
}
}
if(gameOver && !state.gameOver){
state.gameOver = true;
Events.fire(GameOverEvent.class);
}
}
@Override
public void update(){
if(threads.isEnabled() && !threads.isOnThread()) return;
if(Vars.control != null){
control.runUpdateLogic();
}
if(!state.is(State.menu)){
if(control != null) control.triggerInputUpdate();
if(control != null) control.triggerUpdateInput();
if(!state.is(State.paused) || Net.active()){
Timers.update();
}
if(!Net.client())
world.pathfinder().update();
if(world.getCore() != null && world.getCore().block() != ProductionBlocks.core && !state.gameOver){
state.gameOver = true;
if(Net.server()) NetEvents.handleGameOver();
Events.fire(GameOverEvent.class);
if(!world.isInvalidMap()){
checkGameOver();
}
if(!state.is(State.paused) || Net.active()){
if(!state.mode.disableWaveTimer){
if(state.enemies <= 0){
if(!world.getMap().name.equals("tutorial")) state.wavetime -= Timers.delta();
if(state.lastUpdated < state.wave + 1 && state.wavetime < aheadPathfinding){ //start updating beforehand
world.pathfinder().resetPaths();
state.lastUpdated = state.wave + 1;
}
}else if(!world.getMap().name.equals("tutorial")){
state.extrawavetime -= Timers.delta();
}
state.wavetime -= Timers.delta();
}
if(!Net.client() && (state.wavetime <= 0 || state.extrawavetime <= 0)){
if(!Net.client() && state.wavetime <= 0){
runWave();
}
Entities.update(Entities.defaultGroup());
if(!Entities.defaultGroup().isEmpty())
throw new RuntimeException("Do not add anything to the default group!");
Entities.update(bulletGroup);
Entities.update(enemyGroup);
for(EntityGroup group : unitGroups){
Entities.update(group);
}
Entities.update(puddleGroup);
Entities.update(tileGroup);
Entities.update(fireGroup);
Entities.update(shieldGroup);
Entities.update(playerGroup);
Entities.update(itemGroup);
Entities.collideGroups(bulletGroup, enemyGroup);
Entities.collideGroups(bulletGroup, playerGroup);
//effect group only contains item drops in the headless version, update it!
if(headless){
Entities.update(effectGroup);
}
for(EntityGroup group : unitGroups){
if(!group.isEmpty()){
EntityPhysics.collideGroups(bulletGroup, group);
}
}
EntityPhysics.collideGroups(bulletGroup, playerGroup);
EntityPhysics.collideGroups(itemGroup, playerGroup);
world.pathfinder().update();
}
}
}

View File

@@ -1,78 +1,138 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.Base64Coder;
import com.badlogic.gdx.utils.IntSet;
import com.badlogic.gdx.utils.TimeUtils;
import io.anuke.mindustry.Vars;
import io.anuke.annotations.Annotations.PacketPriority;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.annotations.Annotations.Variant;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.Bullet;
import io.anuke.mindustry.entities.BulletType;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.entities.SyncEntity;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.entities.traits.TypeTrait;
import io.anuke.mindustry.gen.Call;
import io.anuke.mindustry.gen.RemoteReadClient;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.net.Net.SendMode;
import io.anuke.mindustry.net.NetworkIO;
import io.anuke.mindustry.net.Packets.*;
import io.anuke.mindustry.resource.Item;
import io.anuke.mindustry.resource.Upgrade;
import io.anuke.mindustry.resource.UpgradeRecipes;
import io.anuke.mindustry.resource.Weapon;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Map;
import io.anuke.mindustry.world.Placement;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.ProductionBlocks;
import io.anuke.ucore.core.Effects;
import io.anuke.mindustry.net.TraceInfo;
import io.anuke.ucore.core.Settings;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.BaseBulletType;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.Entity;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.io.ReusableByteArrayInputStream;
import io.anuke.ucore.io.delta.DEZDecoder;
import io.anuke.ucore.modules.Module;
import io.anuke.ucore.util.Log;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Pooling;
import io.anuke.ucore.util.Timer;
import java.nio.ByteBuffer;
import java.io.DataInputStream;
import java.util.Arrays;
import java.util.Random;
import static io.anuke.mindustry.Vars.*;
public class NetClient extends Module {
private final static float dataTimeout = 60*18; //18 seconds timeout
public class NetClient extends Module{
private final static float dataTimeout = 60 * 18;
private final static float playerSyncTime = 2;
private final static int maxRequests = 50;
private Timer timer = new Timer(5);
/**
* Whether the client is currently connecting.
*/
private boolean connecting = false;
private boolean kicked = false;
private IntSet recieved = new IntSet();
private IntMap<Entity> recent = new IntMap<>();
private int requests = 0;
private float timeoutTime = 0f; //data timeout counter
/**
* If true, no message will be shown on disconnect.
*/
private boolean quiet = false;
/**
* Counter for data timeout.
*/
private float timeoutTime = 0f;
/**
* Last sent client snapshot ID.
*/
private int lastSent;
/**
* Last snapshot ID recieved.
*/
private int lastSnapshotBaseID = -1;
/**
* Last snapshot recieved.
*/
private byte[] lastSnapshotBase;
/**
* Current snapshot that is being built from chinks.
*/
private byte[] currentSnapshot;
/**
* Array of recieved chunk statuses.
*/
private boolean[] recievedChunks;
/**
* Counter of how many chunks have been recieved.
*/
private int recievedChunkCounter;
/**
* ID of snapshot that is currently being constructed.
*/
private int currentSnapshotID = -1;
/**
* Decoder for uncompressing snapshots.
*/
private DEZDecoder decoder = new DEZDecoder();
/**
* List of entities that were removed, and need not be added while syncing.
*/
private IntSet removed = new IntSet();
/**
* Byte stream for reading in snapshots.
*/
private ReusableByteArrayInputStream byteStream = new ReusableByteArrayInputStream();
private DataInputStream dataStream = new DataInputStream(byteStream);
public NetClient(){
Net.handleClient(Connect.class, packet -> {
Player player = players[0];
player.isAdmin = false;
Net.setClientLoaded(false);
recieved.clear();
recent.clear();
removed.clear();
timeoutTime = 0f;
connecting = true;
kicked = false;
quiet = false;
lastSent = 0;
lastSnapshotBase = null;
currentSnapshot = null;
currentSnapshotID = -1;
lastSnapshotBaseID = -1;
ui.chatfrag.clearMessages();
ui.loadfrag.hide();
ui.loadfrag.show("$text.connecting.data");
ui.loadfrag.setButton(() -> {
ui.loadfrag.hide();
connecting = false;
quiet = true;
Net.disconnect();
});
Entities.clear();
ConnectPacket c = new ConnectPacket();
c.name = player.name;
c.android = mobile;
c.mobile = mobile;
c.color = Color.rgba8888(player.color);
c.usid = getUsid(packet.addressTCP);
c.uuid = Platform.instance.getUUID();
if(c.uuid == null){
@@ -86,7 +146,7 @@ public class NetClient extends Module {
});
Net.handleClient(Disconnect.class, packet -> {
if (kicked) return;
if(quiet) return;
Timers.runTask(3f, ui.loadfrag::hide);
@@ -98,226 +158,177 @@ public class NetClient extends Module {
Platform.instance.updateRPC();
});
Net.handleClient(WorldData.class, data -> {
Net.handleClient(WorldStream.class, data -> {
Log.info("Recieved world data: {0} bytes.", data.stream.available());
NetworkIO.loadWorld(data.stream);
player.set(world.getSpawnX(), world.getSpawnY());
finishConnecting();
});
Net.handleClient(CustomMapPacket.class, packet -> {
Log.info("Recieved custom map: {0} bytes.", packet.stream.available());
//custom map is always sent before world data
Map map = NetworkIO.loadMap(packet.stream);
world.maps().setNetworkMap(map);
MapAckPacket ack = new MapAckPacket();
Net.send(ack, SendMode.tcp);
Net.handleClient(InvokePacket.class, packet -> {
packet.writeBuffer.position(0);
RemoteReadClient.readPacket(packet.writeBuffer, packet.type);
});
}
Net.handleClient(SyncPacket.class, packet -> {
if (connecting) return;
int players = 0;
int enemies = 0;
@Remote(variants = Variant.one, priority = PacketPriority.high)
public static void onKick(KickReason reason){
netClient.disconnectQuietly();
state.set(State.menu);
if(!reason.quiet) ui.showError("$text.server.kicked." + reason.name());
ui.loadfrag.hide();
}
ByteBuffer data = ByteBuffer.wrap(packet.data);
long time = data.getLong();
@Remote(variants = Variant.one)
public static void onPositionSet(float x, float y){
players[0].x = x;
players[0].y = y;
}
byte groupid = data.get();
@Remote(variants = Variant.one)
public static void onTraceInfo(TraceInfo info){
Player player = playerGroup.getByID(info.playerid);
ui.traces.show(player, info);
}
EntityGroup<?> group = Entities.getGroup(groupid);
@Remote
public static void onPlayerDisconnect(int playerid){
playerGroup.removeByID(playerid);
}
while (data.position() < data.capacity()) {
int id = data.getInt();
@Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
public static void onSnapshot(byte[] chunk, int snapshotID, short chunkID, int totalLength, int base){
if(NetServer.showSnapshotSize)
Log.info("Recieved snapshot: len {0} ID {1} chunkID {2} totalLength {3} base {4} client-base {5}", chunk.length, snapshotID, chunkID, totalLength, base, netClient.lastSnapshotBaseID);
SyncEntity entity = (SyncEntity) group.getByID(id);
//skip snapshot IDs that have already been recieved OR snapshots that are too far in front
if(snapshotID < netClient.lastSnapshotBaseID || base != netClient.lastSnapshotBaseID){
if(NetServer.showSnapshotSize) Log.info("//SKIP SNAPSHOT");
return;
}
if(entity instanceof Player) players ++;
if(entity instanceof Enemy) enemies ++;
try{
byte[] snapshot;
if (entity == null || id == player.id) {
if (id != player.id && requests < maxRequests) {
EntityRequestPacket req = new EntityRequestPacket();
req.id = id;
req.group = groupid;
Net.send(req, SendMode.udp);
requests ++;
//total length exceeds that needed to hold one snapshot, therefore, it is split into chunks
if(totalLength > NetServer.maxSnapshotSize){
//total amount of chunks to recieve
int totalChunks = Mathf.ceil((float) totalLength / NetServer.maxSnapshotSize);
//reset status when a new snapshot sending begins
if(netClient.currentSnapshotID != snapshotID){
netClient.currentSnapshotID = snapshotID;
netClient.currentSnapshot = new byte[totalLength];
netClient.recievedChunkCounter = 0;
netClient.recievedChunks = new boolean[totalChunks];
}
//if this chunk hasn't been recieved yet...
if(!netClient.recievedChunks[chunkID]){
netClient.recievedChunks[chunkID] = true;
netClient.recievedChunkCounter++; //update recieved status
//copy the recieved bytes into the holding array
System.arraycopy(chunk, 0, netClient.currentSnapshot, chunkID * NetServer.maxSnapshotSize,
Math.min(NetServer.maxSnapshotSize, totalLength - chunkID * NetServer.maxSnapshotSize));
}
//when all chunks have been recieved, begin
if(netClient.recievedChunkCounter >= totalChunks){
snapshot = netClient.currentSnapshot;
}else{
return;
}
}else{
snapshot = chunk;
}
if(NetServer.showSnapshotSize)
Log.info("Finished recieving snapshot ID {0} length {1}", snapshotID, chunk.length);
byte[] result;
int length;
if(base == -1){ //fresh snapshot
result = snapshot;
length = snapshot.length;
netClient.lastSnapshotBase = Arrays.copyOf(snapshot, snapshot.length);
}else{ //otherwise, last snapshot must not be null, decode it
if(NetServer.showSnapshotSize)
Log.info("Base size: {0} Patch size: {1}", netClient.lastSnapshotBase.length, snapshot.length);
netClient.decoder.init(netClient.lastSnapshotBase, snapshot);
result = netClient.decoder.decode();
length = netClient.decoder.getDecodedLength();
//set last snapshot to a copy to prevent issues
netClient.lastSnapshotBase = Arrays.copyOf(result, length);
}
netClient.lastSnapshotBaseID = snapshotID;
//set stream bytes to begin snapshot reaeding
netClient.byteStream.setBytes(result, 0, length);
//get data input for reading from the stream
DataInputStream input = netClient.dataStream;
//read wave info
state.wavetime = input.readFloat();
state.wave = input.readInt();
byte cores = input.readByte();
for(int i = 0; i < cores; i++){
int pos = input.readInt();
world.tile(pos).entity.items.read(input);
}
long timestamp = input.readLong();
byte totalGroups = input.readByte();
//for each group...
for(int i = 0; i < totalGroups; i++){
//read group info
byte groupID = input.readByte();
short amount = input.readShort();
EntityGroup group = Entities.getGroup(groupID);
//go through each entity
for(int j = 0; j < amount; j++){
int position = netClient.byteStream.position(); //save position to check read/write correctness
int id = input.readInt();
byte typeID = input.readByte();
SyncTrait entity = (SyncTrait) group.getByID(id);
boolean add = false;
//entity must not be added yet, so create it
if(entity == null){
entity = (SyncTrait) TypeTrait.getTypeByID(typeID).get(); //create entity from supplier
entity.resetID(id);
if(!netClient.isEntityUsed(entity.getID())){
add = true;
}
}
//read the entity
entity.read(input, timestamp);
byte readLength = input.readByte();
if(netClient.byteStream.position() - position - 1 != readLength){
throw new RuntimeException("Error reading entity of type '" + group.getType() + "': Read length mismatch [write=" + readLength + ", read=" + (netClient.byteStream.position() - position - 1) + "]");
}
if(add){
entity.add();
netClient.addRemovedEntity(entity.getID());
}
data.position(data.position() + SyncEntity.getWriteSize((Class<? extends SyncEntity>) group.getType()));
} else {
entity.read(data, time);
}
}
if(debugNet){
clientDebug.setSyncDebug(players, enemies);
}
});
//confirm that snapshot has been recieved
netClient.lastSnapshotBaseID = snapshotID;
Net.handleClient(StateSyncPacket.class, packet -> {
System.arraycopy(packet.items, 0, state.inventory.getItems(), 0, packet.items.length);
state.enemies = packet.enemies;
state.wavetime = packet.countdown;
state.wave = packet.wave;
ui.hudfrag.updateItems();
});
Net.handleClient(BlockLogRequestPacket.class, packet -> {
currentEditLogs = packet.editlogs;
});
Net.handleClient(PlacePacket.class, (packet) -> {
Placement.placeBlock(packet.x, packet.y, Block.getByID(packet.block), packet.rotation, true, Timers.get("placeblocksound", 10));
if(packet.playerid == player.id){
Tile tile = world.tile(packet.x, packet.y);
if(tile != null) Block.getByID(packet.block).placed(tile);
}
});
Net.handleClient(BreakPacket.class, (packet) ->
Placement.breakBlock(packet.x, packet.y, true, Timers.get("breakblocksound", 10)));
Net.handleClient(EntitySpawnPacket.class, packet -> {
EntityGroup group = packet.group;
//duplicates.
if (group.getByID(packet.entity.id) != null ||
recieved.contains(packet.entity.id)) return;
recieved.add(packet.entity.id);
recent.put(packet.entity.id, packet.entity);
packet.entity.add();
Log.info("Recieved entity {0}", packet.entity.id);
});
Net.handleClient(EnemyDeathPacket.class, packet -> {
Enemy enemy = enemyGroup.getByID(packet.id);
if (enemy != null){
enemy.type.onDeath(enemy, true);
}else if(recent.get(packet.id) != null){
recent.get(packet.id).remove();
}else{
Log.err("Got remove for null entity! {0}", packet.id);
}
recieved.add(packet.id);
});
Net.handleClient(BulletPacket.class, packet -> {
//TODO shoot effects for enemies, clientside as well as serverside
BulletType type = (BulletType) BaseBulletType.getByID(packet.type);
Entity owner = enemyGroup.getByID(packet.owner);
new Bullet(type, owner, packet.x, packet.y, packet.angle).add();
});
Net.handleClient(BlockDestroyPacket.class, packet -> {
Tile tile = world.tile(packet.position % world.width(), packet.position / world.width());
if (tile != null && tile.entity != null) {
tile.entity.onDeath(true);
}
});
Net.handleClient(BlockUpdatePacket.class, packet -> {
Tile tile = world.tile(packet.position % world.width(), packet.position / world.width());
if (tile != null && tile.entity != null) {
tile.entity.health = packet.health;
}
});
Net.handleClient(DisconnectPacket.class, packet -> {
Player player = playerGroup.getByID(packet.playerid);
if (player != null) {
player.remove();
}
Platform.instance.updateRPC();
});
Net.handleClient(KickPacket.class, packet -> {
kicked = true;
Net.disconnect();
state.set(State.menu);
if(!packet.reason.quiet) ui.showError("$text.server.kicked." + packet.reason.name());
ui.loadfrag.hide();
});
Net.handleClient(GameOverPacket.class, packet -> {
if(world.getCore().block() != ProductionBlocks.core &&
world.getCore().entity != null){
world.getCore().entity.onDeath(true);
}
kicked = true;
ui.restart.show();
});
Net.handleClient(FriendlyFireChangePacket.class, packet -> state.friendlyFire = packet.enabled);
Net.handleClient(ItemTransferPacket.class, packet -> {
Runnable r = () -> {
Tile tile = world.tile(packet.position);
if (tile == null || tile.entity == null) return;
Tile next = tile.getNearby(packet.rotation);
tile.entity.items[packet.itemid] --;
next.block().handleItem(Item.getByID(packet.itemid), next, tile);
};
threads.run(r);
});
Net.handleClient(ItemSetPacket.class, packet -> {
Runnable r = () -> {
Tile tile = world.tile(packet.position);
if (tile == null || tile.entity == null) return;
tile.entity.items[packet.itemid] = packet.amount;
};
threads.run(r);
});
Net.handleClient(ItemOffloadPacket.class, packet -> {
Runnable r = () -> {
Tile tile = world.tile(packet.position);
if (tile == null || tile.entity == null) return;
Tile next = tile.getNearby(tile.getRotation());
next.block().handleItem(Item.getByID(packet.itemid), next, tile);
};
threads.run(r);
});
Net.handleClient(NetErrorPacket.class, packet -> {
ui.showError(packet.message);
disconnectQuietly();
});
Net.handleClient(PlayerAdminPacket.class, packet -> {
Player player = playerGroup.getByID(packet.id);
player.isAdmin = packet.admin;
ui.listfrag.rebuild();
});
Net.handleClient(TracePacket.class, packet -> {
Player player = playerGroup.getByID(packet.info.playerid);
ui.traces.show(player, packet.info);
});
Net.handleClient(UpgradePacket.class, packet -> {
Weapon weapon = (Weapon) Upgrade.getByID(packet.id);
state.inventory.removeItems(UpgradeRecipes.get(weapon));
control.upgrades().addWeapon(weapon);
ui.hudfrag.updateWeapons();
Effects.sound("purchase");
});
}catch(Exception e){
throw new RuntimeException(e);
}
}
@Override
@@ -333,7 +344,7 @@ public class NetClient extends Module {
if(timeoutTime > dataTimeout){
Log.err("Failed to load data!");
ui.loadfrag.hide();
kicked = true;
quiet = true;
ui.showError("$text.disconnect.data");
Net.disconnect();
timeoutTime = 0f;
@@ -351,7 +362,7 @@ public class NetClient extends Module {
ui.loadfrag.hide();
ui.join.hide();
Net.setClientLoaded(true);
Timers.runTask(1f, () -> Net.send(new ConnectConfirmPacket(), SendMode.tcp));
Gdx.app.postRunnable(Call::connectConfirm);
Timers.runTask(40f, Platform.instance::updateRPC);
}
@@ -360,26 +371,25 @@ public class NetClient extends Module {
}
public void disconnectQuietly(){
kicked = true;
quiet = true;
Net.disconnect();
}
public void clearRecieved(){
recieved.clear();
public synchronized void addRemovedEntity(int id){
removed.add(id);
}
public synchronized boolean isEntityUsed(int id){
return removed.contains(id);
}
void sync(){
requests = 0;
if(timer.get(0, playerSyncTime)){
byte[] bytes = new byte[player.getWriteSize() + 8];
ByteBuffer buffer = ByteBuffer.wrap(bytes);
buffer.putLong(TimeUtils.millis());
player.write(buffer);
PositionPacket packet = new PositionPacket();
packet.data = bytes;
ClientSnapshotPacket packet = Pooling.obtain(ClientSnapshotPacket.class);
packet.lastSnapshot = lastSnapshotBaseID;
packet.snapid = lastSent++;
Net.send(packet, SendMode.udp);
}
@@ -387,4 +397,17 @@ public class NetClient extends Module {
Net.updatePing();
}
}
}
String getUsid(String ip){
if(Settings.getString("usid-" + ip, null) != null){
return Settings.getString("usid-" + ip, null);
}else{
byte[] bytes = new byte[8];
new Random().nextBytes(bytes);
String result = new String(Base64Coder.encode(bytes));
Settings.putString("usid-" + ip, result);
Settings.save();
return result;
}
}
}

View File

@@ -1,69 +0,0 @@
package io.anuke.mindustry.core;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.net.Net.SendMode;
import io.anuke.mindustry.net.Packets.*;
import io.anuke.mindustry.resource.Upgrade;
import io.anuke.mindustry.resource.Weapon;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.modules.Module;
import static io.anuke.mindustry.Vars.*;
public class NetCommon extends Module {
public NetCommon(){
Net.handle(ShootPacket.class, (packet) -> {
Player player = playerGroup.getByID(packet.playerid);
Weapon weapon = (Weapon) Upgrade.getByID(packet.weaponid);
weapon.shoot(player, packet.x, packet.y, packet.rotation);
});
Net.handle(ChatPacket.class, (packet) -> {
ui.chatfrag.addMessage(packet.text, colorizeName(packet.id, packet.name));
});
Net.handle(WeaponSwitchPacket.class, (packet) -> {
Player player = playerGroup.getByID(packet.playerid);
if (player == null) return;
player.weaponLeft = (Weapon) Upgrade.getByID(packet.left);
player.weaponRight = (Weapon) Upgrade.getByID(packet.right);
});
Net.handle(BlockTapPacket.class, (packet) -> {
Tile tile = world.tile(packet.position);
tile.block().tapped(tile);
});
Net.handle(BlockConfigPacket.class, (packet) -> {
Tile tile = world.tile(packet.position);
if (tile != null) tile.block().configure(tile, packet.data);
});
Net.handle(PlayerDeathPacket.class, (packet) -> {
Player player = playerGroup.getByID(packet.id);
if(player == null) return;
player.doRespawn();
});
}
public void sendMessage(String message){
ChatPacket packet = new ChatPacket();
packet.name = null;
packet.text = message;
Net.send(packet, SendMode.tcp);
if(!headless) ui.chatfrag.addMessage(message, null);
}
public String colorizeName(int id, String name){
Player player = playerGroup.getByID(id);
if(name == null || player == null) return null;
return "[#" + player.color.toString().toUpperCase() + "]" + name;
}
}

View File

@@ -1,69 +1,102 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.utils.*;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Colors;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.TimeUtils;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.mindustry.content.Mechs;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.entities.SyncEntity;
import io.anuke.mindustry.game.EventType.GameOverEvent;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.gen.Call;
import io.anuke.mindustry.gen.RemoteReadServer;
import io.anuke.mindustry.io.Version;
import io.anuke.mindustry.net.*;
import io.anuke.mindustry.net.Administration.PlayerInfo;
import io.anuke.mindustry.net.Net.SendMode;
import io.anuke.mindustry.net.Packets.*;
import io.anuke.mindustry.resource.*;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Placement;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Events;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.trait.Entity;
import io.anuke.ucore.io.CountableByteArrayOutputStream;
import io.anuke.ucore.io.delta.ByteDeltaEncoder;
import io.anuke.ucore.io.delta.ByteMatcherHash;
import io.anuke.ucore.io.delta.DEZEncoder;
import io.anuke.ucore.modules.Module;
import io.anuke.ucore.util.Log;
import io.anuke.ucore.util.Timer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Arrays;
import static io.anuke.mindustry.Vars.*;
public class NetServer extends Module{
private final static float serverSyncTime = 4, itemSyncTime = 10, kickDuration = 30 * 1000;
public final static int maxSnapshotSize = 2047;
public final static boolean showSnapshotSize = false;
private final static int timerEntitySync = 0;
private final static int timerStateSync = 1;
private final static byte[] reusableSnapArray = new byte[maxSnapshotSize];
private final static float serverSyncTime = 4, kickDuration = 30 * 1000;
private final static Vector2 vector = new Vector2();
/**
* If a play goes away of their server-side coordinates by this distance, they get teleported back.
*/
private final static float correctDist = 16f;
public final Administration admins = new Administration();
/**Maps connection IDs to players.*/
/**
* Maps connection IDs to players.
*/
private IntMap<Player> connections = new IntMap<>();
private ObjectMap<String, ByteArray> weapons = new ObjectMap<>();
private boolean closing = false;
private Timer timer = new Timer(5);
/**
* Stream for writing player sync data to.
*/
private CountableByteArrayOutputStream syncStream = new CountableByteArrayOutputStream();
/**
* Data stream for writing player sync data to.
*/
private DataOutputStream dataStream = new DataOutputStream(syncStream);
/**
* Encoder for computing snapshot deltas.
*/
private DEZEncoder encoder = new DEZEncoder();
public NetServer(){
Events.on(GameOverEvent.class, () -> {
weapons.clear();
admins.getEditLogs().clear();
});
Net.handleServer(Connect.class, (id, connect) -> {
if(admins.isIPBanned(connect.addressTCP)){
kick(id, KickReason.banned);
}
});
Net.handleServer(Disconnect.class, (id, packet) -> {
Player player = connections.get(id);
if(player != null){
onDisconnect(player);
}
connections.remove(id);
});
Net.handleServer(ConnectPacket.class, (id, packet) -> {
String uuid = new String(Base64Coder.encode(packet.uuid));
String uuid = packet.uuid;
if(Net.getConnection(id) == null ||
admins.isIPBanned(Net.getConnection(id).address)) return;
TraceInfo trace = admins.getTrace(Net.getConnection(id).address);
TraceInfo trace = admins.getTraceByID(uuid);
PlayerInfo info = admins.getInfo(uuid);
trace.uuid = uuid;
trace.android = packet.android;
trace.android = packet.mobile;
if(admins.isIDBanned(uuid)){
kick(id, KickReason.banned);
@@ -75,6 +108,34 @@ public class NetServer extends Module{
return;
}
if(packet.version == -1 && Version.build != -1 && !admins.allowsCustomClients()){
kick(id, KickReason.customClient);
return;
}
boolean preventDuplicates = headless;
if(preventDuplicates){
for(Player player : playerGroup.all()){
if(player.name.equalsIgnoreCase(packet.name)){
kick(id, KickReason.nameInUse);
return;
}
if(player.uuid.equals(packet.uuid)){
kick(id, KickReason.idInUse);
return;
}
}
}
packet.name = fixName(packet.name);
if(packet.name.trim().length() <= 0){
kick(id, KickReason.nameEmpty);
return;
}
Log.info("Recieved connect packet for player '{0}' / UUID {1} / IP {2}", packet.name, uuid, trace.ip);
String ip = Net.getConnection(id).address;
@@ -91,282 +152,180 @@ public class NetServer extends Module{
}
Player player = new Player();
player.isAdmin = admins.isAdmin(uuid, ip);
player.clientid = id;
player.isAdmin = admins.isAdmin(uuid, packet.usid);
player.con = Net.getConnection(id);
player.usid = packet.usid;
player.name = packet.name;
player.isAndroid = packet.android;
player.set(world.getSpawnX(), world.getSpawnY());
player.setNet(player.x, player.y);
player.uuid = uuid;
player.isMobile = packet.mobile;
player.mech = packet.mobile ? Mechs.starterMobile : Mechs.starterDesktop;
player.dead = true;
player.setNet(player.x, player.y);
player.color.set(packet.color);
player.color.a = 1f;
connections.put(id, player);
trace.playerid = player.id;
if(world.getMap().custom){
ByteArrayOutputStream stream = new ByteArrayOutputStream();
NetworkIO.writeMap(world.getMap(), stream);
CustomMapPacket data = new CustomMapPacket();
data.stream = new ByteArrayInputStream(stream.toByteArray());
Net.sendStream(id, data);
Log.info("Sending custom map: Packed {0} uncompressed bytes of MAP data.", stream.size());
}else{
//hack-- simulate the map ack packet recieved to send the world data to the client.
Net.handleServerReceived(id, new MapAckPacket());
}
Platform.instance.updateRPC();
});
Net.handleServer(MapAckPacket.class, (id, packet) -> {
Player player = connections.get(id);
//TODO try DeflaterOutputStream
ByteArrayOutputStream stream = new ByteArrayOutputStream();
NetworkIO.writeWorld(player, weapons.get(admins.getTrace(Net.getConnection(id).address).uuid, new ByteArray()), stream);
WorldData data = new WorldData();
NetworkIO.writeWorld(player, stream);
WorldStream data = new WorldStream();
data.stream = new ByteArrayInputStream(stream.toByteArray());
Net.sendStream(id, data);
Log.info("Packed {0} uncompressed bytes of WORLD data.", stream.size());
});
Net.handleServer(ConnectConfirmPacket.class, (id, packet) -> {
Player player = connections.get(id);
if (player == null) return;
player.add();
Log.info("&y{0} has connected.", player.name);
netCommon.sendMessage("[accent]" + player.name + " has connected.");
});
Net.handleServer(Disconnect.class, (id, packet) -> {
Player player = connections.get(packet.id);
if (player == null) {
Log.err("Unknown client has disconnected (ID={0})", id);
return;
}
Log.info("&y{0} has disconnected.", player.name);
netCommon.sendMessage("[accent]" + player.name + " has disconnected.");
player.remove();
DisconnectPacket dc = new DisconnectPacket();
dc.playerid = player.id;
Net.send(dc, SendMode.tcp);
Platform.instance.updateRPC();
admins.save();
});
Net.handleServer(PositionPacket.class, (id, packet) -> {
ByteBuffer buffer = ByteBuffer.wrap(packet.data);
long time = buffer.getLong();
//update last recieved snapshot based on client snapshot
Net.handleServer(ClientSnapshotPacket.class, (id, packet) -> {
Player player = connections.get(id);
player.read(buffer, time);
NetConnection connection = Net.getConnection(id);
if(player == null || connection == null || packet.snapid < connection.lastRecievedClientSnapshot) return;
boolean verifyPosition = !player.isDead() && !debug && headless && !player.mech.flying && player.getCarrier() == null;
if(connection.lastRecievedClientTime == 0) connection.lastRecievedClientTime = TimeUtils.millis() - 16;
long elapsed = TimeUtils.timeSinceMillis(connection.lastRecievedClientTime);
float maxSpeed = (packet.boosting && !player.mech.flying ? player.mech.boostSpeed : player.mech.speed) * 2.5f;
//extra 1.1x multiplicaton is added just in case
float maxMove = elapsed / 1000f * 60f * maxSpeed * 1.1f;
player.pointerX = packet.pointerX;
player.pointerY = packet.pointerY;
player.setMineTile(packet.mining);
player.isBoosting = packet.boosting;
player.isShooting = packet.shooting;
player.getPlaceQueue().clear();
if(packet.currentRequest != null){
player.getPlaceQueue().addLast(packet.currentRequest);
}
vector.set(packet.x - player.getInterpolator().target.x, packet.y - player.getInterpolator().target.y);
vector.limit(maxMove);
float prevx = player.x, prevy = player.y;
player.set(player.getInterpolator().target.x, player.getInterpolator().target.y);
player.move(vector.x, vector.y);
float newx = player.x, newy = player.y;
if(!verifyPosition){
player.x = prevx;
player.y = prevy;
newx = packet.x;
newy = packet.y;
}else if(Vector2.dst(packet.x, packet.y, newx, newy) > correctDist){
Call.onPositionSet(id, newx, newy); //teleport and correct position when necessary
}
//reset player to previous synced position so it gets interpolated
player.x = prevx;
player.y = prevy;
//set interpolator target to *new* position so it moves toward it
player.getInterpolator().read(player.x, player.y, newx, newy, packet.timeSent, packet.rotation, packet.baseRotation);
player.getVelocity().set(packet.xv, packet.yv); //only for visual calculation purposes, doesn't actually update the player
//when the client confirms recieveing a snapshot, update base and clear map
if(packet.lastSnapshot > connection.currentBaseID){
connection.currentBaseID = packet.lastSnapshot;
connection.currentBaseSnapshot = connection.lastSentRawSnapshot;
}
connection.lastRecievedClientSnapshot = packet.snapid;
connection.lastRecievedClientTime = TimeUtils.millis();
});
Net.handleServer(ShootPacket.class, (id, packet) -> {
TraceInfo info = admins.getTrace(Net.getConnection(id).address);
Weapon weapon = (Weapon)Upgrade.getByID(packet.weaponid);
Net.handleServer(InvokePacket.class, (id, packet) -> {
Player player = connections.get(id);
if(player == null) return;
RemoteReadServer.readPacket(packet.writeBuffer, packet.type, player);
});
}
float wtrc = 80;
if(!Timers.get("fastshoot-" + id + "-" + weapon.id, wtrc)){
info.fastShots.getAndIncrement(weapon.id, 0, 1);
if(info.fastShots.get(weapon.id, 0) > (int)(wtrc / (weapon.getReload() / 2f)) + 30){
kick(id, KickReason.fastShoot);
/**
* Sends a raw byte[] snapshot to a client, splitting up into chunks when needed.
*/
private static void sendSplitSnapshot(int userid, byte[] bytes, int snapshotID, int base){
if(bytes.length < maxSnapshotSize){
Call.onSnapshot(userid, bytes, snapshotID, (short) 0, bytes.length, base);
}else{
int remaining = bytes.length;
int offset = 0;
int chunkid = 0;
while(remaining > 0){
int used = Math.min(remaining, maxSnapshotSize);
byte[] toSend;
//re-use sent byte arrays when possible
if(used == maxSnapshotSize){
toSend = reusableSnapArray;
System.arraycopy(bytes, offset, toSend, 0, Math.min(offset + maxSnapshotSize, bytes.length) - offset);
}else{
toSend = Arrays.copyOfRange(bytes, offset, Math.min(offset + maxSnapshotSize, bytes.length));
}
Call.onSnapshot(userid, toSend, snapshotID, (short) chunkid, bytes.length, base);
remaining -= used;
offset += used;
chunkid++;
}
}
}
public static void onDisconnect(Player player){
Call.sendMessage("[accent]" + player.name + " has disconnected.");
Call.onPlayerDisconnect(player.id);
player.remove();
netServer.connections.remove(player.con.id);
}
@Remote(targets = Loc.client, called = Loc.server)
public static void onAdminRequest(Player player, Player other, AdminAction action){
if(!player.isAdmin){
Log.err("ACCESS DENIED: Player {0} / {1} attempted to perform admin action without proper security access.",
player.name, player.con.address);
return;
}
if(other == null || (other.isAdmin && other != player)){ //fun fact: this means you can ban yourself
Log.err("{0} attempted to perform admin action on nonexistant or admin player.", player.name);
return;
}
if(action == AdminAction.wave){
//no verification is done, so admins can hypothetically spam waves
//not a real issue, because server owners may want to do just that
state.wavetime = 0f;
}else if(action == AdminAction.ban){
netServer.admins.banPlayerIP(other.con.address);
netServer.kick(other.con.id, KickReason.banned);
Log.info("&lc{0} has banned {1}.", player.name, other.name);
}else if(action == AdminAction.kick){
netServer.kick(other.con.id, KickReason.kick);
Log.info("&lc{0} has kicked {1}.", player.name, other.name);
}else if(action == AdminAction.trace){
//TODO
if(player.con != null){
Call.onTraceInfo(player.con.id, netServer.admins.getTraceByID(other.uuid));
}else{
info.fastShots.put(weapon.id, 0);
NetClient.onTraceInfo(netServer.admins.getTraceByID(other.uuid));
}
Log.info("&lc{0} has requested trace info of {1}.", player.name, other.name);
}
}
packet.playerid = connections.get(id).id;
Net.sendExcept(id, packet, SendMode.udp);
});
Net.handleServer(PlacePacket.class, (id, packet) -> {
packet.playerid = connections.get(id).id;
Block block = Block.getByID(packet.block);
if(!Placement.validPlace(packet.x, packet.y, block)) return;
Recipe recipe = Recipes.getByResult(block);
if(recipe == null) return;
Tile tile = world.tile(packet.x, packet.y);
if(tile.synthetic() && admins.isValidateReplace() && !admins.validateBreak(admins.getTrace(Net.getConnection(id).address).uuid, Net.getConnection(id).address)){
if(Timers.get("break-message-" + id, 120)){
sendMessageTo(id, "[scarlet]Anti-grief: you are replacing blocks too quickly. wait until replacing again.");
}
return;
}
state.inventory.removeItems(recipe.requirements);
Placement.placeBlock(packet.x, packet.y, block, packet.rotation, true, false);
admins.logEdit(packet.x, packet.y, connections.get(id), block, packet.rotation, EditLog.EditAction.PLACE);
admins.getTrace(Net.getConnection(id).address).lastBlockPlaced = block;
admins.getTrace(Net.getConnection(id).address).totalBlocksPlaced ++;
admins.getInfo(admins.getTrace(Net.getConnection(id).address).uuid).totalBlockPlaced ++;
Net.send(packet, SendMode.tcp);
});
Net.handleServer(BreakPacket.class, (id, packet) -> {
packet.playerid = connections.get(id).id;
if(!Placement.validBreak(packet.x, packet.y)) return;
Tile tile = world.tile(packet.x, packet.y);
if(tile.synthetic() && !admins.validateBreak(admins.getTrace(Net.getConnection(id).address).uuid, Net.getConnection(id).address)){
if(Timers.get("break-message-" + id, 120)){
sendMessageTo(id, "[scarlet]Anti-grief: you are breaking blocks too quickly. wait until breaking again.");
}
return;
}
Block block = Placement.breakBlock(packet.x, packet.y, true, false);
if(block != null) {
admins.logEdit(packet.x, packet.y, connections.get(id), block, tile.getRotation(), EditLog.EditAction.BREAK);
admins.getTrace(Net.getConnection(id).address).lastBlockBroken = block;
admins.getTrace(Net.getConnection(id).address).totalBlocksBroken++;
admins.getInfo(admins.getTrace(Net.getConnection(id).address).uuid).totalBlocksBroken ++;
if (block.update || block.destructible)
admins.getTrace(Net.getConnection(id).address).structureBlocksBroken++;
}
Net.send(packet, SendMode.tcp);
});
Net.handleServer(ChatPacket.class, (id, packet) -> {
if(!Timers.get("chatFlood" + id, 20)){
ChatPacket warn = new ChatPacket();
warn.text = "[scarlet]You are sending messages too quickly.";
Net.sendTo(id, warn, SendMode.tcp);
return;
}
Player player = connections.get(id);
packet.name = player.name;
packet.id = player.id;
Net.send(packet, SendMode.tcp);
});
Net.handleServer(UpgradePacket.class, (id, packet) -> {
Player player = connections.get(id);
Weapon weapon = (Weapon) Upgrade.getByID(packet.id);
String uuid = admins.getTrace(Net.getConnection(id).address).uuid;
if(!state.inventory.hasItems(UpgradeRecipes.get(weapon))){
return;
}
if (!weapons.containsKey(uuid)) weapons.put(uuid, new ByteArray());
if (!weapons.get(uuid).contains(weapon.id)){
weapons.get(uuid).add(weapon.id);
}else{
return;
}
state.inventory.removeItems(UpgradeRecipes.get(weapon));
Net.sendTo(id, packet, SendMode.tcp);
});
Net.handleServer(WeaponSwitchPacket.class, (id, packet) -> {
TraceInfo info = admins.getTrace(Net.getConnection(id).address);
packet.playerid = connections.get(id).id;
Net.sendExcept(id, packet, SendMode.tcp);
});
Net.handleServer(BlockTapPacket.class, (id, packet) -> {
Net.sendExcept(id, packet, SendMode.tcp);
});
Net.handleServer(BlockConfigPacket.class, (id, packet) -> {
Net.sendExcept(id, packet, SendMode.tcp);
});
Net.handleServer(EntityRequestPacket.class, (cid, packet) -> {
int id = packet.id;
int dest = cid;
EntityGroup group = Entities.getGroup(packet.group);
if(group.getByID(id) != null){
EntitySpawnPacket p = new EntitySpawnPacket();
p.entity = (SyncEntity)group.getByID(id);
p.group = group;
Net.sendTo(dest, p, SendMode.tcp);
}
});
Net.handleServer(PlayerDeathPacket.class, (id, packet) -> {
packet.id = connections.get(id).id;
Net.sendExcept(id, packet, SendMode.tcp);
});
Net.handleServer(AdministerRequestPacket.class, (id, packet) -> {
Player player = connections.get(id);
if(!player.isAdmin){
Log.err("ACCESS DENIED: Player {0} / {1} attempted to perform admin action without proper security access.",
player.name, Net.getConnection(player.clientid).address);
return;
}
Player other = playerGroup.getByID(packet.id);
if(other == null || other.isAdmin){
Log.err("{0} attempted to perform admin action on nonexistant or admin player.", player.name);
return;
}
String ip = Net.getConnection(other.clientid).address;
if(packet.action == AdminAction.ban){
admins.banPlayerIP(ip);
kick(other.clientid, KickReason.banned);
Log.info("&lc{0} has banned {1}.", player.name, other.name);
}else if(packet.action == AdminAction.kick){
kick(other.clientid, KickReason.kick);
Log.info("&lc{0} has kicked {1}.", player.name, other.name);
}else if(packet.action == AdminAction.trace){
TracePacket trace = new TracePacket();
trace.info = admins.getTrace(ip);
Net.sendTo(id, trace, SendMode.tcp);
Log.info("&lc{0} has requested trace info of {1}.", player.name, other.name);
}
});
Net.handleServer(BlockLogRequestPacket.class, (id, packet) -> {
packet.editlogs = admins.getEditLogs().get(packet.x + packet.y * world.width(), new Array<>());
Net.sendTo(id, packet, SendMode.udp);
});
Net.handleServer(RollbackRequestPacket.class, (id, packet) -> {
Player player = connections.get(id);
if(!player.isAdmin){
Log.err("ACCESS DENIED: Player {0} / {1} attempted to perform a rollback without proper security access.",
player.name, Net.getConnection(player.clientid).address);
return;
}
admins.rollbackWorld(packet.rollbackTimes);
Log.info("&lc{0} has rolled back the world {1} times.", player.name, packet.rollbackTimes);
});
@Remote(targets = Loc.client)
public static void connectConfirm(Player player){
player.add();
player.con.hasConnected = true;
Call.sendMessage("[accent]" + player.name + " has connected.");
Log.info("&y{0} has connected.", player.name);
}
public void update(){
@@ -387,7 +346,6 @@ public class NetServer extends Module{
}
public void reset(){
weapons.clear();
admins.clearTraces();
}
@@ -400,104 +358,184 @@ public class NetServer extends Module{
Log.info("Kicking connection #{0} / IP: {1}. Reason: {2}", connection, con.address, reason);
}
if((reason == KickReason.kick || reason == KickReason.banned) && admins.getTrace(con.address).uuid != null){
PlayerInfo info = admins.getInfo(admins.getTrace(con.address).uuid);
info.timesKicked ++;
if((reason == KickReason.kick || reason == KickReason.banned) && admins.getTraceByID(getUUID(con.id)).uuid != null){
PlayerInfo info = admins.getInfo(admins.getTraceByID(getUUID(con.id)).uuid);
info.timesKicked++;
info.lastKicked = TimeUtils.millis();
}
KickPacket p = new KickPacket();
p.reason = reason;
//TODO kick player, send kick packet
Call.onKick(connection, reason);
con.send(p, SendMode.tcp);
Timers.runTask(2f, con::close);
admins.save();
}
void sendMessageTo(int id, String message){
ChatPacket packet = new ChatPacket();
packet.text = message;
Net.sendTo(id, packet, SendMode.tcp);
String getUUID(int connectionID){
return connections.get(connectionID).uuid;
}
void sync(){
String fixName(String name){
if(timer.get(timerEntitySync, serverSyncTime)){
//scan through all groups with syncable entities
for(EntityGroup<?> group : Entities.getAllGroups()) {
if(group.size() == 0 || !(group.all().iterator().next() instanceof SyncEntity)) continue;
for(int i = 0; i < name.length(); i++){
if(name.charAt(i) == '[' && i != name.length() - 1 && name.charAt(i + 1) != '[' && (i == 0 || name.charAt(i - 1) != '[')){
String prev = name.substring(0, i);
String next = name.substring(i);
String result = checkColor(next);
//get write size for one entity (adding 4, as you need to write the ID as well)
int writesize = SyncEntity.getWriteSize((Class<? extends SyncEntity>)group.getType()) + 4;
//amount of entities
int amount = group.size();
//maximum amount of entities per packet
int maxsize = 64;
//current buffer you're writing to
ByteBuffer current = null;
//number of entities written to this packet/buffer
int written = 0;
//for all the entities...
for (int i = 0; i < amount; i++) {
//if the buffer is null, create a new one
if(current == null){
//calculate amount of entities to go into this packet
int csize = Math.min(amount-i, maxsize);
//create a byte array to write to
byte[] bytes = new byte[csize*writesize + 1 + 8];
//wrap it for easy writing
current = ByteBuffer.wrap(bytes);
current.putLong(TimeUtils.millis());
//write the group ID so the client knows which group this is
current.put((byte)group.getID());
}
SyncEntity entity = (SyncEntity) group.all().get(i);
//write ID to the buffer
current.putInt(entity.id);
int previous = current.position();
//write extra data to the buffer
entity.write(current);
written ++;
//if the packet is too big now...
if(written >= maxsize){
//send the packet.
SyncPacket packet = new SyncPacket();
packet.data = current.array();
Net.send(packet, SendMode.udp);
//reset data, send the next packet
current = null;
written = 0;
}
}
//make sure to send incomplete packets too
if(current != null){
SyncPacket packet = new SyncPacket();
packet.data = current.array();
Net.send(packet, SendMode.udp);
}
name = prev + result;
}
}
if(timer.get(timerStateSync, itemSyncTime)){
StateSyncPacket packet = new StateSyncPacket();
packet.items = state.inventory.getItems();
packet.countdown = state.wavetime;
packet.enemies = state.enemies;
packet.wave = state.wave;
packet.time = Timers.time();
packet.timestamp = TimeUtils.millis();
Net.send(packet, SendMode.udp);
return name.substring(0, Math.min(name.length(), maxNameLength));
}
String checkColor(String str){
for(int i = 1; i < str.length(); i++){
if(str.charAt(i) == ']'){
String color = str.substring(1, i);
if(Colors.get(color.toUpperCase()) != null || Colors.get(color.toLowerCase()) != null){
Color result = (Colors.get(color.toLowerCase()) == null ? Colors.get(color.toUpperCase()) : Colors.get(color.toLowerCase()));
if(result.a <= 0.8f){
return str.substring(i + 1);
}
}else{
try{
Color result = Color.valueOf(color);
if(result.a <= 0.8f){
return str.substring(i + 1);
}
}catch(Exception e){
return str;
}
}
}
}
return str;
}
void sync(){
try{
//iterate through each player
for(Player player : connections.values()){
NetConnection connection = player.con;
if(!connection.isConnected()){
//player disconnected, ignore them
onDisconnect(player);
return;
}
if(!player.timer.get(Player.timerSync, serverSyncTime) || !connection.hasConnected) continue;
//if the player hasn't acknowledged that it has recieved the packet, send the same thing again
if(connection.currentBaseID < connection.lastSentSnapshotID){
if(showSnapshotSize)
Log.info("Re-sending snapshot: {0} bytes, ID {1} base {2} baselength {3}", connection.lastSentSnapshot.length, connection.lastSentSnapshotID, connection.lastSentBase, connection.currentBaseSnapshot.length);
sendSplitSnapshot(connection.id, connection.lastSentSnapshot, connection.lastSentSnapshotID, connection.lastSentBase);
return;
}
//reset stream to begin writing
syncStream.reset();
//write wave datas
dataStream.writeFloat(state.wavetime);
dataStream.writeInt(state.wave);
Array<Tile> cores = state.teams.get(player.getTeam()).cores;
dataStream.writeByte(cores.size);
//write all core inventory data
for(Tile tile : cores){
dataStream.writeInt(tile.packedPosition());
tile.entity.items.write(dataStream);
}
//write timestamp
dataStream.writeLong(TimeUtils.millis());
int totalGroups = 0;
for(EntityGroup<?> group : Entities.getAllGroups()){
if(!group.isEmpty() && (group.all().get(0) instanceof SyncTrait)) totalGroups++;
}
//write total amount of serializable groups
dataStream.writeByte(totalGroups);
//check for syncable groups
for(EntityGroup<?> group : Entities.getAllGroups()){
//TODO range-check sync positions to optimize?
if(group.isEmpty() || !(group.all().get(0) instanceof SyncTrait)) continue;
//make sure mapping is enabled for this group
if(!group.mappingEnabled()){
throw new RuntimeException("Entity group '" + group.getType() + "' contains SyncTrait entities, yet mapping is not enabled. In order for syncing to work, you must enable mapping for this group.");
}
int amount = 0;
for(Entity entity : group.all()){
if(((SyncTrait) entity).isSyncing()){
amount++;
}
}
//write group ID + group size
dataStream.writeByte(group.getID());
dataStream.writeShort(amount);
for(Entity entity : group.all()){
if(!((SyncTrait) entity).isSyncing()) continue;
int position = syncStream.position();
//write all entities now
dataStream.writeInt(entity.getID()); //write id
dataStream.writeByte(((SyncTrait) entity).getTypeID()); //write type ID
((SyncTrait) entity).write(dataStream); //write entity
int length = syncStream.position() - position; //length must always be less than 127 bytes
if(length > 127)
throw new RuntimeException("Write size for entity of type " + group.getType() + " must not exceed 127!");
dataStream.writeByte(length);
}
}
byte[] bytes = syncStream.toByteArray();
if(connection.currentBaseID == -1){
//assign to last sent snapshot so that there is only ever one unique snapshot with ID 0
if(connection.lastSentSnapshot != null){
bytes = connection.lastSentSnapshot;
}else{
connection.lastSentRawSnapshot = bytes;
connection.lastSentSnapshot = bytes;
}
if(showSnapshotSize) Log.info("Sent raw snapshot: {0} bytes.", bytes.length);
///Nothing to diff off of in this case, send the whole thing
sendSplitSnapshot(connection.id, bytes, 0, -1);
}else{
connection.lastSentRawSnapshot = bytes;
//send diff, otherwise
byte[] diff = ByteDeltaEncoder.toDiff(new ByteMatcherHash(connection.currentBaseSnapshot, bytes), encoder);
if(showSnapshotSize)
Log.info("Shrank snapshot: {0} -> {1}, Base {2} ID {3} base length = {4}", bytes.length, diff.length, connection.currentBaseID, connection.currentBaseID + 1, connection.currentBaseSnapshot.length);
sendSplitSnapshot(connection.id, diff, connection.currentBaseID + 1, connection.currentBaseID);
connection.lastSentSnapshot = diff;
connection.lastSentSnapshotID = connection.currentBaseID + 1;
connection.lastSentBase = connection.currentBaseID;
}
}
}catch(IOException e){
e.printStackTrace();
}
}
}

View File

@@ -4,8 +4,6 @@ import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Base64Coder;
import io.anuke.mindustry.core.ThreadHandler.ThreadProvider;
import io.anuke.ucore.core.Settings;
import io.anuke.ucore.entities.Entity;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.scene.ui.TextField;
@@ -13,77 +11,184 @@ import java.util.Date;
import java.util.Locale;
import java.util.Random;
public abstract class Platform {
/**Each separate game platform should set this instance to their own implementation.*/
public static Platform instance = new Platform() {};
public abstract class Platform{
/**
* Each separate game platform should set this instance to their own implementation.
*/
public static Platform instance = new Platform(){
};
/**Format the date using the default date formatter.*/
public String format(Date date){return "invalid";}
/**Format a number by adding in commas or periods where needed.*/
public String format(int number){return "invalid";}
/**Show a native error dialog.*/
public void showError(String text){}
/**Add a text input dialog that should show up after the field is tapped.*/
public void addDialog(TextField field){
addDialog(field, 16);
}
/**See addDialog().*/
public void addDialog(TextField field, int maxLength){}
/**Update discord RPC.*/
public void updateRPC(){}
/**Called when the game is exited.*/
public void onGameExit(){}
/**Open donation dialog. Currently android only.*/
public void openDonations(){}
/**Whether discord RPC is supported.*/
public boolean hasDiscord(){return true;}
/**Request Android permissions for writing files.*/
public void requestWritePerms(){}
/**Return the localized name for the locale. This is basically a workaround for GWT not supporting getName().*/
public String getLocaleName(Locale locale){
return locale.toString();
}
/**Whether joining games is supported.*/
public boolean canJoinGame(){
return true;
}
/**Whether debug mode is enabled.*/
public boolean isDebug(){return false;}
/**Must be 8 bytes in length.*/
public byte[] getUUID(){
String uuid = Settings.getString("uuid", "");
if(uuid.isEmpty()){
byte[] result = new byte[8];
new Random().nextBytes(result);
uuid = new String(Base64Coder.encode(result));
Settings.putString("uuid", uuid);
Settings.save();
return result;
}
return Base64Coder.decode(uuid);
}
/**Only used for iOS or android: open the share menu for a map or save.*/
public void shareFile(FileHandle file){}
/**
* Format the date using the default date formatter.
*/
public String format(Date date){
return "invalid";
}
/**Show a file chooser. Desktop only.
/**
* Format a number by adding in commas or periods where needed.
*/
public String format(int number){
return "invalid";
}
/**
* Show a native error dialog.
*/
public void showError(String text){
}
/**
* Add a text input dialog that should show up after the field is tapped.
*/
public void addDialog(TextField field){
addDialog(field, 16);
}
/**
* See addDialog().
*/
public void addDialog(TextField field, int maxLength){
}
/**
* Update discord RPC.
*/
public void updateRPC(){
}
/**
* Called when the game is exited.
*/
public void onGameExit(){
}
/**
* Open donation dialog. Currently android only.
*/
public void openDonations(){
}
/**
* Whether donating is supported.
*/
public boolean canDonate(){
return false;
}
/**
* Whether discord RPC is supported.
*/
public boolean hasDiscord(){
return true;
}
/**
* Return the localized name for the locale. This is basically a workaround for GWT not supporting getName().
*/
public String getLocaleName(Locale locale){
return locale.toString();
}
/**
* Whether joining games is supported.
*/
public boolean canJoinGame(){
return true;
}
/**
* Whether debug mode is enabled.
*/
public boolean isDebug(){
return false;
}
/**
* Must be a base64 string 8 bytes in length.
*/
public String getUUID(){
String uuid = Settings.getString("uuid", "");
if(uuid.isEmpty()){
byte[] result = new byte[8];
new Random().nextBytes(result);
uuid = new String(Base64Coder.encode(result));
Settings.putString("uuid", uuid);
Settings.save();
return uuid;
}
return uuid;
}
/**
* Only used for iOS or android: open the share menu for a map or save.
*/
public void shareFile(FileHandle file){
}
/**
* Download a file. Only used on GWT backend.
*/
public void downloadFile(String name, byte[] bytes){
}
/**
* Show a file chooser. Desktop only.
*
* @param text File chooser title text
* @param content Type of files to be loaded
* @param content Description of the type of files to be loaded
* @param cons Selection listener
* @param open Whether to open or save files.
* @param filetype File extensions to filter.
* @param open Whether to open or save files
* @param filetype File extension to filter
*/
public void showFileChooser(String text, String content, Consumer<FileHandle> cons, boolean open, String filetype){}
/**Use the default thread provider from the kryonet module for this.*/
public ThreadProvider getThreadProvider(){
return new ThreadProvider() {
@Override public boolean isOnThread() {return true;}
@Override public void sleep(long ms) {}
@Override public void start(Runnable run) {}
@Override public void stop() {}
@Override public void notify(Object object) {}
@Override public void wait(Object object) {}
@Override public <T extends Entity> void switchContainer(EntityGroup<T> group) {}
};
}
public void showFileChooser(String text, String content, Consumer<FileHandle> cons, boolean open, String filetype){
}
/**
* Use the default thread provider from the kryonet module for this.
*/
public ThreadProvider getThreadProvider(){
return new ThreadProvider(){
@Override
public boolean isOnThread(){
return true;
}
@Override
public void sleep(long ms){
}
@Override
public void start(Runnable run){
}
@Override
public void stop(){
}
@Override
public void notify(Object object){
}
@Override
public void wait(Object object){
}
};
}
//TODO iOS implementation
/**
* Forces the app into landscape mode. Currently Android only.
*/
public void beginForceLandscape(){
}
//TODO iOS implementation
/**
* Stops forcing the app into landscape orientation. Currently Android only.
*/
public void endForceLandscape(){
}
}

View File

@@ -2,592 +2,475 @@ package io.anuke.mindustry.core;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Colors;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureWrap;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.Pools;
import com.badlogic.gdx.utils.ObjectIntMap;
import com.badlogic.gdx.utils.TimeUtils;
import io.anuke.mindustry.content.fx.Fx;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.entities.SyncEntity;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.mindustry.game.SpawnPoint;
import io.anuke.mindustry.graphics.BlockRenderer;
import io.anuke.mindustry.graphics.Shaders;
import io.anuke.mindustry.input.InputHandler;
import io.anuke.mindustry.input.PlaceMode;
import io.anuke.mindustry.ui.fragments.ToolFragment;
import io.anuke.mindustry.world.BlockBar;
import io.anuke.mindustry.entities.Unit;
import io.anuke.mindustry.entities.effect.GroundEffectEntity;
import io.anuke.mindustry.entities.effect.GroundEffectEntity.GroundEffect;
import io.anuke.mindustry.entities.traits.BelowLiquidTrait;
import io.anuke.mindustry.entities.units.BaseUnit;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.graphics.*;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.Blocks;
import io.anuke.mindustry.world.blocks.ProductionBlocks;
import io.anuke.ucore.core.*;
import io.anuke.ucore.entities.EffectEntity;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.function.Callable;
import io.anuke.ucore.graphics.*;
import io.anuke.mindustry.world.meta.BlockFlag;
import io.anuke.ucore.core.Core;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Graphics;
import io.anuke.ucore.core.Settings;
import io.anuke.ucore.entities.EntityDraw;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.impl.BaseEntity;
import io.anuke.ucore.entities.impl.EffectEntity;
import io.anuke.ucore.entities.trait.DrawTrait;
import io.anuke.ucore.entities.trait.SolidTrait;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.function.Predicate;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Hue;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.graphics.Surface;
import io.anuke.ucore.modules.RendererModule;
import io.anuke.ucore.scene.ui.layout.Unit;
import io.anuke.ucore.scene.utils.Cursors;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Tmp;
import io.anuke.ucore.util.Pooling;
import io.anuke.ucore.util.Translator;
import static io.anuke.mindustry.Vars.*;
import static io.anuke.ucore.core.Core.batch;
import static io.anuke.ucore.core.Core.camera;
public class Renderer extends RendererModule{
private final static float shieldHitDuration = 18f;
public Surface shadowSurface, shieldSurface, indicatorSurface;
private int targetscale = baseCameraScale;
private Texture background = new Texture("sprites/background.png");
private FloatArray shieldHits = new FloatArray();
private Array<Callable> shieldDraws = new Array<>();
private Rectangle rect = new Rectangle(), rect2 = new Rectangle();
private BlockRenderer blocks = new BlockRenderer();
public Surface effectSurface;
public Renderer() {
Lines.setCircleVertices(14);
private int targetscale = baseCameraScale;
private Texture background = new Texture("sprites/background.png");
Core.cameraScale = baseCameraScale;
Effects.setEffectProvider((name, color, x, y, rotation) -> {
if(Settings.getBool("effects")){
Rectangle view = rect.setSize(camera.viewportWidth, camera.viewportHeight)
.setCenter(camera.position.x, camera.position.y);
Rectangle pos = rect2.setSize(name.size).setCenter(x, y);
if(view.overlaps(pos)){
new EffectEntity(name, color, rotation).set(x, y).add(effectGroup);
}
}
});
private Rectangle rect = new Rectangle(), rect2 = new Rectangle();
private Vector2 avgPosition = new Translator();
private Vector2 tmpVector1 = new Translator();
private Vector2 tmpVector2 = new Translator();
Cursors.cursorScaling = 3;
Cursors.outlineColor = Color.valueOf("444444");
Cursors.arrow = Cursors.loadCursor("cursor");
Cursors.hand = Cursors.loadCursor("hand");
Cursors.ibeam = Cursors.loadCursor("ibar");
private BlockRenderer blocks = new BlockRenderer();
private MinimapRenderer minimap = new MinimapRenderer();
private OverlayRenderer overlays = new OverlayRenderer();
private FogRenderer fog = new FogRenderer();
clearColor = Hue.lightness(0.4f);
clearColor.a = 1f;
public Renderer(){
pixelate = true;
Lines.setCircleVertices(14);
background.setWrap(TextureWrap.Repeat, TextureWrap.Repeat);
}
Shaders.init();
@Override
public void init(){
pixelate = Settings.getBool("pixelate");
int scale = Settings.getBool("pixelate") ? Core.cameraScale : 1;
shadowSurface = Graphics.createSurface(scale);
shieldSurface = Graphics.createSurface(scale);
indicatorSurface = Graphics.createSurface(scale);
pixelSurface = Graphics.createSurface(scale);
}
Core.cameraScale = baseCameraScale;
Effects.setEffectProvider((effect, color, x, y, rotation, data) -> {
if(effect == Fx.none) return;
if(Settings.getBool("effects")){
Rectangle view = rect.setSize(camera.viewportWidth, camera.viewportHeight)
.setCenter(camera.position.x, camera.position.y);
Rectangle pos = rect2.setSize(effect.size).setCenter(x, y);
public void setPixelate(boolean pixelate){
this.pixelate = pixelate;
}
if(view.overlaps(pos)){
@Override
public void update(){
if(!(effect instanceof GroundEffect)){
EffectEntity entity = Pooling.obtain(EffectEntity.class);
entity.effect = effect;
entity.color = color;
entity.rotation = rotation;
entity.data = data;
entity.id++;
entity.set(x, y);
if(data instanceof BaseEntity){
entity.setParent((BaseEntity) data);
}
threads.runGraphics(() -> effectGroup.add(entity));
}else{
GroundEffectEntity entity = Pooling.obtain(GroundEffectEntity.class);
entity.effect = effect;
entity.color = color;
entity.rotation = rotation;
entity.id++;
entity.data = data;
entity.set(x, y);
threads.runGraphics(() -> groundEffectGroup.add(entity));
}
}
}
});
if(Core.cameraScale != targetscale){
float targetzoom = (float) Core.cameraScale / targetscale;
camera.zoom = Mathf.lerpDelta(camera.zoom, targetzoom, 0.2f);
Cursors.cursorScaling = 3;
Cursors.outlineColor = Color.valueOf("444444");
if(Mathf.in(camera.zoom, targetzoom, 0.005f)){
camera.zoom = 1f;
Graphics.setCameraScale(targetscale);
control.input().resetCursor();
}
}else{
camera.zoom = Mathf.lerpDelta(camera.zoom, 1f, 0.2f);
}
Cursors.arrow = Cursors.loadCursor("cursor");
Cursors.hand = Cursors.loadCursor("hand");
Cursors.ibeam = Cursors.loadCursor("ibar");
Cursors.restoreCursor();
Cursors.loadCustom("drill");
Cursors.loadCustom("unload");
if(state.is(State.menu)){
clearScreen();
}else{
boolean smoothcam = Settings.getBool("smoothcam");
clearColor = Hue.lightness(0.4f);
clearColor.a = 1f;
if(world.getCore() == null || world.getCore().block() == ProductionBlocks.core){
if(!smoothcam){
setCamera(player.x, player.y);
}else{
smoothCamera(player.x, player.y, mobile ? 0.3f : 0.14f);
}
}else{
smoothCamera(world.getCore().worldx(), world.getCore().worldy(), 0.4f);
}
if(Settings.getBool("pixelate"))
limitCamera(4f, player.x, player.y);
float prex = camera.position.x, prey = camera.position.y;
updateShake(0.75f);
float prevx = camera.position.x, prevy = camera.position.y;
clampCamera(-tilesize / 2f, -tilesize / 2f + 1, world.width() * tilesize - tilesize / 2f, world.height() * tilesize - tilesize / 2f);
float deltax = camera.position.x - prex, deltay = camera.position.y - prey;
if(mobile){
player.x += camera.position.x - prevx;
player.y += camera.position.y - prevy;
}
float lastx = camera.position.x, lasty = camera.position.y;
if(snapCamera && smoothcam && Settings.getBool("pixelate")){
camera.position.set((int) camera.position.x, (int) camera.position.y, 0);
}
if(Gdx.graphics.getHeight() / Core.cameraScale % 2 == 1){
camera.position.add(0, -0.5f, 0);
}
if(Gdx.graphics.getWidth() / Core.cameraScale % 2 == 1){
camera.position.add(-0.5f, 0, 0);
}
draw();
camera.position.set(lastx - deltax, lasty - deltay, 0);
if(debug && !ui.chatfrag.chatOpen())
record(); //this only does something if GdxGifRecorder is on the class path, which it usually isn't
}
}
@Override
public void draw(){
camera.update();
clearScreen(clearColor);
batch.setProjectionMatrix(camera.combined);
if(pixelate)
Graphics.surface(pixelSurface, false);
else
batch.begin();
//clears shield surface
Graphics.surface(shieldSurface);
Graphics.surface();
drawPadding();
blocks.drawFloor();
blocks.processBlocks();
blocks.drawBlocks(false);
Graphics.shader(Shaders.outline, false);
Entities.draw(enemyGroup);
Entities.draw(playerGroup, p -> !p.isAndroid);
Graphics.shader();
Entities.draw(Entities.defaultGroup());
blocks.drawBlocks(true);
Graphics.shader(Shaders.outline, false);
Entities.draw(playerGroup, p -> p.isAndroid);
Graphics.shader();
Entities.draw(bulletGroup);
Entities.draw(effectGroup);
drawShield();
drawOverlay();
if(Settings.getBool("indicators") && showUI){
drawEnemyMarkers();
}
if(pixelate)
Graphics.flushSurface();
drawPlayerNames();
batch.end();
}
@Override
public void resize(int width, int height){
super.resize(width, height);
control.input().resetCursor();
camera.position.set(player.x, player.y, 0);
}
@Override
public void dispose() {
background.dispose();
}
public void clearTiles(){
blocks.clearTiles();
}
void drawPadding(){
float vw = world.width() * tilesize;
float cw = camera.viewportWidth * camera.zoom;
float ch = camera.viewportHeight * camera.zoom;
if(vw < cw){
batch.draw(background,
camera.position.x + vw/2,
Mathf.round(camera.position.y - ch/2, tilesize),
(cw - vw) /2,
ch + tilesize,
0, 0,
((cw - vw) / 2 / tilesize), -ch / tilesize + 1);
batch.draw(background,
camera.position.x - vw/2,
Mathf.round(camera.position.y - ch/2, tilesize),
-(cw - vw) /2,
ch + tilesize,
0, 0,
-((cw - vw) / 2 / tilesize), -ch / tilesize + 1);
}
}
void drawPlayerNames(){
GlyphLayout layout = Pools.obtain(GlyphLayout.class);
Draw.tscl(0.25f/2);
for(Player player : playerGroup.all()){
if(!player.isLocal && !player.isDead()){
layout.setText(Core.font, player.name);
Draw.color(0f, 0f, 0f, 0.3f);
Draw.rect("blank", player.getDrawPosition().x, player.getDrawPosition().y + 8 - layout.height/2, layout.width + 2, layout.height + 2);
Draw.color();
Draw.tcolor(player.getColor());
Draw.text(player.name, player.getDrawPosition().x, player.getDrawPosition().y + 8);
if(player.isAdmin){
Draw.color(player.getColor());
float s = 3f;
Draw.rect("icon-admin-small", player.getDrawPosition().x + layout.width/2f + 2 + 1, player.getDrawPosition().y + 7f, s, s);
}
Draw.reset();
}
}
Pools.free(layout);
Draw.tscl(fontscale);
background.setWrap(TextureWrap.Repeat, TextureWrap.Repeat);
}
void drawEnemyMarkers(){
Graphics.surface(indicatorSurface);
Draw.color(Color.RED);
@Override
public void init(){
int scale = Core.cameraScale;
for(Enemy enemy : enemyGroup.all()) {
effectSurface = Graphics.createSurface(scale);
pixelSurface = Graphics.createSurface(scale);
}
if (rect.setSize(camera.viewportWidth, camera.viewportHeight).setCenter(camera.position.x, camera.position.y)
.overlaps(enemy.hitbox.getRect(enemy.x, enemy.y))) {
continue;
}
@Override
public void update(){
float angle = Angles.angle(camera.position.x, camera.position.y, enemy.x, enemy.y);
float tx = Angles.trnsx(angle, Unit.dp.scl(20f));
float ty = Angles.trnsy(angle, Unit.dp.scl(20f));
Draw.rect("enemyarrow", camera.position.x + tx, camera.position.y + ty, angle);
}
if(Core.cameraScale != targetscale){
float targetzoom = (float) Core.cameraScale / targetscale;
camera.zoom = Mathf.lerpDelta(camera.zoom, targetzoom, 0.2f);
Draw.color();
Draw.alpha(0.4f);
Graphics.flushSurface();
Draw.color();
}
if(Mathf.in(camera.zoom, targetzoom, 0.005f)){
camera.zoom = 1f;
Graphics.setCameraScale(targetscale);
for(Player player : players){
control.input(player.playerIndex).resetCursor();
}
}
}else{
camera.zoom = Mathf.lerpDelta(camera.zoom, 1f, 0.2f);
}
void drawShield(){
if(shieldGroup.size() == 0 && shieldDraws.size == 0) return;
Graphics.surface(renderer.shieldSurface, false);
Draw.color(Color.ROYAL);
Entities.draw(shieldGroup);
for(Callable c : shieldDraws){
c.run();
}
Draw.reset();
Graphics.surface();
for(int i = 0; i < shieldHits.size / 3; i++){
float time = shieldHits.get(i * 3 + 2);
if(state.is(State.menu)){
Graphics.clear(Color.BLACK);
}else{
Vector2 position = averagePosition();
time += Timers.delta() / shieldHitDuration;
shieldHits.set(i * 3 + 2, time);
if(!mobile){
setCamera(position.x + 0.0001f, position.y + 0.0001f);
}
if(time >= 1f){
shieldHits.removeRange(i * 3, i * 3 + 2);
i--;
}
}
clampCamera(-tilesize / 2f, -tilesize / 2f + 1, world.width() * tilesize - tilesize / 2f, world.height() * tilesize - tilesize / 2f);
Texture texture = shieldSurface.texture();
Shaders.shield.color.set(Color.SKY);
float prex = camera.position.x, prey = camera.position.y;
updateShake(0.75f);
Tmp.tr2.setRegion(texture);
Shaders.shield.region = Tmp.tr2;
Shaders.shield.hits = shieldHits;
if(Shaders.shield.isFallback){
Draw.color(1f, 1f, 1f, 0.3f);
Shaders.outline.color = Color.SKY;
Shaders.outline.region = Tmp.tr2;
}
float deltax = camera.position.x - prex, deltay = camera.position.y - prey;
float lastx = camera.position.x, lasty = camera.position.y;
Graphics.end();
Graphics.shader(Shaders.shield.isFallback ? Shaders.outline : Shaders.shield);
Graphics.setScreen();
if(snapCamera){
camera.position.set((int) camera.position.x, (int) camera.position.y, 0);
}
Core.batch.draw(texture, 0, Gdx.graphics.getHeight(), Gdx.graphics.getWidth(), -Gdx.graphics.getHeight());
if(Gdx.graphics.getHeight() / Core.cameraScale % 2 == 1){
camera.position.add(0, -0.5f, 0);
}
Graphics.shader();
Graphics.end();
Graphics.beginCam();
Draw.color();
shieldDraws.clear();
}
if(Gdx.graphics.getWidth() / Core.cameraScale % 2 == 1){
camera.position.add(-0.5f, 0, 0);
}
public BlockRenderer getBlocks() {
return blocks;
}
draw();
public void addShieldHit(float x, float y){
shieldHits.addAll(x, y, 0f);
}
camera.position.set(lastx - deltax, lasty - deltay, 0);
}
public void addShield(Callable call){
shieldDraws.add(call);
}
if(debug && !ui.chatfrag.chatOpen()){
renderer.record(); //this only does something if GdxGifRecorder is on the class path, which it usually isn't
}
}
void drawOverlay(){
@Override
public void draw(){
camera.update();
//draw tutorial placement point
if(world.getMap().name.equals("tutorial") && control.tutorial().showBlock()){
int x = world.getCore().x + control.tutorial().getPlacePoint().x;
int y = world.getCore().y + control.tutorial().getPlacePoint().y;
int rot = control.tutorial().getPlaceRotation();
Graphics.clear(clearColor);
Lines.stroke(1f);
Draw.color(Color.YELLOW);
Lines.square(x * tilesize, y * tilesize, tilesize / 2f + Mathf.sin(Timers.time(), 4f, 1f));
batch.setProjectionMatrix(camera.combined);
Draw.color(Color.ORANGE);
Lines.stroke(2f);
if(rot != -1){
Lines.lineAngle(x * tilesize, y * tilesize, rot * 90, 6);
}
Draw.reset();
}
Graphics.surface(pixelSurface, false);
//draw config selected block
if(ui.configfrag.isShown()){
Tile tile = ui.configfrag.getSelectedTile();
Draw.color(Colors.get("accent"));
Lines.stroke(1f);
Lines.square(tile.drawx(), tile.drawy(),
tile.block().width * tilesize / 2f + 1f);
Draw.reset();
}
int tilex = control.input().getBlockX();
int tiley = control.input().getBlockY();
if(mobile){
Vector2 vec = Graphics.world(Gdx.input.getX(0), Gdx.input.getY(0));
tilex = Mathf.scl2(vec.x, tilesize);
tiley = Mathf.scl2(vec.y, tilesize);
}
drawPadding();
InputHandler input = control.input();
blocks.drawFloor();
//draw placement box
if((input.recipe != null && state.inventory.hasItems(input.recipe.requirements) && (!ui.hasMouse() || mobile)
&& control.input().drawPlace())){
drawAndInterpolate(groundEffectGroup, e -> e instanceof BelowLiquidTrait);
drawAndInterpolate(puddleGroup);
drawAndInterpolate(groundEffectGroup, e -> !(e instanceof BelowLiquidTrait));
input.placeMode.draw(control.input().getBlockX(), control.input().getBlockY(),
control.input().getBlockEndX(), control.input().getBlockEndY());
blocks.processBlocks();
blocks.drawBlocks(Layer.block);
Lines.stroke(1f);
Draw.color(Color.SCARLET);
for(SpawnPoint spawn : world.getSpawns()){
Lines.dashCircle(spawn.start.worldx(), spawn.start.worldy(), enemyspawnspace);
}
Graphics.shader(Shaders.blockbuild, false);
blocks.drawBlocks(Layer.placement);
Graphics.shader();
if(world.getCore() != null) {
Draw.color(Color.LIME);
Lines.poly(world.getSpawnX(), world.getSpawnY(), 4, 6f, Timers.time() * 2f);
}
if(input.breakMode == PlaceMode.holdDelete)
input.breakMode.draw(tilex, tiley, 0, 0);
}else if(input.breakMode.delete && control.input().drawPlace()
&& (input.recipe == null || !state.inventory.hasItems(input.recipe.requirements))
&& (input.placeMode.delete || input.breakMode.both || !mobile)){
blocks.drawBlocks(Layer.overlay);
if(input.breakMode == PlaceMode.holdDelete)
input.breakMode.draw(tilex, tiley, 0, 0);
else
input.breakMode.draw(control.input().getBlockX(), control.input().getBlockY(),
control.input().getBlockEndX(), control.input().getBlockEndY());
}
if(itemGroup.size() > 0){
Shaders.outline.color.set(Team.none.color);
if(ui.toolfrag.confirming){
ToolFragment t = ui.toolfrag;
PlaceMode.areaDelete.draw(t.px, t.py, t.px2, t.py2);
}
Draw.reset();
Graphics.beginShaders(Shaders.outline);
drawAndInterpolate(itemGroup);
Graphics.endShaders();
}
//draw selected block bars and info
if(input.recipe == null && !ui.hasMouse()){
Tile tile = world.tileWorld(Graphics.mouseWorld().x, Graphics.mouseWorld().y);
drawAllTeams(false);
if(tile != null && tile.block() != Blocks.air){
Tile target = tile;
if(tile.isLinked())
target = tile.getLinked();
blocks.skipLayer(Layer.turret);
blocks.drawBlocks(Layer.laser);
if(showBlockDebug && target.entity != null){
Draw.color(Color.RED);
Lines.crect(target.drawx(), target.drawy(), target.block().width * tilesize, target.block().height * tilesize);
Vector2 v = new Vector2();
drawFlyerShadows();
drawAllTeams(true);
drawAndInterpolate(bulletGroup);
drawAndInterpolate(effectGroup);
Draw.tcolor(Color.YELLOW);
Draw.tscl(0.25f);
Array<Object> arr = target.block().getDebugInfo(target);
StringBuilder result = new StringBuilder();
for(int i = 0; i < arr.size/2; i ++){
result.append(arr.get(i*2));
result.append(": ");
result.append(arr.get(i*2 + 1));
result.append("\n");
}
Draw.textc(result.toString(), target.drawx(), target.drawy(), v);
Draw.color(0f, 0f, 0f, 0.5f);
Fill.rect(target.drawx(), target.drawy(), v.x, v.y);
Draw.textc(result.toString(), target.drawx(), target.drawy(), v);
Draw.tscl(fontscale);
Draw.reset();
}
overlays.drawBottom();
drawAndInterpolate(playerGroup, p -> true, Player::drawBuildRequests);
overlays.drawTop();
if(Inputs.keyDown("block_info") && target.block().fullDescription != null){
Draw.color(Colors.get("accent"));
Lines.crect(target.drawx(), target.drawy(), target.block().width * tilesize, target.block().height * tilesize);
Draw.color();
}
if(Inputs.keyDown("block_logs")){
Draw.color(Colors.get("accent"));
Lines.crect(target.drawx(), target.drawy(), target.block().width * tilesize, target.block().height * tilesize);
Draw.color();
}
Graphics.flushSurface();
if(target.entity != null) {
int bot = 0, top = 0;
for (BlockBar bar : target.block().bars) {
float offset = Mathf.sign(bar.top) * (target.block().height / 2f * tilesize + 3f + 4f * ((bar.top ? top : bot))) +
(bar.top ? -1f : 0f);
if(showPaths && debug) drawDebug();
float value = bar.value.get(target);
batch.end();
if(MathUtils.isEqual(value, -1f)) continue;
if(showFog){
fog.draw();
}
drawBar(bar.color, target.drawx(), target.drawy() + offset, value);
Graphics.beginCam();
EntityDraw.setClip(false);
drawAndInterpolate(playerGroup, p -> !p.isDead() && !p.isLocal, Player::drawName);
EntityDraw.setClip(true);
Graphics.end();
}
if (bar.top)
top++;
else
bot++;
}
}
private void drawFlyerShadows(){
Graphics.surface(effectSurface, true, false);
target.block().drawSelect(target);
}
}
if((!debug || showUI) && Settings.getBool("healthbars")){
float trnsX = 12, trnsY = -13;
//draw entity health bars
for(Enemy entity : enemyGroup.all()){
drawHealth(entity);
}
Graphics.end();
Core.batch.getTransformMatrix().translate(trnsX, trnsY, 0);
Graphics.begin();
for(Player player : playerGroup.all()){
if(!player.isDead() && !player.isAndroid) drawHealth(player);
}
}
}
for(EntityGroup<? extends BaseUnit> group : unitGroups){
if(!group.isEmpty()){
drawAndInterpolate(group, unit -> unit.isFlying() && !unit.isDead(), Unit::drawShadow);
}
}
void drawHealth(SyncEntity dest){
float x = dest.getDrawPosition().x;
float y = dest.getDrawPosition().y;
if(dest instanceof Player && snapCamera && Settings.getBool("smoothcam") && Settings.getBool("pixelate")){
drawHealth((int) x, (int) y - 7f, dest.health, dest.maxhealth);
}else{
drawHealth(x, y - 7f, dest.health, dest.maxhealth);
}
}
if(!playerGroup.isEmpty()){
drawAndInterpolate(playerGroup, unit -> unit.isFlying() && !unit.isDead(), Unit::drawShadow);
}
void drawHealth(float x, float y, float health, float maxhealth){
drawBar(Color.RED, x, y, health / maxhealth);
}
//TODO optimize!
public void drawBar(Color color, float x, float y, float finion){
finion = Mathf.clamp(finion);
Graphics.end();
Core.batch.getTransformMatrix().translate(-trnsX, -trnsY, 0);
Graphics.begin();
if(finion > 0) finion = Mathf.clamp(finion + 0.2f, 0.24f, 1f);
//TODO this actually isn't necessary
Draw.color(0, 0, 0, 0.15f);
Graphics.flushSurface();
Draw.color();
}
float len = 3;
private void drawAllTeams(boolean flying){
for(Team team : Team.all){
EntityGroup<BaseUnit> group = unitGroups[team.ordinal()];
float w = (int) (len * 2 * finion) + 0.5f;
if(group.count(p -> p.isFlying() == flying) +
playerGroup.count(p -> p.isFlying() == flying && p.getTeam() == team) == 0 && flying) continue;
x -= 0.5f;
y += 0.5f;
drawAndInterpolate(unitGroups[team.ordinal()], u -> u.isFlying() == flying && !u.isDead(), Unit::drawUnder);
drawAndInterpolate(playerGroup, p -> p.isFlying() == flying && p.getTeam() == team, Unit::drawUnder);
Lines.stroke(3f);
Draw.color(Color.SLATE);
Lines.line(x - len + 1, y, x + len + 1.5f, y);
Lines.stroke(1f);
Draw.color(Color.BLACK);
Lines.line(x - len + 1, y, x + len + 0.5f, y);
Draw.color(color);
if(w >= 1)
Lines.line(x - len + 1, y, x - len + w, y);
Draw.reset();
}
Shaders.outline.color.set(team.color);
Shaders.mix.color.set(Color.WHITE);
public void setCameraScale(int amount){
targetscale = amount;
clampScale();
//scale up all surfaces in preparation for the zoom
if(Settings.getBool("pixelate")){
for(Surface surface : Graphics.getSurfaces()){
surface.setScale(targetscale);
}
}
}
Graphics.beginShaders(Shaders.outline);
Graphics.shader(Shaders.mix, true);
drawAndInterpolate(unitGroups[team.ordinal()], u -> u.isFlying() == flying && !u.isDead());
drawAndInterpolate(playerGroup, p -> p.isFlying() == flying && p.getTeam() == team);
Graphics.shader();
blocks.drawTeamBlocks(Layer.turret, team);
Graphics.endShaders();
public void scaleCamera(int amount){
setCameraScale(targetscale + amount);
}
drawAndInterpolate(unitGroups[team.ordinal()], u -> u.isFlying() == flying && !u.isDead(), Unit::drawOver);
drawAndInterpolate(playerGroup, p -> p.isFlying() == flying && p.getTeam() == team, Unit::drawOver);
}
}
public void clampScale(){
targetscale = Mathf.clamp(targetscale, Math.round(Unit.dp.scl(2)), Math.round(Unit.dp.scl((5))));
}
public <T extends DrawTrait> void drawAndInterpolate(EntityGroup<T> group){
drawAndInterpolate(group, t -> true, DrawTrait::draw);
}
public <T extends DrawTrait> void drawAndInterpolate(EntityGroup<T> group, Predicate<T> toDraw){
drawAndInterpolate(group, toDraw, DrawTrait::draw);
}
public <T extends DrawTrait> void drawAndInterpolate(EntityGroup<T> group, Predicate<T> toDraw, Consumer<T> drawer){
EntityDraw.drawWith(group, toDraw, t -> {
float lastx = t.getX(), lasty = t.getY(), lastrot = 0f;
if(threads.doInterpolate() && threads.isEnabled() && t instanceof SolidTrait){
SolidTrait s = (SolidTrait) t;
lastrot = s.getRotation();
if(s.lastUpdated() != 0){
float timeSinceUpdate = TimeUtils.timeSinceMillis(s.lastUpdated());
float alpha = Math.min(timeSinceUpdate / s.updateSpacing(), 1f);
tmpVector1.set(s.lastPosition().x, s.lastPosition().y)
.lerp(tmpVector2.set(lastx, lasty), alpha);
s.setRotation(Mathf.slerp(s.lastPosition().z, lastrot, alpha));
s.setX(tmpVector1.x);
s.setY(tmpVector1.y);
}
}
//TODO extremely hacky
if(t instanceof Player && ((Player) t).getCarry() != null && ((Player) t).getCarry() instanceof Player && ((Player) ((Player) t).getCarry()).isLocal){
((Player) t).x = ((Player) t).getCarry().getX();
((Player) t).y = ((Player) t).getCarry().getY();
}
drawer.accept(t);
t.setX(lastx);
t.setY(lasty);
if(threads.doInterpolate() && threads.isEnabled()){
if(t instanceof SolidTrait){
((SolidTrait) t).setRotation(lastrot);
}
}
});
}
@Override
public void resize(int width, int height){
super.resize(width, height);
for(Player player : players){
control.input(player.playerIndex).resetCursor();
}
camera.position.set(players[0].x, players[0].y, 0);
}
@Override
public void dispose(){
background.dispose();
fog.dispose();
}
public Vector2 averagePosition(){
avgPosition.setZero();
drawAndInterpolate(playerGroup, p -> p.isLocal, p -> {
avgPosition.add(p.x, p.y);
});
avgPosition.scl(1f / players.length);
return avgPosition;
}
public FogRenderer fog(){
return fog;
}
public MinimapRenderer minimap(){
return minimap;
}
void drawPadding(){
float vw = world.width() * tilesize;
float cw = camera.viewportWidth * camera.zoom;
float ch = camera.viewportHeight * camera.zoom;
if(vw < cw){
batch.draw(background,
camera.position.x + vw / 2,
Mathf.round(camera.position.y - ch / 2, tilesize),
(cw - vw) / 2,
ch + tilesize,
0, 0,
((cw - vw) / 2 / tilesize), -ch / tilesize + 1);
batch.draw(background,
camera.position.x - vw / 2,
Mathf.round(camera.position.y - ch / 2, tilesize),
-(cw - vw) / 2,
ch + tilesize,
0, 0,
-((cw - vw) / 2 / tilesize), -ch / tilesize + 1);
}
}
void drawDebug(){
int rangex = (int) (Core.camera.viewportWidth / tilesize / 2), rangey = (int) (Core.camera.viewportHeight / tilesize / 2);
for(int x = -rangex; x <= rangex; x++){
for(int y = -rangey; y <= rangey; y++){
int worldx = Mathf.scl(camera.position.x, tilesize) + x;
int worldy = Mathf.scl(camera.position.y, tilesize) + y;
if(world.tile(worldx, worldy) == null) continue;
float value = world.pathfinder().getDebugValue(worldx, worldy);
Draw.color(Color.PURPLE);
Draw.alpha((value % 10f) / 10f);
Lines.square(worldx * tilesize, worldy * tilesize, 4f);
}
}
Draw.color(Color.ORANGE);
Draw.tcolor(Color.ORANGE);
ObjectIntMap<Tile> seen = new ObjectIntMap<>();
for(BlockFlag flag : BlockFlag.values()){
for(Tile tile : world.indexer().getEnemy(Team.blue, flag)){
int index = seen.getAndIncrement(tile, 0, 1);
Draw.tscl(0.125f);
Draw.text(flag.name(), tile.drawx(), tile.drawy() + tile.block().size * tilesize / 2f + 4 + index * 3);
Lines.square(tile.drawx(), tile.drawy(), tile.block().size * tilesize / 2f);
}
}
Draw.tscl(fontScale);
Draw.tcolor();
Draw.color();
}
public BlockRenderer getBlocks(){
return blocks;
}
public void setCameraScale(int amount){
targetscale = amount;
clampScale();
//scale up all surfaces in preparation for the zoom
for(Surface surface : Graphics.getSurfaces()){
surface.setScale(targetscale);
}
}
public void scaleCamera(int amount){
setCameraScale(targetscale + amount);
}
public void clampScale(){
float s = io.anuke.ucore.scene.ui.layout.Unit.dp.scl(1f);
targetscale = Mathf.clamp(targetscale, Math.round(s * 2), Math.round(s * 5));
}
}

View File

@@ -1,54 +1,68 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Queue;
import com.badlogic.gdx.utils.TimeUtils;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.Entity;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.EntityGroup.ArrayContainer;
import io.anuke.ucore.util.Log;
import static io.anuke.mindustry.Vars.control;
import static io.anuke.mindustry.Vars.logic;
public class ThreadHandler {
private final Array<Runnable> toRun = new Array<>();
public class ThreadHandler{
private final Queue<Runnable> toRun = new Queue<>();
private final ThreadProvider impl;
private final Object updateLock = new Object();
private float delta = 1f;
private long frame = 0;
private float smoothDelta = 1f;
private long frame = 0, lastDeltaUpdate;
private float framesSinceUpdate;
private boolean enabled;
private final Object updateLock = new Object();
private boolean rendered = true;
public ThreadHandler(ThreadProvider impl){
this.impl = impl;
Timers.setDeltaProvider(() -> {
float result = impl.isOnThread() ? delta : Gdx.graphics.getDeltaTime()*60f;
return Math.min(Float.isNaN(result) ? 1f : result, 12f);
float result = impl.isOnThread() ? delta : Gdx.graphics.getDeltaTime() * 60f;
return Math.min(Float.isNaN(result) ? 1f : result, 15f);
});
}
public void run(Runnable r){
if(enabled) {
synchronized (toRun) {
toRun.add(r);
if(enabled){
synchronized(toRun){
toRun.addLast(r);
}
}else{
r.run();
}
}
public int getFPS(){
return (int)(60/delta);
public void runGraphics(Runnable r){
if(enabled){
Gdx.app.postRunnable(r);
}else{
r.run();
}
}
public void runDelay(Runnable r){
if(enabled){
synchronized(toRun){
toRun.addLast(r);
}
}else{
Gdx.app.postRunnable(r);
}
}
public int getTPS(){
return (int) (60 / smoothDelta);
}
public long getFrameID(){
return frame;
return enabled ? frame : Gdx.graphics.getFrameId();
}
public float getFramesSinceUpdate(){
@@ -61,85 +75,105 @@ public class ThreadHandler {
framesSinceUpdate += Timers.delta();
synchronized (updateLock) {
synchronized(updateLock){
rendered = true;
impl.notify(updateLock);
}
}
public void setEnabled(boolean enabled){
if(enabled){
logic.doUpdate = false;
for(EntityGroup<?> group : Entities.getAllGroups()){
impl.switchContainer(group);
}
Timers.runTask(2f, () -> {
impl.start(this::runLogic);
this.enabled = true;
});
}else{
this.enabled = false;
impl.stop();
for(EntityGroup<?> group : Entities.getAllGroups()){
group.setContainer(new ArrayContainer<>());
}
Timers.runTask(2f, () -> {
logic.doUpdate = true;
});
}
}
public boolean isEnabled(){
return enabled;
}
private void runLogic(){
try {
while (true) {
long time = TimeUtils.millis();
public void setEnabled(boolean enabled){
if(enabled){
logic.doUpdate = false;
Timers.runTask(2f, () -> {
impl.start(this::runLogic);
this.enabled = true;
});
}else{
this.enabled = false;
impl.stop();
Timers.runTask(2f, () -> {
logic.doUpdate = true;
});
}
}
synchronized (toRun) {
for(Runnable r : toRun){
r.run();
public boolean doInterpolate(){
return enabled && Gdx.graphics.getFramesPerSecond() - getTPS() > 20 && getTPS() < 30;
}
public boolean isOnThread(){
return impl.isOnThread();
}
private void runLogic(){
try{
while(true){
long time = TimeUtils.nanoTime();
while(true){
Runnable r;
synchronized(toRun){
if(toRun.size > 0){
r = toRun.removeFirst();
}else{
break;
}
}
toRun.clear();
r.run();
}
logic.doUpdate = true;
logic.update();
logic.doUpdate = false;
long elapsed = TimeUtils.timeSinceMillis(time);
long target = (long) (1000 / 60f);
long elapsed = TimeUtils.nanosToMillis(TimeUtils.timeSinceNanos(time));
long target = (long) ((1000) / 60f);
delta = Math.max(elapsed, target) / 1000f * 60f;
if (elapsed < target) {
if(elapsed < target){
impl.sleep(target - elapsed);
}
synchronized(updateLock) {
while(!rendered) {
synchronized(updateLock){
while(!rendered){
impl.wait(updateLock);
}
rendered = false;
}
frame ++;
long actuallyElapsed = TimeUtils.nanosToMillis(TimeUtils.timeSinceNanos(time));
delta = Math.max(actuallyElapsed, target) / 1000f * 60f;
if(TimeUtils.timeSinceMillis(lastDeltaUpdate) > 1000){
lastDeltaUpdate = TimeUtils.millis();
smoothDelta = delta;
}
frame++;
framesSinceUpdate = 0;
}
} catch (InterruptedException ex) {
}catch(InterruptedException ex){
Log.info("Stopping logic thread.");
} catch (Throwable ex) {
}catch(Throwable ex){
control.setError(ex);
}
}
public interface ThreadProvider {
public interface ThreadProvider{
boolean isOnThread();
void sleep(long ms) throws InterruptedException;
void start(Runnable run);
void stop();
void wait(Object object) throws InterruptedException;
void notify(Object object);
<T extends Entity> void switchContainer(EntityGroup<T> group);
}
}

View File

@@ -2,265 +2,304 @@ package io.anuke.mindustry.core;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Colors;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.utils.Align;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.mapeditor.MapEditorDialog;
import io.anuke.mindustry.editor.MapEditorDialog;
import io.anuke.mindustry.game.EventType.ResizeEvent;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.input.InputHandler;
import io.anuke.mindustry.ui.dialogs.*;
import io.anuke.mindustry.ui.fragments.*;
import io.anuke.ucore.core.Core;
import io.anuke.ucore.core.Graphics;
import io.anuke.ucore.core.Settings;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.core.*;
import io.anuke.ucore.function.Callable;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.function.Listenable;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.modules.SceneModule;
import io.anuke.ucore.scene.Group;
import io.anuke.ucore.scene.Skin;
import io.anuke.ucore.scene.actions.Actions;
import io.anuke.ucore.scene.builders.build;
import io.anuke.ucore.scene.ui.Dialog;
import io.anuke.ucore.scene.ui.TextField;
import io.anuke.ucore.scene.ui.TextField.TextFieldFilter;
import io.anuke.ucore.scene.ui.TooltipManager;
import io.anuke.ucore.scene.ui.layout.Table;
import io.anuke.ucore.scene.ui.layout.Unit;
import io.anuke.ucore.util.Mathf;
import java.util.Locale;
import static io.anuke.mindustry.Vars.control;
import static io.anuke.mindustry.Vars.players;
import static io.anuke.mindustry.Vars.threads;
import static io.anuke.ucore.scene.actions.Actions.*;
public class UI extends SceneModule{
public AboutDialog about;
public RestartDialog restart;
public LevelDialog levels;
public LoadDialog load;
public DiscordDialog discord;
public JoinDialog join;
public HostDialog host;
public PausedDialog paused;
public SettingsMenuDialog settings;
public ControlsDialog controls;
public MapEditorDialog editor;
public LanguageDialog language;
public BansDialog bans;
public AdminsDialog admins;
public TraceDialog traces;
public RollbackDialog rollback;
public ChangelogDialog changelog;
public final MenuFragment menufrag = new MenuFragment();
public final ToolFragment toolfrag = new ToolFragment();
public final MenuFragment menufrag = new MenuFragment();
public final HudFragment hudfrag = new HudFragment();
public final PlacementFragment placefrag = new PlacementFragment();
public final ChatFragment chatfrag = new ChatFragment();
public final PlayerListFragment listfrag = new PlayerListFragment();
public final BackgroundFragment backfrag = new BackgroundFragment();
public final LoadingFragment loadfrag = new LoadingFragment();
public final BlockConfigFragment configfrag = new BlockConfigFragment();
public final DebugFragment debugfrag = new DebugFragment();
public AboutDialog about;
public RestartDialog restart;
public LevelDialog levels;
public MapsDialog maps;
public LoadDialog load;
public DiscordDialog discord;
public JoinDialog join;
public HostDialog host;
public PausedDialog paused;
public SettingsMenuDialog settings;
public ControlsDialog controls;
public MapEditorDialog editor;
public LanguageDialog language;
public BansDialog bans;
public AdminsDialog admins;
public TraceDialog traces;
public RollbackDialog rollback;
public ChangelogDialog changelog;
public LocalPlayerDialog localplayers;
public UnlocksDialog unlocks;
public ContentInfoDialog content;
private Locale lastLocale;
public UI() {
Dialog.setShowAction(()-> sequence(
alpha(0f),
originCenter(),
moveToAligned(Gdx.graphics.getWidth()/2, Gdx.graphics.getHeight()/2, Align.center),
scaleTo(0.0f, 1f),
parallel(
scaleTo(1f, 1f, 0.1f, Interpolation.fade),
fadeIn(0.1f, Interpolation.fade)
)
));
Dialog.setHideAction(()-> sequence(
parallel(
scaleTo(0.01f, 0.01f, 0.1f, Interpolation.fade),
fadeOut(0.1f, Interpolation.fade)
)
));
TooltipManager.getInstance().animations = false;
Settings.setErrorHandler(()-> Timers.run(1f, ()-> showError("[crimson]Failed to access local storage.\nSettings will not be saved.")));
Settings.defaults("pixelate", true);
Dialog.closePadR = -1;
Dialog.closePadT = 5;
Colors.put("description", Color.WHITE);
Colors.put("turretinfo", Color.ORANGE);
Colors.put("iteminfo", Color.LIGHT_GRAY);
Colors.put("powerinfo", Color.YELLOW);
Colors.put("liquidinfo", Color.ROYAL);
Colors.put("craftinfo", Color.LIGHT_GRAY);
Colors.put("missingitems", Color.SCARLET);
Colors.put("health", Color.YELLOW);
Colors.put("healthstats", Color.SCARLET);
Colors.put("interact", Color.ORANGE);
Colors.put("accent", Color.valueOf("f4ba6e"));
Colors.put("place", Color.PURPLE);
Colors.put("placeInvalid", Color.RED);
Colors.put("placeRotate", Color.ORANGE);
Colors.put("break", Color.CORAL);
Colors.put("breakStart", Color.YELLOW);
Colors.put("breakInvalid", Color.RED);
}
@Override
protected void loadSkin(){
skin = new Skin(Gdx.files.internal("ui/uiskin.json"), Core.atlas);
Mathf.each(font -> {
font.setUseIntegerPositions(false);
font.getData().setScale(Vars.fontscale);
font.getData().down += Unit.dp.scl(4f);
font.getData().lineHeight -= Unit.dp.scl(2f);
}, skin.font(), skin.getFont("default-font-chat"), skin.getFont("korean"), skin.getFont("trad-chinese"));
}
public UI(){
Dialog.setShowAction(() -> sequence(
alpha(0f),
originCenter(),
moveToAligned(Gdx.graphics.getWidth() / 2, Gdx.graphics.getHeight() / 2, Align.center),
scaleTo(0.0f, 1f),
parallel(
scaleTo(1f, 1f, 0.1f, Interpolation.fade),
fadeIn(0.1f, Interpolation.fade)
)
));
@Override
public synchronized void update(){
if(Vars.debug && !Vars.showUI) return;
Dialog.setHideAction(() -> sequence(
parallel(
scaleTo(0.01f, 0.01f, 0.1f, Interpolation.fade),
fadeOut(0.1f, Interpolation.fade)
)
));
if(Graphics.drawing()) Graphics.end();
act();
TooltipManager.getInstance().animations = false;
if(control.showCursor()) {
Draw.color();
Settings.setErrorHandler(() -> Timers.run(1f, () -> showError("[crimson]Failed to access local storage.\nSettings will not be saved.")));
float scl = Unit.dp.scl(3f);
Dialog.closePadR = -1;
Dialog.closePadT = 5;
Graphics.begin();
Draw.rect("controller-cursor", Graphics.mouse().x, Graphics.mouse().y, 16*scl, 16*scl);
Graphics.end();
}
}
Colors.put("description", Palette.description);
Colors.put("turretinfo", Palette.turretinfo);
Colors.put("iteminfo", Palette.iteminfo);
Colors.put("powerinfo", Palette.powerinfo);
Colors.put("liquidinfo", Palette.liquidinfo);
Colors.put("craftinfo", Palette.craftinfo);
Colors.put("missingitems", Palette.missingitems);
Colors.put("health", Palette.health);
Colors.put("healthstats", Palette.healthstats);
Colors.put("interact", Palette.interact);
Colors.put("accent", Palette.accent);
Colors.put("place", Palette.place);
Colors.put("remove", Palette.remove);
Colors.put("placeRotate", Palette.placeRotate);
Colors.put("range", Palette.range);
Colors.put("power", Palette.power);
}
@Override
public void init(){
@Override
protected void loadSkin(){
skin = new Skin(Gdx.files.internal("ui/uiskin.json"), Core.atlas);
Mathf.each(font -> {
font.setUseIntegerPositions(false);
font.getData().setScale(Vars.fontScale);
font.getData().down += Unit.dp.scl(4f);
font.getData().lineHeight -= Unit.dp.scl(2f);
}, skin.font(), skin.getFont("default-font-chat"), skin.getFont("korean"), skin.getFont("trad-chinese"));
}
editor = new MapEditorDialog();
controls = new ControlsDialog();
restart = new RestartDialog();
join = new JoinDialog();
discord = new DiscordDialog();
load = new LoadDialog();
levels = new LevelDialog();
language = new LanguageDialog();
settings = new SettingsMenuDialog();
paused = new PausedDialog();
changelog = new ChangelogDialog();
about = new AboutDialog();
host = new HostDialog();
bans = new BansDialog();
admins = new AdminsDialog();
traces = new TraceDialog();
rollback = new RollbackDialog();
build.begin(scene);
@Override
public synchronized void update(){
if(Vars.debug && !Vars.showUI) return;
backfrag.build();
hudfrag.build();
configfrag.build();
menufrag.build();
placefrag.build();
toolfrag.build();
chatfrag.build();
listfrag.build();
debugfrag.build();
loadfrag.build();
if(Graphics.drawing()) Graphics.end();
build.end();
}
act();
@Override
public synchronized boolean hasMouse() {
return super.hasMouse();
}
Graphics.begin();
public Locale getLocale(){
String loc = Settings.getString("locale");
if(loc.equals("default")){
return Locale.getDefault();
}else{
if(lastLocale == null || !lastLocale.toString().equals(loc)){
if(loc.contains("_")){
String[] split = loc.split("_");
lastLocale = new Locale(split[0], split[1]);
}else{
lastLocale = new Locale(loc);
}
}
for(int i = 0; i < players.length; i++){
InputHandler input = control.input(i);
return lastLocale;
}
}
if(input.isCursorVisible()){
Draw.color();
public void showTextInput(String title, String text, String def, TextFieldFilter filter, Consumer<String> confirmed){
new Dialog(title, "dialog"){{
content().margin(30).add(text).padRight(6f);
TextField field = content().addField(def, t->{}).size(170f, 50f).get();
field.setTextFieldFilter((f, c) -> field.getText().length() < 12 && filter.acceptChar(f, c));
Platform.instance.addDialog(field);
buttons().defaults().size(120, 54).pad(4);
buttons().addButton("$text.ok", () -> {
confirmed.accept(field.getText());
hide();
}).disabled(b -> field.getText().isEmpty());
buttons().addButton("$text.cancel", this::hide);
}}.show();
}
float scl = Unit.dp.scl(3f);
public void showTextInput(String title, String text, String def, Consumer<String> confirmed){
showTextInput(title, text, def, (field, c) -> true, confirmed);
}
Draw.rect("controller-cursor", input.getMouseX(), Gdx.graphics.getHeight() - input.getMouseY(), 16 * scl, 16 * scl);
}
}
public void showInfo(String info){
new Dialog("$text.info.title", "dialog"){{
content().margin(15).add(info).width(600f).get().setWrap(true);
buttons().addButton("$text.ok", this::hide).size(90, 50).pad(4);
}}.show();
}
Graphics.end();
Draw.color();
}
public void showError(String text){
new Dialog("$text.error.title", "dialog"){{
content().margin(15).add(text);
buttons().addButton("$text.ok", this::hide).size(90, 50).pad(4);
}}.show();
}
@Override
public void init(){
editor = new MapEditorDialog();
controls = new ControlsDialog();
restart = new RestartDialog();
join = new JoinDialog();
discord = new DiscordDialog();
load = new LoadDialog();
levels = new LevelDialog();
language = new LanguageDialog();
settings = new SettingsMenuDialog();
paused = new PausedDialog();
changelog = new ChangelogDialog();
about = new AboutDialog();
host = new HostDialog();
bans = new BansDialog();
admins = new AdminsDialog();
traces = new TraceDialog();
rollback = new RollbackDialog();
maps = new MapsDialog();
localplayers = new LocalPlayerDialog();
unlocks = new UnlocksDialog();
content = new ContentInfoDialog();
public void showConfirm(String title, String text, Listenable confirmed){
FloatingDialog dialog = new FloatingDialog(title);
dialog.content().add(text).pad(4f);
dialog.buttons().defaults().size(200f, 54f).pad(2f);
dialog.buttons().addButton("$text.cancel", dialog::hide);
dialog.buttons().addButton("$text.ok", () -> {
dialog.hide();
confirmed.listen();
});
dialog.keyDown(Keys.ESCAPE, dialog::hide);
dialog.keyDown(Keys.BACK, dialog::hide);
dialog.show();
}
build.begin(scene);
public void showConfirmListen(String title, String text, Consumer<Boolean> listener){
FloatingDialog dialog = new FloatingDialog(title);
dialog.content().add(text).pad(4f);
dialog.buttons().defaults().size(200f, 54f).pad(2f);
dialog.buttons().addButton("$text.cancel", () -> {
dialog.hide();
listener.accept(true);
});
dialog.buttons().addButton("$text.ok", () -> {
dialog.hide();
listener.accept(true);
});
dialog.show();
}
Group group = Core.scene.getRoot();
backfrag.build(group);
hudfrag.build(group);
menufrag.build(group);
chatfrag.container().build(group);
listfrag.build(group);
debugfrag.build(group);
loadfrag.build(group);
build.end();
}
@Override
public boolean hasMouse(){
return super.hasMouse();
}
@Override
public void resize(int width, int height){
super.resize(width, height);
Events.fire(ResizeEvent.class);
}
public Locale getLocale(){
String loc = Settings.getString("locale");
if(loc.equals("default")){
return Locale.getDefault();
}else{
if(lastLocale == null || !lastLocale.toString().equals(loc)){
if(loc.contains("_")){
String[] split = loc.split("_");
lastLocale = new Locale(split[0], split[1]);
}else{
lastLocale = new Locale(loc);
}
}
return lastLocale;
}
}
public void loadAnd(Callable call){
loadAnd("$text.loading", call);
}
public void loadAnd(String text, Callable call){
loadfrag.show(text);
Timers.runTask(7f, () -> {
call.run();
loadfrag.hide();
});
}
public void loadLogic(Callable call){
loadLogic("$text.loading", call);
}
public void loadLogic(String text, Callable call){
loadfrag.show();
Timers.runTask(7f, () -> {
threads.run(() -> {
call.run();
threads.runGraphics(loadfrag::hide);
});
});
}
public void showTextInput(String title, String text, String def, TextFieldFilter filter, Consumer<String> confirmed){
new Dialog(title, "dialog"){{
content().margin(30).add(text).padRight(6f);
TextField field = content().addField(def, t -> {
}).size(170f, 50f).get();
field.setTextFieldFilter((f, c) -> field.getText().length() < 12 && filter.acceptChar(f, c));
Platform.instance.addDialog(field);
buttons().defaults().size(120, 54).pad(4);
buttons().addButton("$text.ok", () -> {
confirmed.accept(field.getText());
hide();
}).disabled(b -> field.getText().isEmpty());
buttons().addButton("$text.cancel", this::hide);
}}.show();
}
public void showTextInput(String title, String text, String def, Consumer<String> confirmed){
showTextInput(title, text, def, (field, c) -> true, confirmed);
}
public void showInfoFade(String info){
Table table = new Table();
table.setFillParent(true);
table.actions(Actions.fadeOut(7f, Interpolation.fade), Actions.removeActor());
table.top().add(info).padTop(8);
Core.scene.add(table);
}
public void showInfo(String info){
new Dialog("$text.info.title", "dialog"){{
getCell(content()).growX();
content().margin(15).add(info).width(400f).wrap().get().setAlignment(Align.center, Align.center);
buttons().addButton("$text.ok", this::hide).size(90, 50).pad(4);
}}.show();
}
public void showError(String text){
new Dialog("$text.error.title", "dialog"){{
content().margin(15).add(text).width(400f).wrap().get().setAlignment(Align.center, Align.center);
buttons().addButton("$text.ok", this::hide).size(90, 50).pad(4);
}}.show();
}
public void showConfirm(String title, String text, Listenable confirmed){
FloatingDialog dialog = new FloatingDialog(title);
dialog.content().add(text).width(400f).wrap().pad(4f).get().setAlignment(Align.center, Align.center);
dialog.buttons().defaults().size(200f, 54f).pad(2f);
dialog.buttons().addButton("$text.cancel", dialog::hide);
dialog.buttons().addButton("$text.ok", () -> {
dialog.hide();
confirmed.listen();
});
dialog.keyDown(Keys.ESCAPE, dialog::hide);
dialog.keyDown(Keys.BACK, dialog::hide);
dialog.show();
}
}

View File

@@ -2,358 +2,380 @@ package io.anuke.mindustry.core;
import com.badlogic.gdx.math.GridPoint2;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.ai.Pathfind;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.game.SpawnPoint;
import com.badlogic.gdx.utils.ObjectMap;
import io.anuke.mindustry.ai.BlockIndexer;
import io.anuke.mindustry.ai.Pathfinder;
import io.anuke.mindustry.content.blocks.Blocks;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.game.EventType.TileChangeEvent;
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.io.Map;
import io.anuke.mindustry.io.MapIO;
import io.anuke.mindustry.io.MapMeta;
import io.anuke.mindustry.io.Maps;
import io.anuke.mindustry.world.*;
import io.anuke.mindustry.world.blocks.Blocks;
import io.anuke.mindustry.world.blocks.DistributionBlocks;
import io.anuke.mindustry.world.blocks.ProductionBlocks;
import io.anuke.mindustry.world.blocks.WeaponBlocks;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.Entity;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.mapgen.WorldGenerator;
import io.anuke.ucore.core.Events;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.EntityPhysics;
import io.anuke.ucore.modules.Module;
import io.anuke.ucore.util.Log;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.ThreadArray;
import io.anuke.ucore.util.Tmp;
import static io.anuke.mindustry.Vars.control;
import static io.anuke.mindustry.Vars.tilesize;
import static io.anuke.mindustry.Vars.*;
public class World extends Module{
private int seed;
private Map currentMap;
private Tile[][] tiles;
private Pathfind pathfind = new Pathfind();
private Maps maps = new Maps();
private Tile core;
private Array<SpawnPoint> spawns = new Array<>();
private int seed;
private Tile[] temptiles = new Tile[4];
public World(){
maps.loadMaps();
currentMap = maps.getMap(0);
}
@Override
public void dispose(){
maps.dispose();
}
private Map currentMap;
private Tile[][] tiles;
private Pathfinder pathfinder = new Pathfinder();
private BlockIndexer indexer = new BlockIndexer();
private Maps maps = new Maps();
public Array<SpawnPoint> getSpawns(){
return spawns;
}
private Array<Tile> tempTiles = new ThreadArray<>();
private boolean generating, invalidMap;
public Tile getCore(){
return core;
}
public Maps maps(){
return maps;
}
public Pathfind pathfinder(){
return pathfind;
}
public World(){
maps.load();
}
public float getSpawnX(){
return core.worldx();
}
@Override
public void dispose(){
maps.dispose();
}
public float getSpawnY(){
return core.worldy() - tilesize*2;
}
public boolean solid(int x, int y){
Tile tile = tile(x, y);
return tile == null || tile.solid();
}
public boolean passable(int x, int y){
Tile tile = tile(x, y);
return tile != null && tile.passable();
}
public boolean wallSolid(int x, int y){
Tile tile = tile(x, y);
return tile == null || tile.block().solid;
}
public boolean isAccessible(int x, int y){
return !wallSolid(x, y-1) || !wallSolid(x, y+1) || !wallSolid(x-1, y) ||!wallSolid(x+1, y);
}
public boolean blends(Block block, int x, int y){
return !floorBlends(x, y-1, block) || !floorBlends(x, y+1, block)
|| !floorBlends(x-1, y, block) ||!floorBlends(x+1, y, block);
}
public boolean floorBlends(int x, int y, Block block){
Tile tile = tile(x, y);
return tile == null || tile.floor().id <= block.id;
}
public Map getMap(){
return currentMap;
}
public int width(){
return currentMap.getWidth();
}
public int height(){
return currentMap.getHeight();
}
public Maps maps(){
return maps;
}
public Tile tile(int packed){
return tile(packed % width(), packed / width());
}
public Tile tile(int x, int y){
if(tiles == null){
return null;
}
if(!Mathf.inBounds(x, y, tiles)) return null;
return tiles[x][y];
}
public Tile tileWorld(float x, float y){
return tile(Mathf.scl2(x, tilesize), Mathf.scl2(y, tilesize));
}
public BlockIndexer indexer(){
return indexer;
}
public int toTile(float coord){
return Mathf.scl2(coord, tilesize);
}
public Tile[][] getTiles(){
return tiles;
}
private void createTiles(){
for(int x = 0; x < tiles.length; x ++){
for(int y = 0; y < tiles[0].length; y ++){
if(tiles[x][y] == null){
tiles[x][y] = new Tile(x, y, Blocks.stone);
}
}
}
}
private void clearTileEntities(){
for(int x = 0; x < tiles.length; x ++){
for(int y = 0; y < tiles[0].length; y ++){
if(tiles[x][y] != null && tiles[x][y].entity != null){
tiles[x][y].entity.remove();
}
}
}
}
public void loadMap(Map map){
loadMap(map, MathUtils.random(0, 99999));
}
public void loadMap(Map map, int seed){
currentMap = map;
if(tiles != null){
clearTileEntities();
if(tiles.length != map.getWidth() || tiles[0].length != map.getHeight()){
tiles = new Tile[map.getWidth()][map.getHeight()];
}
createTiles();
}else{
tiles = new Tile[map.getWidth()][map.getHeight()];
createTiles();
}
spawns.clear();
Entities.resizeTree(0, 0, map.getWidth() * tilesize, map.getHeight() * tilesize);
this.seed = seed;
core = WorldGenerator.generate(map.pixmap, tiles, spawns);
public Pathfinder pathfinder(){
return pathfinder;
}
Placement.placeBlock(core.x, core.y, ProductionBlocks.core, 0, false, false);
if(!map.name.equals("tutorial")){
setDefaultBlocks();
}else{
control.tutorial().setDefaultBlocks(core.x, core.y);
}
pathfind.resetPaths();
}
void setDefaultBlocks(){
int x = core.x, y = core.y;
int flip = Mathf.sign(!currentMap.flipBase);
int fr = currentMap.flipBase ? 2 : 0;
set(x, y-2*flip, DistributionBlocks.conveyor, 1 + fr);
set(x, y-3*flip, DistributionBlocks.conveyor, 1 + fr);
for(int i = 0; i < 2; i ++){
int d = Mathf.sign(i-0.5f);
set(x+2*d, y-2*flip, ProductionBlocks.stonedrill, d);
set(x+2*d, y-1*flip, DistributionBlocks.conveyor, 1 + fr);
set(x+2*d, y, DistributionBlocks.conveyor, 1 + fr);
set(x+2*d, y+1*flip, WeaponBlocks.doubleturret, 0 + fr);
set(x+1*d, y-3*flip, DistributionBlocks.conveyor, 2*d);
set(x+2*d, y-3*flip, DistributionBlocks.conveyor, 2*d);
set(x+2*d, y-4*flip, DistributionBlocks.conveyor, 1 + fr);
set(x+2*d, y-5*flip, DistributionBlocks.conveyor, 1 + fr);
set(x+3*d, y-5*flip, ProductionBlocks.stonedrill, 0 + fr);
set(x+3*d, y-4*flip, ProductionBlocks.stonedrill, 0 + fr);
set(x+3*d, y-3*flip, ProductionBlocks.stonedrill, 0 + fr);
}
}
void set(int x, int y, Block type, int rot){
if(!Mathf.inBounds(x, y, tiles)){
return;
}
if(type == ProductionBlocks.stonedrill){
tiles[x][y].setFloor(Blocks.stone);
}
tiles[x][y].setBlock(type, rot);
}
public int getSeed(){
return seed;
}
public boolean isInvalidMap(){
return invalidMap;
}
public void removeBlock(Tile tile){
if(!tile.block().isMultiblock() && !tile.isLinked()){
tile.setBlock(Blocks.air);
}else{
Tile target = tile.target();
Array<Tile> removals = target.getLinkedTiles();
for(Tile toremove : removals){
//note that setting a new block automatically unlinks it
if(toremove != null) toremove.setBlock(Blocks.air);
}
}
}
public TileEntity findTileTarget(float x, float y, Tile tile, float range, boolean damaged){
Entity closest = null;
float dst = 0;
int rad = (int)(range/tilesize)+1;
int tilex = Mathf.scl2(x, tilesize);
int tiley = Mathf.scl2(y, tilesize);
for(int rx = -rad; rx <= rad; rx ++){
for(int ry = -rad; ry <= rad; ry ++){
Tile other = tile(rx+tilex, ry+tiley);
if(other != null && other.getLinked() != null){
other = other.getLinked();
}
if(other == null || other.entity == null || (tile != null && other.entity == tile.entity)) continue;
TileEntity e = other.entity;
if(damaged && e.health >= e.tile.block().health)
continue;
float ndst = Vector2.dst(x, y, e.x, e.y);
if(ndst < range && (closest == null || ndst < dst)){
dst = ndst;
closest = e;
}
}
}
public boolean solid(int x, int y){
Tile tile = tile(x, y);
return (TileEntity) closest;
}
return tile == null || tile.solid();
}
/**Raycast, but with world coordinates.*/
public GridPoint2 raycastWorld(float x, float y, float x2, float y2){
return raycast(Mathf.scl2(x, tilesize), Mathf.scl2(y, tilesize),
Mathf.scl2(x2, tilesize), Mathf.scl2(y2, tilesize));
}
/**
* Input is in block coordinates, not world coordinates.
* @return null if no collisions found, block position otherwise.*/
public GridPoint2 raycast(int x0f, int y0f, int x1, int y1){
int x0 = x0f;
int y0 = y0f;
int dx = Math.abs(x1 - x0);
int dy = Math.abs(y1 - y0);
public boolean passable(int x, int y){
Tile tile = tile(x, y);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
return tile != null && tile.passable();
}
int err = dx - dy;
int e2;
while(true){
public boolean wallSolid(int x, int y){
Tile tile = tile(x, y);
return tile == null || tile.block().solid;
}
if(!passable(x0, y0)){
return Tmp.g1.set(x0, y0);
}
if(x0 == x1 && y0 == y1) break;
public boolean isAccessible(int x, int y){
return !wallSolid(x, y - 1) || !wallSolid(x, y + 1) || !wallSolid(x - 1, y) || !wallSolid(x + 1, y);
}
e2 = 2 * err;
if(e2 > -dy){
err = err - dy;
x0 = x0 + sx;
}
public boolean floorBlends(int x, int y, Block block){
Tile tile = tile(x, y);
return tile == null || tile.floor().id <= block.id;
}
if(e2 < dx){
err = err + dx;
y0 = y0 + sy;
}
}
return null;
}
public Map getMap(){
return currentMap;
}
public void raycastEach(int x0f, int y0f, int x1, int y1, Raycaster cons){
int x0 = x0f;
int y0 = y0f;
int dx = Math.abs(x1 - x0);
int dy = Math.abs(y1 - y0);
public void setMap(Map map){
this.currentMap = map;
}
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
public int width(){
return tiles == null ? 0 : tiles.length;
}
int err = dx - dy;
int e2;
while(true){
public int height(){
return tiles == null ? 0 : tiles[0].length;
}
if(cons.accept(x0, y0)) break;
if(x0 == x1 && y0 == y1) break;
public int toPacked(int x, int y){
return x + y * width();
}
e2 = 2 * err;
if(e2 > -dy){
err = err - dy;
x0 = x0 + sx;
}
public Tile tile(int packed){
return tiles == null ? null : tile(packed % width(), packed / width());
}
if(e2 < dx){
err = err + dx;
y0 = y0 + sy;
}
}
}
public Tile tile(int x, int y){
if(tiles == null){
return null;
}
if(!Mathf.inBounds(x, y, tiles)) return null;
return tiles[x][y];
}
public interface Raycaster{
boolean accept(int x, int y);
}
public Tile rawTile(int x, int y){
return tiles[x][y];
}
public Tile tileWorld(float x, float y){
return tile(Mathf.scl2(x, tilesize), Mathf.scl2(y, tilesize));
}
public int toTile(float coord){
return Mathf.scl2(coord, tilesize);
}
public Tile[][] getTiles(){
return tiles;
}
private void clearTileEntities(){
for(int x = 0; x < tiles.length; x++){
for(int y = 0; y < tiles[0].length; y++){
if(tiles[x][y] != null && tiles[x][y].entity != null){
tiles[x][y].entity.remove();
}
}
}
}
/**
* Resizes the tile array to the specified size and returns the resulting tile array.
* Only use for loading saves!
*/
public Tile[][] createTiles(int width, int height){
if(tiles != null){
clearTileEntities();
if(tiles.length != width || tiles[0].length != height){
tiles = new Tile[width][height];
}
}else{
tiles = new Tile[width][height];
}
return tiles;
}
/**
* Call to signify the beginning of map loading.
* TileChangeEvents will not be fired until endMapLoad().
*/
public void beginMapLoad(){
generating = true;
}
/**
* Call to signify the end of map loading. Updates tile occlusions and sets up physics for the world.
* A WorldLoadEvent will be fire.
*/
public void endMapLoad(){
for(int x = 0; x < tiles.length; x++){
for(int y = 0; y < tiles[0].length; y++){
tiles[x][y].updateOcclusion();
if(tiles[x][y].entity != null){
tiles[x][y].entity.updateProximity();
}
}
}
EntityPhysics.resizeTree(0, 0, tiles.length * tilesize, tiles[0].length * tilesize);
generating = false;
Events.fire(WorldLoadEvent.class);
}
/**
* Loads up a procedural map. This does not call play(), but calls reset().
*/
public void loadProceduralMap(){
Timers.mark();
Timers.mark();
logic.reset();
beginMapLoad();
int width = 400, height = 400;
Tile[][] tiles = createTiles(width, height);
Map map = new Map("Generated Map", new MapMeta(0, new ObjectMap<>(), width, height, null), true, () -> null);
setMap(map);
EntityPhysics.resizeTree(0, 0, width * tilesize, height * tilesize);
Timers.mark();
WorldGenerator.generateMap(tiles, Mathf.random(9999999));
Log.info("Time to generate base map: {0}", Timers.elapsed());
Log.info("Time to generate fully without additional events: {0}", Timers.elapsed());
endMapLoad();
Log.info("Full time to generate: {0}", Timers.elapsed());
}
public void loadMap(Map map){
loadMap(map, MathUtils.random(0, 999999));
}
public void loadMap(Map map, int seed){
beginMapLoad();
this.currentMap = map;
this.seed = seed;
int width = map.meta.width, height = map.meta.height;
createTiles(width, height);
EntityPhysics.resizeTree(0, 0, width * tilesize, height * tilesize);
WorldGenerator.loadTileData(tiles, MapIO.readTileData(map, true), map.meta.hasOreGen(), seed);
if(!headless && state.teams.get(players[0].getTeam()).cores.size == 0){
ui.showError("$text.map.nospawn");
threads.runDelay(() -> state.set(State.menu));
invalidMap = true;
}else{
invalidMap = false;
}
endMapLoad();
}
public int getSeed(){
return seed;
}
public void notifyChanged(Tile tile){
if(!generating){
threads.runDelay(() -> Events.fire(TileChangeEvent.class, tile));
}
}
public void removeBlock(Tile tile){
if(!tile.block().isMultiblock() && !tile.isLinked()){
tile.setBlock(Blocks.air);
}else{
Tile target = tile.target();
Array<Tile> removals = target.getLinkedTiles(tempTiles);
for(Tile toremove : removals){
//note that setting a new block automatically unlinks it
if(toremove != null) toremove.setBlock(Blocks.air);
}
}
}
public void setBlock(Tile tile, Block block, Team team){
tile.setBlock(block);
if(block.isMultiblock()){
int offsetx = -(block.size - 1) / 2;
int offsety = -(block.size - 1) / 2;
for(int dx = 0; dx < block.size; dx++){
for(int dy = 0; dy < block.size; dy++){
int worldx = dx + offsetx + tile.x;
int worldy = dy + offsety + tile.y;
if(!(worldx == tile.x && worldy == tile.y)){
Tile toplace = world.tile(worldx, worldy);
if(toplace != null){
toplace.setLinked((byte) (dx + offsetx), (byte) (dy + offsety));
toplace.setTeam(team);
}
}
}
}
}
}
/**
* Raycast, but with world coordinates.
*/
public GridPoint2 raycastWorld(float x, float y, float x2, float y2){
return raycast(Mathf.scl2(x, tilesize), Mathf.scl2(y, tilesize),
Mathf.scl2(x2, tilesize), Mathf.scl2(y2, tilesize));
}
/**
* Input is in block coordinates, not world coordinates.
*
* @return null if no collisions found, block position otherwise.
*/
public GridPoint2 raycast(int x0f, int y0f, int x1, int y1){
int x0 = x0f;
int y0 = y0f;
int dx = Math.abs(x1 - x0);
int dy = Math.abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx - dy;
int e2;
while(true){
if(!passable(x0, y0)){
return Tmp.g1.set(x0, y0);
}
if(x0 == x1 && y0 == y1) break;
e2 = 2 * err;
if(e2 > -dy){
err = err - dy;
x0 = x0 + sx;
}
if(e2 < dx){
err = err + dx;
y0 = y0 + sy;
}
}
return null;
}
public void raycastEach(int x0f, int y0f, int x1, int y1, Raycaster cons){
int x0 = x0f;
int y0 = y0f;
int dx = Math.abs(x1 - x0);
int dy = Math.abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx - dy;
int e2;
while(true){
if(cons.accept(x0, y0)) break;
if(x0 == x1 && y0 == y1) break;
e2 = 2 * err;
if(e2 > -dy){
err = err - dy;
x0 = x0 + sx;
}
if(e2 < dx){
err = err + dx;
y0 = y0 + sy;
}
}
}
public interface Raycaster{
boolean accept(int x, int y);
}
}

View File

@@ -0,0 +1,72 @@
package io.anuke.mindustry.editor;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.IntSet;
import io.anuke.mindustry.io.MapTileData;
import io.anuke.mindustry.io.MapTileData.TileDataMarker;
import io.anuke.ucore.util.Bits;
public class DrawOperation{
/**
* Data to apply operation to.
*/
private MapTileData data;
/**
* List of per-tile operations that occurred.
*/
private Array<TileOperation> operations = new Array<>();
/**
* Checks for duplicate operations, useful for brushes.
*/
private IntSet checks = new IntSet();
public DrawOperation(MapTileData data){
this.data = data;
}
public boolean isEmpty(){
return operations.size == 0;
}
public boolean checkDuplicate(short x, short y){
int i = Bits.packInt(x, y);
if(checks.contains(i)) return true;
checks.add(i);
return false;
}
public void addOperation(TileOperation op){
operations.add(op);
}
public void undo(MapEditor editor){
for(int i = operations.size - 1; i >= 0; i--){
TileOperation op = operations.get(i);
data.position(op.x, op.y);
data.write(op.from);
editor.renderer().updatePoint(op.x, op.y);
}
}
public void redo(MapEditor editor){
for(TileOperation op : operations){
data.position(op.x, op.y);
data.write(op.to);
editor.renderer().updatePoint(op.x, op.y);
}
}
public static class TileOperation{
public short x, y;
public TileDataMarker from;
public TileDataMarker to;
public TileOperation(short x, short y, TileDataMarker from, TileDataMarker to){
this.x = x;
this.y = y;
this.from = from;
this.to = to;
}
}
}

View File

@@ -0,0 +1,143 @@
package io.anuke.mindustry.editor;
import com.badlogic.gdx.utils.IntArray;
import com.badlogic.gdx.utils.IntSet;
import io.anuke.mindustry.content.blocks.Blocks;
import io.anuke.mindustry.io.MapTileData.DataPosition;
import io.anuke.mindustry.io.MapTileData.TileDataMarker;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.Floor;
import io.anuke.ucore.util.Bits;
import static io.anuke.mindustry.Vars.ui;
public enum EditorTool{
pick{
public void touched(MapEditor editor, int x, int y){
byte bf = editor.getMap().read(x, y, DataPosition.floor);
byte bw = editor.getMap().read(x, y, DataPosition.wall);
byte link = editor.getMap().read(x, y, DataPosition.link);
if(link != 0){
x -= (Bits.getLeftByte(link) - 8);
y -= (Bits.getRightByte(link) - 8);
bf = editor.getMap().read(x, y, DataPosition.floor);
bw = editor.getMap().read(x, y, DataPosition.wall);
}
Block block = Block.getByID(bw == 0 ? bf : bw);
editor.setDrawBlock(block);
ui.editor.updateSelectedBlock();
}
},
pencil{
{
edit = true;
draggable = true;
}
@Override
public void touched(MapEditor editor, int x, int y){
editor.draw(x, y);
}
},
eraser{
{
edit = true;
draggable = true;
}
@Override
public void touched(MapEditor editor, int x, int y){
editor.draw(x, y, Blocks.air);
}
},
elevation{
{
edit = true;
draggable = true;
}
@Override
public void touched(MapEditor editor, int x, int y){
editor.elevate(x, y);
}
},
line{
{
}
},
fill{
{
edit = true;
}
public void touched(MapEditor editor, int x, int y){
if(editor.getDrawBlock().isMultiblock()){
//don't fill multiblocks, thanks
pencil.touched(editor, x, y);
return;
}
boolean floor = editor.getDrawBlock() instanceof Floor;
byte bf = editor.getMap().read(x, y, DataPosition.floor);
byte bw = editor.getMap().read(x, y, DataPosition.wall);
boolean synth = editor.getDrawBlock().synthetic();
byte brt = Bits.packByte((byte) editor.getDrawRotation(), (byte) editor.getDrawTeam().ordinal());
byte dest = floor ? bf : bw;
byte draw = (byte) editor.getDrawBlock().id;
int width = editor.getMap().width();
int height = editor.getMap().height();
IntSet set = new IntSet();
IntArray points = new IntArray();
points.add(asInt(x, y, editor.getMap().width()));
while(points.size != 0){
int pos = points.pop();
int px = pos % width;
int py = pos / width;
set.add(pos);
byte nbf = editor.getMap().read(px, py, DataPosition.floor);
byte nbw = editor.getMap().read(px, py, DataPosition.wall);
if((floor ? nbf : nbw) == dest){
TileDataMarker prev = editor.getPrev(px, py, false);
if(floor){
editor.getMap().write(px, py, DataPosition.floor, draw);
}else{
editor.getMap().write(px, py, DataPosition.wall, draw);
}
if(synth){
editor.getMap().write(px, py, DataPosition.rotationTeam, brt);
}
if(px > 0 && !set.contains(asInt(px - 1, py, width))) points.add(asInt(px - 1, py, width));
if(py > 0 && !set.contains(asInt(px, py - 1, width))) points.add(asInt(px, py - 1, width));
if(px < width - 1 && !set.contains(asInt(px + 1, py, width))) points.add(asInt(px + 1, py, width));
if(py < height - 1 && !set.contains(asInt(px, py + 1, width))) points.add(asInt(px, py + 1, width));
editor.onWrite(px, py, prev);
}
}
}
int asInt(int x, int y, int width){
return x + y * width;
}
},
zoom;
boolean edit, draggable;
public void touched(MapEditor editor, int x, int y){
}
}

View File

@@ -0,0 +1,282 @@
package io.anuke.mindustry.editor;
import com.badlogic.gdx.utils.ObjectMap;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.content.blocks.Blocks;
import io.anuke.mindustry.editor.DrawOperation.TileOperation;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.io.MapTileData;
import io.anuke.mindustry.io.MapTileData.DataPosition;
import io.anuke.mindustry.io.MapTileData.TileDataMarker;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.Floor;
import io.anuke.ucore.util.Bits;
import io.anuke.ucore.util.Mathf;
public class MapEditor{
public static final int minMapSize = 128, maxMapSize = 512;
public static final int[] brushSizes = {1, 2, 3, 4, 5, 9, 15};
private MapTileData map;
private ObjectMap<String, String> tags = new ObjectMap<>();
private MapRenderer renderer = new MapRenderer(this);
private int brushSize = 1;
private byte elevation;
private int rotation;
private Block drawBlock = Blocks.stone;
private Team drawTeam = Team.none;
public MapEditor(){
}
public MapTileData getMap(){
return map;
}
public ObjectMap<String, String> getTags(){
return tags;
}
public void beginEdit(MapTileData map, ObjectMap<String, String> tags, boolean clear){
this.map = map;
this.brushSize = 1;
this.tags = tags;
if(clear){
for(int x = 0; x < map.width(); x++){
for(int y = 0; y < map.height(); y++){
map.write(x, y, DataPosition.floor, (byte) Blocks.stone.id);
}
}
}
drawBlock = Blocks.stone;
renderer.resize(map.width(), map.height());
}
public byte getDrawElevation(){
return elevation;
}
public void setDrawElevation(int elevation){
this.elevation = (byte) elevation;
}
public int getDrawRotation(){
return rotation;
}
public void setDrawRotation(int rotation){
this.rotation = rotation;
}
public Team getDrawTeam(){
return drawTeam;
}
public void setDrawTeam(Team team){
this.drawTeam = team;
}
public Block getDrawBlock(){
return drawBlock;
}
public void setDrawBlock(Block block){
this.drawBlock = block;
}
public int getBrushSize(){
return brushSize;
}
public void setBrushSize(int size){
this.brushSize = size;
}
public void draw(int x, int y){
draw(x, y, drawBlock);
}
public void draw(int x, int y, Block drawBlock){
if(x < 0 || y < 0 || x >= map.width() || y >= map.height()){
return;
}
byte writeID = (byte) drawBlock.id;
byte partID = (byte) Blocks.blockpart.id;
byte rotationTeam = Bits.packByte(drawBlock.rotate ? (byte) rotation : 0, drawBlock.synthetic() ? (byte) drawTeam.ordinal() : 0);
boolean isfloor = drawBlock instanceof Floor && drawBlock != Blocks.air;
if(drawBlock.isMultiblock()){
x = Mathf.clamp(x, (drawBlock.size-1)/2, map.width() - drawBlock.size/2 - 1);
y = Mathf.clamp(y, (drawBlock.size-1)/2, map.height() - drawBlock.size/2 - 1);
int offsetx = -(drawBlock.size - 1) / 2;
int offsety = -(drawBlock.size - 1) / 2;
for(int i = 0; i < 2; i++){
for(int dx = 0; dx < drawBlock.size; dx++){
for(int dy = 0; dy < drawBlock.size; dy++){
int worldx = dx + offsetx + x;
int worldy = dy + offsety + y;
if(Mathf.inBounds(worldx, worldy, map.width(), map.height())){
TileDataMarker prev = getPrev(worldx, worldy, false);
if(i == 1){
map.write(worldx, worldy, DataPosition.wall, partID);
map.write(worldx, worldy, DataPosition.rotationTeam, rotationTeam);
map.write(worldx, worldy, DataPosition.link, Bits.packByte((byte) (dx + offsetx + 8), (byte) (dy + offsety + 8)));
}else{
byte link = map.read(worldx, worldy, DataPosition.link);
byte block = map.read(worldx, worldy, DataPosition.wall);
if(link != 0){
removeLinked(worldx - (Bits.getLeftByte(link) - 8), worldy - (Bits.getRightByte(link) - 8));
}else if(Block.getByID(block).isMultiblock()){
removeLinked(worldx, worldy);
}
}
onWrite(worldx, worldy, prev);
}
}
}
}
TileDataMarker prev = getPrev(x, y, false);
map.write(x, y, DataPosition.wall, writeID);
map.write(x, y, DataPosition.link, (byte) 0);
map.write(x, y, DataPosition.rotationTeam, rotationTeam);
onWrite(x, y, prev);
}else{
for(int rx = -brushSize; rx <= brushSize; rx++){
for(int ry = -brushSize; ry <= brushSize; ry++){
if(Mathf.dst(rx, ry) <= brushSize - 0.5f){
int wx = x + rx, wy = y + ry;
if(wx < 0 || wy < 0 || wx >= map.width() || wy >= map.height()){
continue;
}
TileDataMarker prev = getPrev(wx, wy, true);
if(!isfloor){
byte link = map.read(wx, wy, DataPosition.link);
if(link != 0){
removeLinked(wx - (Bits.getLeftByte(link) - 8), wy - (Bits.getRightByte(link) - 8));
}
}
if(isfloor){
map.write(wx, wy, DataPosition.floor, writeID);
map.write(wx, wy, DataPosition.elevation, elevation);
}else{
map.write(wx, wy, DataPosition.wall, writeID);
map.write(wx, wy, DataPosition.link, (byte) 0);
map.write(wx, wy, DataPosition.rotationTeam, rotationTeam);
}
onWrite(x + rx, y + ry, prev);
}
}
}
}
}
public void elevate(int x, int y){
if(x < 0 || y < 0 || x >= map.width() || y >= map.height()){
return;
}
for(int rx = -brushSize; rx <= brushSize; rx++){
for(int ry = -brushSize; ry <= brushSize; ry++){
if(Mathf.dst(rx, ry) <= brushSize - 0.5f){
int wx = x + rx, wy = y + ry;
if(wx < 0 || wy < 0 || wx >= map.width() || wy >= map.height()){
continue;
}
TileDataMarker prev = getPrev(wx, wy, true);
map.write(wx, wy, DataPosition.elevation, elevation);
onWrite(x + rx, y + ry, prev);
}
}
}
}
private void removeLinked(int x, int y){
Block block = Block.getByID(map.read(x, y, DataPosition.wall));
int offsetx = -(block.size - 1) / 2;
int offsety = -(block.size - 1) / 2;
for(int dx = 0; dx < block.size; dx++){
for(int dy = 0; dy < block.size; dy++){
int worldx = x + dx + offsetx, worldy = y + dy + offsety;
if(Mathf.inBounds(worldx, worldy, map.width(), map.height())){
TileDataMarker prev = getPrev(worldx, worldy, false);
map.write(worldx, worldy, DataPosition.link, (byte) 0);
map.write(worldx, worldy, DataPosition.rotationTeam, (byte) 0);
map.write(worldx, worldy, DataPosition.wall, (byte) 0);
onWrite(worldx, worldy, prev);
}
}
}
}
boolean checkDupes(int x, int y){
return Vars.ui.editor.getView().checkForDuplicates((short) x, (short) y);
}
void onWrite(int x, int y, TileDataMarker previous){
if(previous == null){
renderer.updatePoint(x, y);
return;
}
TileDataMarker current = map.new TileDataMarker();
map.position(x, y);
map.read(current);
Vars.ui.editor.getView().addTileOp(new TileOperation((short) x, (short) y, previous, current));
renderer.updatePoint(x, y);
}
TileDataMarker getPrev(int x, int y, boolean checkDupes){
if(checkDupes && checkDupes(x, y)){
return null;
}else{
TileDataMarker marker = map.newDataMarker();
map.position(x, y);
map.read(marker);
return marker;
}
}
public MapRenderer renderer(){
return renderer;
}
public void resize(int width, int height){
map = new MapTileData(width, height);
for(int x = 0; x < map.width(); x++){
for(int y = 0; y < map.height(); y++){
map.write(x, y, DataPosition.floor, (byte) Blocks.stone.id);
}
}
renderer.resize(width, height);
}
}

View File

@@ -0,0 +1,623 @@
package io.anuke.mindustry.editor;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.ObjectMap;
import io.anuke.mindustry.content.blocks.StorageBlocks;
import io.anuke.mindustry.core.Platform;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.io.Map;
import io.anuke.mindustry.io.MapIO;
import io.anuke.mindustry.io.MapMeta;
import io.anuke.mindustry.io.MapTileData;
import io.anuke.mindustry.type.Recipe;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import io.anuke.mindustry.world.Block;
import io.anuke.ucore.core.Core;
import io.anuke.ucore.core.Graphics;
import io.anuke.ucore.core.Inputs;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.function.Listenable;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.input.Input;
import io.anuke.ucore.scene.actions.Actions;
import io.anuke.ucore.scene.builders.build;
import io.anuke.ucore.scene.builders.label;
import io.anuke.ucore.scene.builders.table;
import io.anuke.ucore.scene.ui.*;
import io.anuke.ucore.scene.ui.layout.Stack;
import io.anuke.ucore.scene.ui.layout.Table;
import io.anuke.ucore.scene.ui.layout.Unit;
import io.anuke.ucore.scene.utils.UIUtils;
import io.anuke.ucore.util.Bundles;
import io.anuke.ucore.util.Log;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Strings;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import static io.anuke.mindustry.Vars.*;
public class MapEditorDialog extends Dialog implements Disposable{
private MapEditor editor;
private MapView view;
private MapInfoDialog infoDialog;
private MapLoadDialog loadDialog;
private MapResizeDialog resizeDialog;
private ScrollPane pane;
private FloatingDialog menu;
private boolean saved = false;
private boolean shownWithMap = false;
private ButtonGroup<ImageButton> blockgroup;
public MapEditorDialog(){
super("$text.mapeditor", "dialog");
editor = new MapEditor();
view = new MapView(editor);
infoDialog = new MapInfoDialog(editor);
menu = new FloatingDialog("$text.menu");
menu.addCloseButton();
float isize = 16 * 2f;
float swidth = 180f;
menu.content().table(t -> {
t.defaults().size(swidth, 60f).padBottom(5).padRight(5).padLeft(5);
t.addImageTextButton("$text.editor.savemap", "icon-floppy-16", isize, this::save).size(swidth * 2f + 10, 60f).colspan(2);
t.row();
t.addImageTextButton("$text.editor.mapinfo", "icon-pencil", isize, () -> {
infoDialog.show();
menu.hide();
});
t.addImageTextButton("$text.editor.resize", "icon-resize", isize, () -> {
resizeDialog.show();
menu.hide();
});
t.row();
t.addImageTextButton("$text.editor.import", "icon-load-map", isize, () ->
createDialog("$text.editor.import",
"$text.editor.importmap", "$text.editor.importmap.description", "icon-load-map", (Listenable) loadDialog::show,
"$text.editor.importfile", "$text.editor.importfile.description", "icon-file", (Listenable) () -> {
Platform.instance.showFileChooser("$text.loadimage", "Map Files", file -> {
ui.loadAnd(() -> {
try{
DataInputStream stream = new DataInputStream(file.read());
MapMeta meta = MapIO.readMapMeta(stream);
MapTileData data = MapIO.readTileData(stream, meta, false);
editor.beginEdit(data, meta.tags, false);
view.clearStack();
}catch(Exception e){
ui.showError(Bundles.format("text.editor.errorimageload", Strings.parseException(e, false)));
Log.err(e);
}
});
}, true, mapExtension);
}/*,
"$text.editor.importimage", "$text.editor.importimage.description", "icon-file-image", (Listenable)() -> {
if(gwt){
ui.showError("$text.web.unsupported");
}else {
Platform.instance.showFileChooser("$text.loadimage", "Image Files", file -> {
ui.loadAnd(() -> {
try{
MapTileData data = MapIO.readPixmap(new Pixmap(file));
editor.beginEdit(data, editor.getTags(), false);
view.clearStack();
}catch (Exception e){
ui.showError(Bundles.format("text.editor.errorimageload", Strings.parseException(e, false)));
Log.err(e);
}
});
}, true, "png");
}
}*/));
t.addImageTextButton("$text.editor.export", "icon-save-map", isize, () -> createDialog("$text.editor.export",
"$text.editor.exportfile", "$text.editor.exportfile.description", "icon-file", (Listenable) () -> {
if(!gwt){
Platform.instance.showFileChooser("$text.saveimage", "Map Files", file -> {
file = file.parent().child(file.nameWithoutExtension() + "." + mapExtension);
FileHandle result = file;
ui.loadAnd(() -> {
try{
if(!editor.getTags().containsKey("name")){
editor.getTags().put("name", result.nameWithoutExtension());
}
MapIO.writeMap(result.write(false), editor.getTags(), editor.getMap());
}catch(Exception e){
ui.showError(Bundles.format("text.editor.errorimagesave", Strings.parseException(e, false)));
Log.err(e);
}
});
}, false, mapExtension);
}else{
try{
ByteArrayOutputStream ba = new ByteArrayOutputStream();
MapIO.writeMap(ba, editor.getTags(), editor.getMap());
Platform.instance.downloadFile(editor.getTags().get("name", "unknown") + "." + mapExtension, ba.toByteArray());
}catch(IOException e){
ui.showError(Bundles.format("text.editor.errorimagesave", Strings.parseException(e, false)));
Log.err(e);
}
}
}/*,
"$text.editor.exportimage", "$text.editor.exportimage.description", "icon-file-image", (Listenable)() -> {
if(gwt){
ui.showError("$text.web.unsupported");
}else{
Platform.instance.showFileChooser("$text.saveimage", "Image Files", file -> {
file = file.parent().child(file.nameWithoutExtension() + ".png");
FileHandle result = file;
ui.loadAnd(() -> {
try{
Pixmaps.write(MapIO.generatePixmap(editor.getMap()), result);
}catch (Exception e){
ui.showError(Bundles.format("text.editor.errorimagesave", Strings.parseException(e, false)));
Log.err(e);
}
});
}, false, "png");
}
}*/));
t.row();
t.row();
});
menu.content().row();
menu.content().addImageTextButton("$text.quit", "icon-back", isize, () -> {
tryExit();
menu.hide();
}).padTop(-5).size(swidth * 2f + 10, 60f);
resizeDialog = new MapResizeDialog(editor, (x, y) -> {
if(!(editor.getMap().width() == x && editor.getMap().height() == y)){
ui.loadAnd(() -> {
editor.resize(x, y);
view.clearStack();
});
}
});
loadDialog = new MapLoadDialog(map -> {
ui.loadAnd(() -> {
try(DataInputStream stream = new DataInputStream(map.stream.get())){
MapMeta meta = MapIO.readMapMeta(stream);
MapTileData data = MapIO.readTileData(stream, meta, false);
editor.beginEdit(data, meta.tags, false);
view.clearStack();
}catch(IOException e){
ui.showError(Bundles.format("text.editor.errormapload", Strings.parseException(e, false)));
Log.err(e);
}
});
});
setFillParent(true);
clearChildren();
margin(0);
build.begin(this);
build();
build.end();
update(() -> {
if(Core.scene.getKeyboardFocus() instanceof Dialog && Core.scene.getKeyboardFocus() != this){
return;
}
Vector2 v = pane.stageToLocalCoordinates(Graphics.mouse());
if(v.x >= 0 && v.y >= 0 && v.x <= pane.getWidth() && v.y <= pane.getHeight()){
Core.scene.setScrollFocus(pane);
}else{
Core.scene.setScrollFocus(null);
}
if(Core.scene != null && Core.scene.getKeyboardFocus() == this){
doInput();
}
});
shown(() -> {
saved = true;
Platform.instance.beginForceLandscape();
view.clearStack();
Core.scene.setScrollFocus(view);
if(!shownWithMap){
editor.beginEdit(new MapTileData(256, 256), new ObjectMap<>(), true);
}
shownWithMap = false;
Timers.runTask(10f, Platform.instance::updateRPC);
});
hidden(() -> {
Platform.instance.updateRPC();
Platform.instance.endForceLandscape();
});
}
private void save(){
String name = editor.getTags().get("name", "");
if(name.isEmpty()){
ui.showError("$text.editor.save.noname");
}else{
Map map = world.maps().getByName(name);
if(map != null && !map.custom){
ui.showError("$text.editor.save.overwrite");
}else{
world.maps().saveMap(name, editor.getMap(), editor.getTags());
ui.showInfoFade("$text.editor.saved");
}
}
menu.hide();
saved = true;
}
/**
* Argument format:
* 0) button name
* 1) description
* 2) icon name
* 3) listener
*/
private FloatingDialog createDialog(String title, Object... arguments){
FloatingDialog dialog = new FloatingDialog(title);
float h = 90f;
dialog.content().defaults().size(360f, h).padBottom(5).padRight(5).padLeft(5);
for(int i = 0; i < arguments.length; i += 4){
String name = (String) arguments[i];
String description = (String) arguments[i + 1];
String iconname = (String) arguments[i + 2];
Listenable listenable = (Listenable) arguments[i + 3];
TextButton button = dialog.content().addButton(name, () -> {
listenable.listen();
dialog.hide();
menu.hide();
}).left().get();
button.clearChildren();
button.table("button", t -> {
t.addImage(iconname).size(16 * 3);
t.update(() -> t.background(button.getClickListener().isOver() ? "button-over" : "button"));
}).padLeft(-10).padBottom(-3).size(h);
button.table(t -> {
t.add(name).growX().wrap();
t.row();
t.add(description).color(Color.GRAY).growX().wrap();
}).growX().padLeft(8);
button.row();
dialog.content().row();
}
dialog.addCloseButton();
dialog.show();
return dialog;
}
@Override
public Dialog show(){
return super.show(Core.scene, Actions.sequence(Actions.alpha(0f), Actions.scaleTo(1f, 1f), Actions.fadeIn(0.3f)));
}
@Override
public void dispose(){
editor.renderer().dispose();
}
public void beginEditMap(InputStream is){
ui.loadAnd(() -> {
try{
shownWithMap = true;
DataInputStream stream = new DataInputStream(is);
MapMeta meta = MapIO.readMapMeta(stream);
editor.beginEdit(MapIO.readTileData(stream, meta, false), meta.tags, false);
is.close();
show();
}catch(Exception e){
Log.err(e);
ui.showError(Bundles.format("text.editor.errorimageload", Strings.parseException(e, false)));
}
});
}
public MapView getView(){
return view;
}
public void resetSaved(){
saved = false;
}
public void updateSelectedBlock(){
Block block = editor.getDrawBlock();
for(int j = 0; j < Block.all().size; j++){
if(block.id == j && j < blockgroup.getButtons().size){
blockgroup.getButtons().get(j).setChecked(true);
break;
}
}
}
public boolean hasPane(){
return Core.scene.getScrollFocus() == pane || Core.scene.getKeyboardFocus() != this;
}
public void build(){
float amount = 10f, baseSize = 60f;
float size = mobile ? (int) (Math.min(Gdx.graphics.getHeight(), Gdx.graphics.getWidth()) / amount / Unit.dp.scl(1f)) :
Math.min(Gdx.graphics.getDisplayMode().height / amount, baseSize);
new table(){{
aleft();
new table("button"){{
margin(0);
Table tools = new Table();
tools.top();
atop();
ButtonGroup<ImageButton> group = new ButtonGroup<>();
Consumer<EditorTool> addTool = tool -> {
ImageButton button = new ImageButton("icon-" + tool.name(), "toggle");
button.clicked(() -> view.setTool(tool));
button.resizeImage(16 * 2f);
button.update(() -> button.setChecked(view.getTool() == tool));
group.add(button);
if(tool == EditorTool.pencil)
button.setChecked(true);
tools.add(button).padBottom(-5.1f);
};
tools.defaults().size(size, size + 4f).padBottom(-5.1f);
//tools.addImageButton("icon-back", 16*2, () -> tryExit());
tools.addImageButton("icon-menu-large", 16 * 2f, menu::show);
ImageButton grid = tools.addImageButton("icon-grid", "toggle", 16 * 2f, () -> view.setGrid(!view.isGrid())).get();
addTool.accept(EditorTool.zoom);
tools.row();
ImageButton undo = tools.addImageButton("icon-undo", 16 * 2f, () -> view.undo()).get();
ImageButton redo = tools.addImageButton("icon-redo", 16 * 2f, () -> view.redo()).get();
addTool.accept(EditorTool.pick);
tools.row();
undo.setDisabled(() -> !view.getStack().canUndo());
redo.setDisabled(() -> !view.getStack().canRedo());
undo.update(() -> undo.getImage().setColor(undo.isDisabled() ? Color.GRAY : Color.WHITE));
redo.update(() -> redo.getImage().setColor(redo.isDisabled() ? Color.GRAY : Color.WHITE));
grid.update(() -> grid.setChecked(view.isGrid()));
addTool.accept(EditorTool.line);
addTool.accept(EditorTool.pencil);
addTool.accept(EditorTool.eraser);
tools.row();
addTool.accept(EditorTool.fill);
addTool.accept(EditorTool.elevation);
ImageButton rotate = tools.addImageButton("icon-arrow-16", 16 * 2f, () -> editor.setDrawRotation((editor.getDrawRotation() + 1) % 4)).get();
rotate.getImage().update(() -> {
rotate.getImage().setRotation(editor.getDrawRotation() * 90);
rotate.getImage().setOrigin(Align.center);
});
tools.row();
tools.table("button", t -> {
t.add("$text.editor.teams");
}).colspan(3).height(40).width(size * 3f);
tools.row();
ButtonGroup<ImageButton> teamgroup = new ButtonGroup<>();
int i = 0;
for(Team team : Team.all){
ImageButton button = new ImageButton("white", "toggle");
button.margin(4f, 4f, 10f, 4f);
button.getImageCell().grow();
button.getStyle().imageUpColor = team.color;
button.clicked(() -> editor.setDrawTeam(team));
button.update(() -> button.setChecked(editor.getDrawTeam() == team));
teamgroup.add(button);
tools.add(button).padBottom(-5.1f);
if(i++ % 3 == 2) tools.row();
}
add(tools).top().padBottom(-6);
row();
new table("button"){{
atop();
Slider slider = new Slider(0, MapEditor.brushSizes.length - 1, 1, false);
slider.moved(f -> editor.setBrushSize(MapEditor.brushSizes[(int) (float) f]));
new label("brush");
row();
add(slider).width(size * 3f - 20).padTop(4f);
}}.padTop(5).growX().growY().top().end();
row();
get().table("button", t -> {
t.add("$text.editor.elevation");
}).colspan(3).height(40).width(size * 3f);
row();
get().table("button", t -> {
t.margin(0);
t.addImageButton("icon-arrow-left", 16 * 2f, () -> editor.setDrawElevation(editor.getDrawElevation() - 1))
.disabled(b -> editor.getDrawElevation() <= -1).size(size);
t.label(() -> editor.getDrawElevation() == -1 ? "$text.editor.slope" : (editor.getDrawElevation() + ""))
.size(size).get().setAlignment(Align.center, Align.center);
t.addImageButton("icon-arrow-right", 16 * 2f, () -> editor.setDrawElevation(editor.getDrawElevation() + 1))
.disabled(b -> editor.getDrawElevation() >= 127).size(size);
}).colspan(3).height(size).padTop(-5).width(size * 3f);
}}.left().growY().end();
new table("button"){{
margin(5);
marginBottom(10);
add(view).grow();
}}.grow().end();
new table(){{
row();
addBlockSelection(get());
row();
}}.right().growY().end();
}}.grow().end();
}
private void doInput(){
//tool select
for(int i = 0; i < EditorTool.values().length; i++){
if(Inputs.keyTap(Input.valueOf("NUM_" + (i + 1)))){
view.setTool(EditorTool.values()[i]);
break;
}
}
if(Inputs.keyTap(Input.R)){
editor.setDrawRotation((editor.getDrawRotation() + 1) % 4);
}
if(Inputs.keyTap(Input.E)){
editor.setDrawRotation(Mathf.mod((editor.getDrawRotation() + 1), 4));
}
//ctrl keys (undo, redo, save)
if(UIUtils.ctrl()){
if(Inputs.keyTap(Input.Z)){
view.undo();
}
if(Inputs.keyTap(Input.Y)){
view.redo();
}
if(Inputs.keyTap(Input.S)){
save();
}
if(Inputs.keyTap(Input.G)){
view.setGrid(!view.isGrid());
}
}
}
private void tryExit(){
if(!saved){
ui.showConfirm("$text.confirm", "$text.editor.unsaved", this::hide);
}else{
hide();
}
}
private void addBlockSelection(Table table){
Table content = new Table();
pane = new ScrollPane(content, "volume");
pane.setFadeScrollBars(false);
pane.setOverscroll(true, false);
ButtonGroup<ImageButton> group = new ButtonGroup<>();
blockgroup = group;
int i = 0;
for(Block block : Block.all()){
TextureRegion[] regions = block.getCompactIcon();
if((block.synthetic() && (Recipe.getByResult(block) == null || !control.database().isUnlocked(Recipe.getByResult(block)))) && !debug && block != StorageBlocks.core)
continue;
if(regions.length == 0 || regions[0] == Draw.region("jjfgj")) continue;
Stack stack = new Stack();
for(TextureRegion region : regions){
stack.add(new Image(region));
}
ImageButton button = new ImageButton("white", "toggle");
button.clicked(() -> editor.setDrawBlock(block));
button.resizeImage(8 * 4f);
button.getImageCell().setActor(stack);
button.addChild(stack);
button.getImage().remove();
button.update(() -> button.setChecked(editor.getDrawBlock() == block));
group.add(button);
content.add(button).pad(4f).size(53f, 58f);
if(i++ % 3 == 2){
content.row();
}
}
group.getButtons().get(2).setChecked(true);
Table extra = new Table("button");
extra.labelWrap(() -> editor.getDrawBlock().formalName).width(220f).center();
table.add(extra).growX();
table.row();
table.add(pane).growY().fillX();
}
}

View File

@@ -0,0 +1,78 @@
package io.anuke.mindustry.editor;
import com.badlogic.gdx.utils.ObjectMap;
import io.anuke.mindustry.core.Platform;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import io.anuke.ucore.core.Settings;
import io.anuke.ucore.scene.ui.TextArea;
import io.anuke.ucore.scene.ui.TextField;
public class MapInfoDialog extends FloatingDialog{
private final MapEditor editor;
private TextArea description;
private TextField author;
private TextField name;
public MapInfoDialog(MapEditor editor){
super("$text.editor.mapinfo");
this.editor = editor;
addCloseButton();
shown(this::setup);
hidden(() -> {
});
}
private void setup(){
content().clear();
ObjectMap<String, String> tags = editor.getTags();
content().add("$text.editor.name").padRight(8).left();
content().defaults().padTop(15);
name = content().addField(tags.get("name", ""), text -> {
tags.put("name", text);
}).size(400, 55f).get();
name.setMessageText("$text.unknown");
content().row();
content().add("$text.editor.description").padRight(8).left();
description = content().addArea(tags.get("description", ""), "textarea", text -> {
tags.put("description", text);
}).size(400f, 140f).get();
content().row();
content().add("$text.editor.author").padRight(8).left();
author = content().addField(tags.get("author", Settings.getString("mapAuthor", "")), text -> {
tags.put("author", text);
Settings.putString("mapAuthor", text);
Settings.save();
}).size(400, 55f).get();
author.setMessageText("$text.unknown");
content().row();
content().add().padRight(8).left();
content().addCheck("$text.editor.oregen", enabled -> {
tags.put("oregen", enabled ? "1" : "0");
}).left();
name.change();
description.change();
author.change();
Platform.instance.addDialog(name, 50);
Platform.instance.addDialog(author, 50);
Platform.instance.addDialog(description, 1000);
}
}

View File

@@ -0,0 +1,81 @@
package io.anuke.mindustry.editor;
import io.anuke.mindustry.io.Map;
import io.anuke.mindustry.ui.BorderImage;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import io.anuke.ucore.core.Core;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.scene.ui.ButtonGroup;
import io.anuke.ucore.scene.ui.ScrollPane;
import io.anuke.ucore.scene.ui.ScrollPane.ScrollPaneStyle;
import io.anuke.ucore.scene.ui.TextButton;
import io.anuke.ucore.scene.ui.layout.Table;
import static io.anuke.mindustry.Vars.world;
public class MapLoadDialog extends FloatingDialog{
private Map selected = null;
public MapLoadDialog(Consumer<Map> loader){
super("$text.editor.loadmap");
shown(this::rebuild);
rebuild();
TextButton button = new TextButton("$text.load");
button.setDisabled(() -> selected == null);
button.clicked(() -> {
if(selected != null){
loader.accept(selected);
hide();
}
});
buttons().defaults().size(200f, 50f);
buttons().addButton("$text.cancel", this::hide);
buttons().add(button);
}
public void rebuild(){
content().clear();
if(world.maps().all().size > 0){
selected = world.maps().all().first();
}
ButtonGroup<TextButton> group = new ButtonGroup<>();
int maxcol = 3;
int i = 0;
Table table = new Table();
table.defaults().size(200f, 90f).pad(4f);
table.margin(10f);
ScrollPane pane = new ScrollPane(table, "horizontal");
pane.setFadeScrollBars(false);
for(Map map : world.maps().all()){
TextButton button = new TextButton(map.getDisplayName(), "toggle");
button.add(new BorderImage(map.texture, 2f)).size(16 * 4f);
button.getCells().reverse();
button.clicked(() -> selected = map);
button.getLabelCell().grow().left().padLeft(5f);
group.add(button);
table.add(button);
if(++i % maxcol == 0) table.row();
}
if(world.maps().all().size == 0){
pane.setStyle(Core.skin.get("clear", ScrollPaneStyle.class));
table.add("$text.maps.none").center();
}else{
content().add("$text.editor.loadmap");
}
content().row();
content().add(pane);
}
}

View File

@@ -0,0 +1,187 @@
package io.anuke.mindustry.editor;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.GridPoint2;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.IntSet;
import com.badlogic.gdx.utils.IntSet.IntSetIterator;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.io.MapTileData.DataPosition;
import io.anuke.mindustry.world.Block;
import io.anuke.ucore.core.Core;
import io.anuke.ucore.core.Graphics;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.IndexedRenderer;
import io.anuke.ucore.util.Bits;
import io.anuke.ucore.util.Geometry;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.tilesize;
public class MapRenderer implements Disposable{
private static final int chunksize = 64;
private IndexedRenderer[][] chunks;
private IntSet updates = new IntSet();
private IntSet delayedUpdates = new IntSet();
private MapEditor editor;
private int width, height;
private Color tmpColor = Color.WHITE.cpy();
public MapRenderer(MapEditor editor){
this.editor = editor;
}
public void resize(int width, int height){
if(chunks != null){
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
chunks[x][y].dispose();
}
}
}
chunks = new IndexedRenderer[(int) Math.ceil((float) width / chunksize)][(int) Math.ceil((float) height / chunksize)];
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
chunks[x][y] = new IndexedRenderer(chunksize * chunksize * 2);
}
}
this.width = width;
this.height = height;
updateAll();
}
public void draw(float tx, float ty, float tw, float th){
Graphics.end();
IntSetIterator it = updates.iterator();
while(it.hasNext){
int i = it.next();
int x = i % width;
int y = i / width;
render(x, y);
}
updates.clear();
updates.addAll(delayedUpdates);
delayedUpdates.clear();
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
IndexedRenderer mesh = chunks[x][y];
mesh.getTransformMatrix().setToTranslation(tx, ty, 0).scl(tw / (width * tilesize),
th / (height * tilesize), 1f);
mesh.setProjectionMatrix(Core.batch.getProjectionMatrix());
mesh.render(Core.atlas.getTextures().first());
}
}
Graphics.begin();
}
public void updatePoint(int x, int y){
//TODO spread out over multiple frames?
updates.add(x + y * width);
}
public void updateAll(){
for(int x = 0; x < width; x++){
for(int y = 0; y < height; y++){
render(x, y);
}
}
}
private void render(int wx, int wy){
int x = wx / chunksize, y = wy / chunksize;
IndexedRenderer mesh = chunks[x][y];
//TileDataMarker data = editor.getMap().readAt(wx, wy);
byte bf = editor.getMap().read(wx, wy, DataPosition.floor);
byte bw = editor.getMap().read(wx, wy, DataPosition.wall);
byte btr = editor.getMap().read(wx, wy, DataPosition.rotationTeam);
byte elev = editor.getMap().read(wx, wy, DataPosition.elevation);
byte rotation = Bits.getLeftByte(btr);
Team team = Team.all[Bits.getRightByte(btr)];
Block floor = Block.getByID(bf);
Block wall = Block.getByID(bw);
int offsetx = -(wall.size - 1) / 2;
int offsety = -(wall.size - 1) / 2;
TextureRegion region;
if(bw != 0){
region = wall.getEditorIcon();
if(wall.rotate){
mesh.draw((wx % chunksize) + (wy % chunksize) * chunksize, region,
wx * tilesize + offsetx * tilesize, wy * tilesize + offsety * tilesize,
region.getRegionWidth(), region.getRegionHeight(), rotation * 90 - 90);
}else{
mesh.draw((wx % chunksize) + (wy % chunksize) * chunksize, region,
wx * tilesize + offsetx * tilesize, wy * tilesize + offsety * tilesize,
region.getRegionWidth(), region.getRegionHeight());
}
}else{
region = floor.getEditorIcon();
mesh.draw((wx % chunksize) + (wy % chunksize) * chunksize, region, wx * tilesize, wy * tilesize, 8, 8);
}
boolean check = checkElevation(elev, wx, wy);
if(wall.update || wall.destructible){
mesh.setColor(team.color);
region = Draw.region("block-border");
}else if(elev > 0 && check){
mesh.setColor(tmpColor.fromHsv((360f * elev / 127f * 4f) % 360f, 0.5f + (elev / 4f) % 0.5f, 1f));
region = Draw.region("block-elevation");
}else if(elev == -1){
region = Draw.region("block-slope");
}else{
region = Draw.region("clear");
}
mesh.draw((wx % chunksize) + (wy % chunksize) * chunksize + chunksize * chunksize, region,
wx * tilesize + offsetx * tilesize, wy * tilesize + offsety * tilesize,
region.getRegionWidth(), region.getRegionHeight());
mesh.setColor(Color.WHITE);
}
private boolean checkElevation(byte elev, int x, int y){
for(GridPoint2 p : Geometry.d4){
int wx = x + p.x, wy = y + p.y;
if(!Mathf.inBounds(wx, wy, editor.getMap().width(), editor.getMap().height())){
return true;
}
byte value = editor.getMap().read(wx, wy, DataPosition.elevation);
if(value < elev){
return true;
}else if(value > elev){
delayedUpdates.add(wx + wy * width);
}
}
return false;
}
@Override
public void dispose(){
if(chunks == null){
return;
}
for(int x = 0; x < chunks.length; x++){
for(int y = 0; y < chunks[0].length; y++){
if(chunks[x][y] != null){
chunks[x][y].dispose();
}
}
}
}
}

View File

@@ -0,0 +1,63 @@
package io.anuke.mindustry.editor;
import io.anuke.mindustry.io.MapTileData;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import io.anuke.ucore.function.BiConsumer;
import io.anuke.ucore.scene.ui.ButtonGroup;
import io.anuke.ucore.scene.ui.TextButton;
import io.anuke.ucore.scene.ui.layout.Table;
import io.anuke.ucore.util.Mathf;
public class MapResizeDialog extends FloatingDialog{
int[] validMapSizes = {200, 300, 400, 500};
int width, height;
public MapResizeDialog(MapEditor editor, BiConsumer<Integer, Integer> cons){
super("$text.editor.resizemap");
shown(() -> {
content().clear();
MapTileData data = editor.getMap();
width = data.width();
height = data.height();
Table table = new Table();
for(boolean w : Mathf.booleans){
int curr = w ? data.width() : data.height();
int idx = 0;
for(int i = 0; i < validMapSizes.length; i++){
if(validMapSizes[i] == curr) idx = i;
}
table.add(w ? "$text.width" : "$text.height").padRight(8f);
ButtonGroup<TextButton> group = new ButtonGroup<>();
for(int i = 0; i < validMapSizes.length; i++){
int size = validMapSizes[i];
TextButton button = new TextButton(size + "", "toggle");
button.clicked(() -> {
if(w)
width = size;
else
height = size;
});
group.add(button);
if(i == idx) button.setChecked(true);
table.add(button).size(100f, 54f).pad(2f);
}
table.row();
}
content().row();
content().add(table);
});
buttons().defaults().size(200f, 50f);
buttons().addButton("$text.cancel", this::hide);
buttons().addButton("$text.editor.resize", () -> {
cons.accept(width, height);
hide();
});
}
}

View File

@@ -0,0 +1,75 @@
package io.anuke.mindustry.editor;
import io.anuke.mindustry.core.Platform;
import io.anuke.mindustry.io.Map;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.scene.ui.TextButton;
import io.anuke.ucore.scene.ui.TextField;
import static io.anuke.mindustry.Vars.ui;
import static io.anuke.mindustry.Vars.world;
public class MapSaveDialog extends FloatingDialog{
private TextField field;
private Consumer<String> listener;
public MapSaveDialog(Consumer<String> cons){
super("$text.editor.savemap");
field = new TextField();
listener = cons;
Platform.instance.addDialog(field);
shown(() -> {
content().clear();
content().label(() -> {
Map map = world.maps().getByName(field.getText());
if(map != null){
if(map.custom){
return "$text.editor.overwrite";
}else{
return "$text.editor.failoverwrite";
}
}
return "";
}).colspan(2);
content().row();
content().add("$text.editor.mapname").padRight(14f);
content().add(field).size(220f, 48f);
});
buttons().defaults().size(200f, 50f).pad(2f);
buttons().addButton("$text.cancel", this::hide);
TextButton button = new TextButton("$text.save");
button.clicked(() -> {
if(!invalid()){
cons.accept(field.getText());
hide();
}
});
button.setDisabled(this::invalid);
buttons().add(button);
}
public void save(){
if(!invalid()){
listener.accept(field.getText());
}else{
ui.showError("$text.editor.failoverwrite");
}
}
public void setFieldText(String text){
field.setText(text);
}
private boolean invalid(){
if(field.getText().isEmpty()){
return true;
}
Map map = world.maps().getByName(field.getText());
return map != null && !map.custom;
}
}

View File

@@ -0,0 +1,398 @@
package io.anuke.mindustry.editor;
import com.badlogic.gdx.Input.Buttons;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.input.GestureDetector;
import com.badlogic.gdx.input.GestureDetector.GestureListener;
import com.badlogic.gdx.math.Bresenham2;
import com.badlogic.gdx.math.GridPoint2;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.utils.ScissorStack;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.editor.DrawOperation.TileOperation;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.ui.GridImage;
import io.anuke.ucore.core.Core;
import io.anuke.ucore.core.Graphics;
import io.anuke.ucore.core.Inputs;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.scene.Element;
import io.anuke.ucore.scene.event.InputEvent;
import io.anuke.ucore.scene.event.InputListener;
import io.anuke.ucore.scene.event.Touchable;
import io.anuke.ucore.scene.ui.TextField;
import io.anuke.ucore.scene.ui.layout.Unit;
import io.anuke.ucore.util.Geometry;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Tmp;
import static io.anuke.mindustry.Vars.mobile;
import static io.anuke.mindustry.Vars.ui;
public class MapView extends Element implements GestureListener{
private MapEditor editor;
private EditorTool tool = EditorTool.pencil;
private OperationStack stack = new OperationStack();
private DrawOperation op;
private Bresenham2 br = new Bresenham2();
private boolean updated = false;
private float offsetx, offsety;
private float zoom = 1f;
private boolean grid = false;
private GridImage image = new GridImage(0, 0);
private Vector2 vec = new Vector2();
private Rectangle rect = new Rectangle();
private Vector2[][] brushPolygons = new Vector2[MapEditor.brushSizes.length][0];
private boolean drawing;
private int lastx, lasty;
private int startx, starty;
private float mousex, mousey;
private EditorTool lastTool;
public MapView(MapEditor editor){
this.editor = editor;
for(int i = 0; i < MapEditor.brushSizes.length; i++){
float size = MapEditor.brushSizes[i];
brushPolygons[i] = Geometry.pixelCircle(size, (index, x, y) -> Vector2.dst(x, y, index, index) <= index - 0.5f);
}
Inputs.addProcessor(0, new GestureDetector(20, 0.5f, 2, 0.15f, this));
setTouchable(Touchable.enabled);
addListener(new InputListener(){
@Override
public boolean mouseMoved(InputEvent event, float x, float y){
mousex = x;
mousey = y;
return false;
}
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, int button){
if(pointer != 0){
return false;
}
if(!mobile && button != Buttons.LEFT && button != Buttons.MIDDLE){
return true;
}
if(button == Buttons.MIDDLE){
lastTool = tool;
tool = EditorTool.zoom;
}
mousex = x;
mousey = y;
op = new DrawOperation(editor.getMap());
updated = false;
GridPoint2 p = project(x, y);
lastx = p.x;
lasty = p.y;
startx = p.x;
starty = p.y;
tool.touched(editor, p.x, p.y);
if(tool.edit){
updated = true;
ui.editor.resetSaved();
}
drawing = true;
return true;
}
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, int button){
if(!mobile && button != Buttons.LEFT && button != Buttons.MIDDLE){
return;
}
drawing = false;
GridPoint2 p = project(x, y);
if(tool == EditorTool.line){
ui.editor.resetSaved();
Array<GridPoint2> points = br.line(startx, starty, p.x, p.y);
for(GridPoint2 point : points){
editor.draw(point.x, point.y);
}
updated = true;
}
if(op != null && updated){
if(!op.isEmpty()){
stack.add(op);
}
op = null;
}
if(lastTool != null){
tool = lastTool;
lastTool = null;
}
}
@Override
public void touchDragged(InputEvent event, float x, float y, int pointer){
mousex = x;
mousey = y;
GridPoint2 p = project(x, y);
if(drawing && tool.draggable){
ui.editor.resetSaved();
Array<GridPoint2> points = br.line(lastx, lasty, p.x, p.y);
for(GridPoint2 point : points){
tool.touched(editor, point.x, point.y);
}
updated = true;
}
lastx = p.x;
lasty = p.y;
}
});
}
public EditorTool getTool(){
return tool;
}
public void setTool(EditorTool tool){
this.tool = tool;
}
public void clearStack(){
stack.clear();
//TODO clear und obuffer
}
public OperationStack getStack(){
return stack;
}
public boolean isGrid(){
return grid;
}
public void setGrid(boolean grid){
this.grid = grid;
}
public void undo(){
if(stack.canUndo()){
stack.undo(editor);
}
}
public void redo(){
if(stack.canRedo()){
stack.redo(editor);
}
}
public void addTileOp(TileOperation t){
op.addOperation(t);
}
public boolean checkForDuplicates(short x, short y){
return op.checkDuplicate(x, y);
}
@Override
public void act(float delta){
super.act(delta);
if(Core.scene.getKeyboardFocus() == null || !(Core.scene.getKeyboardFocus() instanceof TextField) &&
!Inputs.keyDown(io.anuke.ucore.input.Input.CONTROL_LEFT)){
float ax = Inputs.getAxis("move_x");
float ay = Inputs.getAxis("move_y");
offsetx -= ax * 15f / zoom;
offsety -= ay * 15f / zoom;
}
if(ui.editor.hasPane()) return;
zoom += Inputs.scroll() / 10f * zoom;
clampZoom();
}
private void clampZoom(){
zoom = Mathf.clamp(zoom, 0.2f, 12f);
}
private GridPoint2 project(float x, float y){
float ratio = 1f / ((float) editor.getMap().width() / editor.getMap().height());
float size = Math.min(width, height);
float sclwidth = size * zoom;
float sclheight = size * zoom * ratio;
x = (x - getWidth() / 2 + sclwidth / 2 - offsetx * zoom) / sclwidth * editor.getMap().width();
y = (y - getHeight() / 2 + sclheight / 2 - offsety * zoom) / sclheight * editor.getMap().height();
if(editor.getDrawBlock().size % 2 == 0 && tool != EditorTool.eraser){
return Tmp.g1.set((int) (x - 0.5f), (int) (y - 0.5f));
}else{
return Tmp.g1.set((int) x, (int) y);
}
}
private Vector2 unproject(int x, int y){
float ratio = 1f / ((float) editor.getMap().width() / editor.getMap().height());
float size = Math.min(width, height);
float sclwidth = size * zoom;
float sclheight = size * zoom * ratio;
float px = ((float) x / editor.getMap().width()) * sclwidth + offsetx * zoom - sclwidth / 2 + getWidth() / 2;
float py = ((float) (y) / editor.getMap().height()) * sclheight
+ offsety * zoom - sclheight / 2 + getHeight() / 2;
return vec.set(px, py);
}
@Override
public void draw(Batch batch, float alpha){
float ratio = 1f / ((float) editor.getMap().width() / editor.getMap().height());
float size = Math.min(width, height);
float sclwidth = size * zoom;
float sclheight = size * zoom * ratio;
float centerx = x + width / 2 + offsetx * zoom;
float centery = y + height / 2 + offsety * zoom;
image.setImageSize(editor.getMap().width(), editor.getMap().height());
batch.flush();
boolean pop = ScissorStack.pushScissors(rect.set(x, y, width, height));
Draw.color(Color.LIGHT_GRAY);
Lines.stroke(-2f);
Lines.rect(centerx - sclwidth / 2 - 1, centery - sclheight / 2 - 1, sclwidth + 2, sclheight + 2);
editor.renderer().draw(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
Draw.reset();
if(grid){
Draw.color(Color.GRAY);
image.setBounds(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
image.draw(batch, alpha);
Draw.color();
}
int index = 0;
for(int i = 0; i < MapEditor.brushSizes.length; i++){
if(editor.getBrushSize() == MapEditor.brushSizes[i]){
index = i;
break;
}
}
//todo is it really math.max?
float scaling = zoom * Math.min(width, height) / Math.max(editor.getMap().width(), editor.getMap().height());
Draw.color(Palette.accent);
Lines.stroke(Unit.dp.scl(1f * zoom));
if(!editor.getDrawBlock().isMultiblock() || tool == EditorTool.eraser){
if(tool == EditorTool.line && drawing){
Vector2 v1 = unproject(startx, starty).add(x, y);
float sx = v1.x, sy = v1.y;
Vector2 v2 = unproject(lastx, lasty).add(x, y);
Lines.poly(brushPolygons[index], sx, sy, scaling);
Lines.poly(brushPolygons[index], v2.x, v2.y, scaling);
}
if(tool.edit && (!mobile || drawing)){
GridPoint2 p = project(mousex, mousey);
Vector2 v = unproject(p.x, p.y).add(x, y);
Lines.poly(brushPolygons[index], v.x, v.y, scaling);
}
}else{
if((tool.edit || tool == EditorTool.line) && (!mobile || drawing)){
GridPoint2 p = project(mousex, mousey);
Vector2 v = unproject(p.x, p.y).add(x, y);
float offset = (editor.getDrawBlock().size % 2 == 0 ? scaling / 2f : 0f);
Lines.square(
v.x + scaling / 2f + offset,
v.y + scaling / 2f + offset,
scaling * editor.getDrawBlock().size / 2f);
}
}
batch.flush();
if(pop) ScissorStack.popScissors();
Draw.color(Palette.accent);
Lines.stroke(Unit.dp.scl(3f));
Lines.rect(x, y, width, height);
Draw.reset();
}
private boolean active(){
return Core.scene.getKeyboardFocus() != null
&& Core.scene.getKeyboardFocus().isDescendantOf(ui.editor)
&& ui.editor.isShown() && tool == EditorTool.zoom &&
Core.scene.hit(Graphics.mouse().x, Graphics.mouse().y, true) == this;
}
@Override
public boolean touchDown(float x, float y, int pointer, int button){
return false;
}
@Override
public boolean tap(float x, float y, int count, int button){
return false;
}
@Override
public boolean longPress(float x, float y){
return false;
}
@Override
public boolean fling(float velocityX, float velocityY, int button){
return false;
}
@Override
public boolean pan(float x, float y, float deltaX, float deltaY){
if(!active()) return false;
offsetx += deltaX / zoom;
offsety -= deltaY / zoom;
return false;
}
@Override
public boolean panStop(float x, float y, int pointer, int button){
return false;
}
@Override
public boolean zoom(float initialDistance, float distance){
if(!active()) return false;
float nzoom = distance - initialDistance;
zoom += nzoom / 10000f / Unit.dp.scl(1f) * zoom;
clampZoom();
return false;
}
@Override
public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2){
return false;
}
@Override
public void pinchStop(){
}
}

View File

@@ -0,0 +1,51 @@
package io.anuke.mindustry.editor;
import com.badlogic.gdx.utils.Array;
public class OperationStack{
private final static int maxSize = 10;
private Array<DrawOperation> stack = new Array<>();
private int index = 0;
public OperationStack(){
}
public void clear(){
stack.clear();
index = 0;
}
public void add(DrawOperation action){
stack.truncate(stack.size + index);
index = 0;
stack.add(action);
if(stack.size > maxSize){
stack.removeIndex(0);
}
}
public boolean canUndo(){
return !(stack.size - 1 + index < 0);
}
public boolean canRedo(){
return !(index > -1 || stack.size + index < 0);
}
public void undo(MapEditor editor){
if(!canUndo()) return;
stack.get(stack.size - 1 + index).undo(editor);
index--;
}
public void redo(MapEditor editor){
if(!canRedo()) return;
index++;
stack.get(stack.size - 1 + index).redo(editor);
}
}

View File

@@ -1,76 +0,0 @@
package io.anuke.mindustry.entities;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.entities.BulletEntity;
import io.anuke.ucore.entities.Entity;
import io.anuke.ucore.util.Timer;
import static io.anuke.mindustry.Vars.*;
public class Bullet extends BulletEntity{
public Timer timer = new Timer(3);
public Bullet(BulletType type, Entity owner, float x, float y, float angle){
super(type, owner, angle);
set(x, y);
this.type = type;
}
public void draw(){
//interpolate position linearly at low tick speeds
if(SyncEntity.isSmoothing()){
x += threads.getFramesSinceUpdate() * velocity.x;
y += threads.getFramesSinceUpdate() * velocity.y;
type.draw(this);
x -= threads.getFramesSinceUpdate() * velocity.x;
y -= threads.getFramesSinceUpdate() * velocity.y;
}else{
type.draw(this);
}
}
public float drawSize(){
return 8;
}
public boolean collidesTiles(){
return owner instanceof Enemy;
}
@Override
public void update(){
super.update();
if (collidesTiles()) {
world.raycastEach(world.toTile(lastX), world.toTile(lastY), world.toTile(x), world.toTile(y), (x, y) -> {
Tile tile = world.tile(x, y);
if (tile == null) return false;
tile = tile.target();
if (tile.entity != null && tile.entity.collide(this) && !tile.entity.dead) {
tile.entity.collision(this);
remove();
type.hit(this);
return true;
}
return false;
});
}
}
@Override
public int getDamage(){
return damage == -1 ? type.damage : damage;
}
@Override
public Bullet add(){
return super.add(bulletGroup);
}
}

View File

@@ -1,485 +0,0 @@
package io.anuke.mindustry.entities;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.entities.effect.DamageArea;
import io.anuke.mindustry.entities.effect.EMP;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.mindustry.graphics.Fx;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.BaseBulletType;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.graphics.Fx.*;
public abstract class BulletType extends BaseBulletType<Bullet>{
public static final BulletType
none = new BulletType(0f, 0){
public void draw(Bullet b){}
},
stone = new BulletType(1.5f, 2){
public void draw(Bullet b){
Draw.colorl(0.64f);
Draw.rect("blank", b.x, b.y, 2f, 2f);
Draw.reset();
}
},
iron = new BulletType(1.7f, 2){
public void draw(Bullet b){
Draw.color(Color.GRAY);
Draw.rect("bullet", b.x, b.y, b.angle());
Draw.reset();
}
},
chain = new BulletType(2f, 8){
public void draw(Bullet b){
Draw.color(whiteOrange);
Draw.rect("chainbullet", b.x, b.y, b.angle());
Draw.reset();
}
},
sniper = new BulletType(3f, 25){
public void draw(Bullet b){
Draw.color(Color.LIGHT_GRAY);
Lines.stroke(1f);
Lines.lineAngleCenter(b.x, b.y, b.angle(), 3f);
Draw.reset();
}
public void update(Bullet b){
if(b.timer.get(0, 4)){
Effects.effect(Fx.railsmoke, b.x, b.y);
}
}
},
emp = new BulletType(1.6f, 8){
{
lifetime = 50f;
hitsize = 6f;
}
public void draw(Bullet b){
float rad = 6f + Mathf.sin(Timers.time(), 5f, 2f);
Draw.color(Color.SKY);
Lines.circle(b.x, b.y, 4f);
Draw.rect("circle", b.x, b.y, rad, rad);
Draw.reset();
}
public void update(Bullet b){
if(b.timer.get(0, 2)){
Effects.effect(Fx.empspark, b.x + Mathf.range(2), b.y + Mathf.range(2));
}
}
public void despawned(Bullet b){
hit(b);
}
public void hit(Bullet b, float hitx, float hity){
Timers.run(5f, ()-> new EMP(b.x, b.y, b.getDamage()).add());
Effects.effect(Fx.empshockwave, b);
Effects.shake(3f, 3f, b);
}
},
//TODO better visuals for shell
shell = new BulletType(1.1f, 60){
{
lifetime = 110f;
hitsize = 11f;
}
public void draw(Bullet b){
float rad = 8f;
Draw.color(Color.ORANGE);
Draw.color(Color.GRAY);
Draw.rect("circle", b.x, b.y, rad, rad);
rad += Mathf.sin(Timers.time(), 3f, 1f);
Draw.color(Color.ORANGE);
Draw.rect("circle", b.x, b.y, rad/1.7f, rad/1.7f);
Draw.reset();
}
public void update(Bullet b){
if(b.timer.get(0, 7)){
Effects.effect(Fx.smoke, b.x + Mathf.range(2), b.y + Mathf.range(2));
}
}
public void despawned(Bullet b){
hit(b);
}
public void hit(Bullet b, float hitx, float hity){
Effects.shake(3f, 3f, b);
Effects.effect(Fx.shellsmoke, b);
Effects.effect(Fx.shellexplosion, b);
DamageArea.damage(!(b.owner instanceof Enemy), b.x, b.y, 25f, (int)(damage * 2f/3f));
}
},
flak = new BulletType(2.9f, 8) {
public void init(Bullet b) {
b.velocity.scl(Mathf.random(0.6f, 1f));
}
public void update(Bullet b){
if(b.timer.get(0, 7)){
Effects.effect(Fx.smoke, b.x + Mathf.range(2), b.y + Mathf.range(2));
}
}
public void draw(Bullet b) {
Draw.color(Color.GRAY);
Lines.stroke(3f);
Lines.lineAngleCenter(b.x, b.y, b.angle(), 2f);
Lines.stroke(1.5f);
Lines.lineAngleCenter(b.x, b.y, b.angle(), 5f);
Draw.reset();
}
public void hit(Bullet b, float hitx, float hity) {
Effects.effect(shellsmoke, b);
for(int i = 0; i < 3; i ++){
Bullet bullet = new Bullet(flakspark, b.owner, hitx, hity, b.angle() + Mathf.range(120f));
bullet.add();
}
}
public void despawned(Bullet b) {
hit(b, b.x, b.y);
}
},
flakspark = new BulletType(2f, 2) {
{
drag = 0.05f;
}
public void init(Bullet b) {
b.velocity.scl(Mathf.random(0.6f, 1f));
}
public void draw(Bullet b) {
Draw.color(Color.LIGHT_GRAY, Color.GRAY, b.fin());
Lines.stroke(2f - b.fin());
Lines.lineAngleCenter(b.x, b.y, b.angle(), 2f);
Draw.reset();
}
},
titanshell = new BulletType(1.8f, 38){
{
lifetime = 70f;
hitsize = 15f;
}
public void draw(Bullet b){
Draw.color(whiteOrange);
Draw.rect("titanshell", b.x, b.y, b.angle());
Draw.reset();
}
public void update(Bullet b){
if(b.timer.get(0, 4)){
Effects.effect(Fx.smoke, b.x + Mathf.range(2), b.y + Mathf.range(2));
}
}
public void despawned(Bullet b){
hit(b);
}
public void hit(Bullet b, float hitx, float hity){
Effects.shake(3f, 3f, b);
Effects.effect(Fx.shellsmoke, b);
Effects.effect(Fx.shockwaveSmall, b);
DamageArea.damage(!(b.owner instanceof Enemy), b.x, b.y, 50f, (int)(damage * 2f/3f));
}
},
yellowshell = new BulletType(1.2f, 20){
{
lifetime = 60f;
hitsize = 11f;
}
public void draw(Bullet b){
Draw.color(whiteYellow);
Draw.rect("titanshell", b.x, b.y, b.angle());
Draw.reset();
}
public void update(Bullet b){
if(b.timer.get(0, 4)){
Effects.effect(Fx.smoke, b.x + Mathf.range(2), b.y + Mathf.range(2));
}
}
public void despawned(Bullet b){
hit(b);
}
public void hit(Bullet b, float hitx, float hity){
Effects.shake(3f, 3f, b);
Effects.effect(Fx.shellsmoke, b);
Effects.effect(Fx.shockwaveSmall, b);
DamageArea.damage(!(b.owner instanceof Enemy), b.x, b.y, 25f, (int)(damage * 2f/3f));
}
},
blast = new BulletType(1.1f, 90){
{
lifetime = 0f;
hitsize = 8f;
speed = 0f;
}
public void despawned(Bullet b){
hit(b);
}
public void hit(Bullet b, float hitx, float hity){
Effects.shake(3f, 3f, b);
Effects.effect(Fx.blastsmoke, b);
Effects.effect(Fx.blastexplosion, b);
//TODO remove translation() usage
Angles.circleVectors(30, 6f, (nx, ny) -> {
float ang = Mathf.atan2(nx, ny);
Bullet o = new Bullet(blastshot, b.owner, b.x + nx, b.y + ny, ang).add();
o.damage = b.damage/9;
});
}
public void draw(Bullet b){}
},
blastshot = new BulletType(1.6f, 6){
{
lifetime = 7f;
}
public void draw(Bullet b){}
},
small = new BulletType(1.5f, 2){
public void draw(Bullet b){
Draw.color(glowy);
Draw.rect("shot", b.x, b.y, b.angle() - 45);
Draw.reset();
}
},
smallSlow = new BulletType(1.2f, 2){
public void draw(Bullet b){
Draw.color(Color.ORANGE);
Draw.rect("shot", b.x, b.y, b.angle() - 45);
Draw.reset();
}
},
purple = new BulletType(1.6f, 2){
Color color = new Color(0x8b5ec9ff);
public void draw(Bullet b){
Draw.color(color);
Draw.rect("bullet", b.x, b.y, b.angle());
Draw.reset();
}
},
flame = new BulletType(0.7f, 5){ //for turrets
public void draw(Bullet b){
Draw.color(Color.YELLOW, Color.SCARLET, b.time/lifetime);
float size = 6f-b.time/lifetime*5f;
Draw.rect("circle", b.x, b.y, size, size);
Draw.reset();
}
},
plasmaflame = new BulletType(0.8f, 17){
{
lifetime = 65f;
}
public void draw(Bullet b){
Draw.color(Color.valueOf("efa66c"), Color.valueOf("72deaf"), b.time/lifetime);
float size = 7f-b.time/lifetime*6f;
Draw.rect("circle", b.x, b.y, size, size);
Draw.reset();
}
},
flameshot = new BulletType(0.5f, 3){ //for enemies
public void draw(Bullet b){
Draw.color(Color.ORANGE, Color.SCARLET, b.time/lifetime);
float size = 6f-b.time/lifetime*5f;
Draw.rect("circle", b.x, b.y, size, size);
Draw.reset();
}
},
shot = new BulletType(2.7f, 5){
{
lifetime = 40;
}
public void draw(Bullet b){
Draw.color(Color.WHITE, lightOrange, b.fout()/2f + 0.25f);
Lines.stroke(1.5f);
Lines.lineAngle(b.x, b.y, b.angle(), 3f);
Draw.reset();
}
},
spread = new BulletType(2.4f, 9) {
{
lifetime = 70;
}
public void draw(Bullet b) {
float size = 3f - b.fin()*1f;
Draw.color(Color.PURPLE, Color.WHITE, 0.8f);
Lines.stroke(1f);
Lines.circle(b.x, b.y, size);
Draw.reset();
}
},
cluster = new BulletType(4.5f, 12){
{
lifetime = 60;
drag = 0.05f;
}
public void draw(Bullet b){
Lines.stroke(2f);
Draw.color(lightOrange, Color.WHITE, 0.4f);
Lines.poly(b.x, b.y, 3, 1.6f, b.angle());
Lines.stroke(1f);
Draw.color(Color.WHITE, lightOrange, b.fin()/2f);
Draw.alpha(b.fin());
Lines.spikes(b.x, b.y, 1.5f, 2f, 6);
Draw.reset();
}
public void despawned(Bullet b){
hit(b);
}
public void hit(Bullet b, float hitx, float hity){
Effects.shake(1.5f, 1.5f, b);
Effects.effect(Fx.clusterbomb, b);
DamageArea.damage(!(b.owner instanceof Enemy), b.x, b.y, 35f, damage);
}
},
vulcan = new BulletType(4.5f, 12) {
{
lifetime = 50;
}
public void init(Bullet b) {
Timers.reset(b, "smoke", Mathf.random(4f));
}
public void draw(Bullet b){
Draw.color(lightGray);
Lines.stroke(1f);
Lines.lineAngleCenter(b.x, b.y, b.angle(), 2f);
Draw.reset();
}
public void update(Bullet b){
if(b.timer.get(0, 4)){
Effects.effect(Fx.chainsmoke, b.x, b.y);
}
}
},
shockshell = new BulletType(5.5f, 11) {
{
drag = 0.03f;
lifetime = 30f;
}
public void init(Bullet b) {
b.velocity.scl(Mathf.random(0.5f, 1f));
}
public void draw(Bullet b) {
Draw.color(Color.WHITE, Color.ORANGE, b.fin());
Lines.stroke(2f);
Lines.lineAngleCenter(b.x, b.y, b.angle(), b.fout()*5f);
Draw.reset();
}
public void despawned(Bullet b) {
hit(b);
}
public void hit(Bullet b, float hitx, float hity) {
for(int i = 0; i < 4; i ++){
Bullet bullet = new Bullet(scrap, b.owner, b.x, b.y, b.angle() + Mathf.range(80f));
bullet.add();
}
}
},
scrap = new BulletType(2f, 3) {
{
drag = 0.06f;
lifetime = 30f;
}
public void init(Bullet b) {
b.velocity.scl(Mathf.random(0.5f, 1f));
}
public void draw(Bullet b) {
Draw.color(Color.WHITE, Color.ORANGE, b.fin());
Lines.stroke(1f);
Lines.lineAngleCenter(b.x, b.y, b.angle(), b.fout()*4f);
Draw.reset();
}
},
beamlaser = new BulletType(0.001f, 38) {
float length = 230f;
{
drawSize = length*2f+20f;
lifetime = 15f;
}
public void init(Bullet b) {
DamageArea.damageLine(b.owner, Fx.beamhit, b.x, b.y, b.angle(), length, damage);
}
public void draw(Bullet b) {
float f = b.fout()*1.5f;
Draw.color(beam);
Draw.rect("circle", b.x, b.y, 6f*f, 6f*f);
Lines.stroke(3f * f);
Lines.lineAngle(b.x, b.y, b.angle(), length);
Lines.stroke(2f * f);
Lines.lineAngle(b.x, b.y, b.angle(), length + 6f);
Lines.stroke(1f * f);
Lines.lineAngle(b.x, b.y, b.angle(), length + 12f);
Draw.color(beamLight);
Lines.stroke(1.5f * f);
Draw.rect("circle", b.x, b.y, 3f*f, 3f*f);
Lines.lineAngle(b.x, b.y, b.angle(), length);
}
};
private BulletType(float speed, int damage){
this.speed = speed;
this.damage = damage;
}
@Override
public void hit(Bullet b, float hitx, float hity){
Effects.effect(Fx.hit, hitx, hity);
}
}

View File

@@ -0,0 +1,200 @@
package io.anuke.mindustry.entities;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import io.anuke.mindustry.content.bullets.TurretBullets;
import io.anuke.mindustry.content.fx.ExplosionFx;
import io.anuke.mindustry.content.fx.Fx;
import io.anuke.mindustry.entities.effect.Fire;
import io.anuke.mindustry.entities.effect.Lightning;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.gen.CallEntity;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.impl.SolidEntity;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.function.Predicate;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Physics;
import io.anuke.ucore.util.Translator;
import static io.anuke.mindustry.Vars.*;
/**
* Utility class for damaging in an area.
*/
public class Damage{
private static Rectangle rect = new Rectangle();
private static Rectangle hitrect = new Rectangle();
private static Translator tr = new Translator();
/**
* Creates a dynamic explosion based on specified parameters.
*/
public static void dynamicExplosion(float x, float y, float flammability, float explosiveness, float power, float radius, Color color){
for(int i = 0; i < Mathf.clamp(power / 20, 0, 6); i++){
int branches = 5 + Mathf.clamp((int) (power / 30), 1, 20);
Timers.run(i * 2f + Mathf.random(4f), () -> Lightning.create(Team.none, Fx.none, Palette.power, 3,
x, y, Mathf.random(360f), branches + Mathf.range(2)));
}
for(int i = 0; i < Mathf.clamp(flammability / 4, 0, 30); i++){
Timers.run(i / 2, () -> CallEntity.createBullet(TurretBullets.fireball, x, y, Mathf.random(360f)));
}
int waves = Mathf.clamp((int) (explosiveness / 4), 0, 30);
for(int i = 0; i < waves; i++){
int f = i;
Timers.run(i * 2f, () -> {
Damage.damage(x, y, Mathf.clamp(radius + explosiveness, 0, 50f) * ((f + 1f) / waves), explosiveness / 2f);
Effects.effect(ExplosionFx.blockExplosionSmoke, x + Mathf.range(radius), y + Mathf.range(radius));
});
}
if(explosiveness > 15f){
Effects.effect(ExplosionFx.shockwave, x, y);
}
if(explosiveness > 30f){
Effects.effect(ExplosionFx.bigShockwave, x, y);
}
float shake = Math.min(explosiveness / 4f + 3f, 9f);
Effects.shake(shake, shake, x, y);
Effects.effect(ExplosionFx.blockExplosion, x, y);
}
public static void createIncend(float x, float y, float range, int amount){
for(int i = 0; i < amount; i++){
float cx = x + Mathf.range(range);
float cy = y + Mathf.range(range);
Tile tile = world.tileWorld(cx, cy);
if(tile != null){
Fire.create(tile);
}
}
}
/**
* Damages entities in a line.
* Only enemies of the specified team are damaged.
*/
public static void collideLine(SolidEntity hitter, Team team, Effect effect, float x, float y, float angle, float length){
tr.trns(angle, length);
rect.setPosition(x, y).setSize(tr.x, tr.y);
float x2 = tr.x + x, y2 = tr.y + y;
if(rect.width < 0){
rect.x += rect.width;
rect.width *= -1;
}
if(rect.height < 0){
rect.y += rect.height;
rect.height *= -1;
}
float expand = 3f;
rect.y -= expand;
rect.x -= expand;
rect.width += expand * 2;
rect.height += expand * 2;
Consumer<Unit> cons = e -> {
e.getHitbox(hitrect);
Rectangle other = hitrect;
other.y -= expand;
other.x -= expand;
other.width += expand * 2;
other.height += expand * 2;
Vector2 vec = Physics.raycastRect(x, y, x2, y2, other);
if(vec != null){
Effects.effect(effect, vec.x, vec.y);
e.collision(hitter, vec.x, vec.y);
hitter.collision(e, vec.x, vec.y);
}
};
Units.getNearbyEnemies(team, rect, cons);
}
/**
* Damages all entities and blocks in a radius that are enemies of the team.
*/
public static void damageUnits(Team team, float x, float y, float size, float damage, Predicate<Unit> predicate, Consumer<Unit> acceptor){
Consumer<Unit> cons = entity -> {
if(!predicate.test(entity)) return;
entity.getHitbox(hitrect);
if(!hitrect.overlaps(rect)){
return;
}
entity.damage(damage);
acceptor.accept(entity);
};
rect.setSize(size * 2).setCenter(x, y);
if(team != null){
Units.getNearbyEnemies(team, rect, cons);
}else{
Units.getNearby(rect, cons);
}
}
/**
* Damages everything in a radius.
*/
public static void damage(float x, float y, float radius, float damage){
damage(null, x, y, radius, damage);
}
/**
* Damages all entities and blocks in a radius that are enemies of the team.
*/
public static void damage(Team team, float x, float y, float radius, float damage){
Consumer<Unit> cons = entity -> {
if(entity.team == team || entity.distanceTo(x, y) > radius){
return;
}
float amount = calculateDamage(x, y, entity.x, entity.y, radius, damage);
entity.damage(amount);
//TODO better velocity displacement
float dst = tr.set(entity.x - x, entity.y - y).len();
entity.getVelocity().add(tr.setLength((1f - dst / radius) * 2f));
};
rect.setSize(radius * 2).setCenter(x, y);
if(team != null){
Units.getNearbyEnemies(team, rect, cons);
}else{
Units.getNearby(rect, cons);
}
int trad = (int) (radius / tilesize);
for(int dx = -trad; dx <= trad; dx++){
for(int dy = -trad; dy <= trad; dy++){
Tile tile = world.tile(Mathf.scl2(x, tilesize) + dx, Mathf.scl2(y, tilesize) + dy);
if(tile != null && tile.entity != null && (team == null || state.teams.areEnemies(team, tile.getTeam())) && Vector2.dst(dx, dy, 0, 0) <= trad){
float amount = calculateDamage(x, y, tile.worldx(), tile.worldy(), radius, damage);
tile.entity.damage(amount);
}
}
}
}
private static float calculateDamage(float x, float y, float tx, float ty, float radius, float damage){
float dist = Vector2.dst(x, y, tx, ty);
float falloff = 0.4f;
float scaled = Mathf.lerp(1f - dist / radius, 1f, falloff);
return damage * scaled;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
package io.anuke.mindustry.entities;
import com.badlogic.gdx.math.Vector2;
import io.anuke.mindustry.entities.traits.TargetTrait;
import io.anuke.ucore.util.Mathf;
/**
* Class for predicting shoot angles based on velocities of targets.
*/
public class Predict{
private static Vector2 vec = new Vector2();
private static Vector2 vresult = new Vector2();
/**
* Calculates of intercept of a stationary and moving target. Do not call from multiple threads!
*
* @param srcx X of shooter
* @param srcy Y of shooter
* @param dstx X of target
* @param dsty Y of target
* @param dstvx X velocity of target (subtract shooter X velocity if needed)
* @param dstvy Y velocity of target (subtract shooter Y velocity if needed)
* @param v speed of bullet
* @return the intercept location
*/
public static Vector2 intercept(float srcx, float srcy, float dstx, float dsty, float dstvx, float dstvy, float v){
float tx = dstx - srcx,
ty = dsty - srcy;
// Get quadratic equation components
float a = dstvx * dstvx + dstvy * dstvy - v * v;
float b = 2 * (dstvx * tx + dstvy * ty);
float c = tx * tx + ty * ty;
// Solve quadratic
Vector2 ts = quad(a, b, c);
// Find smallest positive solution
Vector2 sol = vresult.set(0, 0);
if(ts != null){
float t0 = ts.x, t1 = ts.y;
float t = Math.min(t0, t1);
if(t < 0) t = Math.max(t0, t1);
if(t > 0){
sol.set(dstx + dstvx * t, dsty + dstvy * t);
}
}
return sol;
}
/**
* See {@link #intercept(float, float, float, float, float, float, float)}.
*/
public static Vector2 intercept(TargetTrait src, TargetTrait dst, float v){
return intercept(src.getX(), src.getY(), dst.getX(), dst.getY(), dst.getVelocity().x - src.getVelocity().x, dst.getVelocity().x - src.getVelocity().y, v);
}
private static Vector2 quad(float a, float b, float c){
Vector2 sol = null;
if(Math.abs(a) < 1e-6){
if(Math.abs(b) < 1e-6){
sol = Math.abs(c) < 1e-6 ? vec.set(0, 0) : null;
}else{
vec.set(-c / b, -c / b);
}
}else{
float disc = b * b - 4 * a * c;
if(disc >= 0){
disc = Mathf.sqrt(disc);
a = 2 * a;
sol = vec.set((-b - disc) / a, (-b + disc) / a);
}
}
return sol;
}
}

View File

@@ -0,0 +1,147 @@
package io.anuke.mindustry.entities;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.content.StatusEffects;
import io.anuke.mindustry.entities.traits.Saveable;
import io.anuke.mindustry.type.StatusEffect;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.util.Pooling;
import io.anuke.ucore.util.ThreadArray;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* Class for controlling status effects on an entity.
*/
public class StatusController implements Saveable{
private static final StatusEntry globalResult = new StatusEntry();
private static final Array<StatusEntry> removals = new ThreadArray<>();
private Array<StatusEntry> statuses = new ThreadArray<>();
private float speedMultiplier;
private float damageMultiplier;
private float armorMultiplier;
public void handleApply(Unit unit, StatusEffect effect, float intensity){
if(effect == StatusEffects.none) return; //don't apply empty effects
float newTime = effect.baseDuration * intensity;
if(statuses.size > 0){
//check for opposite effects
for(StatusEntry entry : statuses){
//extend effect
if(entry.effect == effect){
entry.time = Math.max(entry.time, newTime);
return;
}else if(entry.effect.isOpposite(effect)){ //find opposite
entry.effect.getTransition(unit, effect, entry.time, newTime, globalResult);
entry.time = globalResult.time;
if(globalResult.effect != entry.effect){
entry.effect.onTransition(unit, globalResult.effect);
entry.effect = globalResult.effect;
}
//stop looking when one is found
return;
}
}
}
//otherwise, no opposites found, add direct effect
StatusEntry entry = Pooling.obtain(StatusEntry.class);
entry.set(effect, newTime);
statuses.add(entry);
}
public void clear(){
statuses.clear();
}
public void update(Unit unit){
speedMultiplier = damageMultiplier = armorMultiplier = 1f;
if(statuses.size == 0) return;
removals.clear();
for(StatusEntry entry : statuses){
entry.time = Math.max(entry.time - Timers.delta(), 0);
if(entry.time <= 0){
Pooling.free(entry);
removals.add(entry);
}else{
speedMultiplier *= entry.effect.speedMultiplier;
armorMultiplier *= entry.effect.armorMultiplier;
damageMultiplier *= entry.effect.damageMultiplier;
entry.effect.update(unit, entry.time);
}
}
if(removals.size > 0){
statuses.removeAll(removals, true);
}
}
public float getSpeedMultiplier(){
return speedMultiplier;
}
public float getDamageMultiplier(){
return damageMultiplier;
}
public float getArmorMultiplier(){
return armorMultiplier;
}
public boolean hasEffect(StatusEffect effect){
for(StatusEntry entry : statuses){
if(entry.effect == effect) return true;
}
return false;
}
@Override
public void writeSave(DataOutput stream) throws IOException{
stream.writeByte(statuses.size);
for(StatusEntry entry : statuses){
stream.writeByte(entry.effect.id);
stream.writeShort((short) (entry.time * 2));
}
}
@Override
public void readSave(DataInput stream) throws IOException{
for(StatusEntry effect : statuses){
Pooling.free(effect);
}
statuses.clear();
byte amount = stream.readByte();
for(int i = 0; i < amount; i++){
byte id = stream.readByte();
float time = stream.readShort() / 2f;
StatusEntry entry = Pooling.obtain(StatusEntry.class);
entry.set(StatusEffect.getByID(id), time);
statuses.add(entry);
}
}
public static class StatusEntry{
public StatusEffect effect;
public float time;
public StatusEntry set(StatusEffect effect, float time){
this.effect = effect;
this.time = time;
return this;
}
}
}

View File

@@ -1,5 +0,0 @@
package io.anuke.mindustry.entities;
public enum StatusEffect{
none;
}

View File

@@ -1,139 +0,0 @@
package io.anuke.mindustry.entities;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.ObjectIntMap;
import com.badlogic.gdx.utils.TimeUtils;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.DestructibleEntity;
import io.anuke.ucore.util.Mathf;
import java.nio.ByteBuffer;
import static io.anuke.mindustry.Vars.threads;
public abstract class SyncEntity extends DestructibleEntity{
private static ObjectIntMap<Class<? extends SyncEntity>> writeSizes = new ObjectIntMap<>();
protected transient Interpolator interpolator = new Interpolator();
//smoothed position/angle
private Vector3 spos = new Vector3();
public float angle;
static{
setWriteSize(Enemy.class, 4 + 4 + 2 + 2);
setWriteSize(Player.class, 4 + 4 + 4 + 2 + 1);
}
public static boolean isSmoothing(){
return threads.isEnabled() && threads.getFPS() <= Gdx.graphics.getFramesPerSecond() / 2f;
}
public abstract void writeSpawn(ByteBuffer data);
public abstract void readSpawn(ByteBuffer data);
public abstract void write(ByteBuffer data);
public abstract void read(ByteBuffer data, long time);
public void interpolate(){
interpolator.update();
x = interpolator.pos.x;
y = interpolator.pos.y;
angle = interpolator.angle;
}
@Override
public final void draw(){
final float x = this.x, y = this.y, angle = this.angle;
//interpolates data at low tick speeds.
if(isSmoothing()){
if(Vector2.dst(spos.x, spos.y, x, y) > 128){
spos.set(x, y, angle);
}
this.x = spos.x = Mathf.lerpDelta(spos.x, x, 0.2f);
this.y = spos.y = Mathf.lerpDelta(spos.y, y, 0.2f);
this.angle = spos.z = Mathf.slerpDelta(spos.z, angle, 0.3f);
}
drawSmooth();
this.x = x;
this.y = y;
this.angle = angle;
}
public Vector3 getDrawPosition(){
return isSmoothing() ? spos : spos.set(x, y, angle);
}
public void drawSmooth(){}
public int getWriteSize(){
return getWriteSize(getClass());
}
public static int getWriteSize(Class<? extends SyncEntity> type){
int i = writeSizes.get(type, -1);
if(i == -1) throw new RuntimeException("Write size for class \"" + type + "\" is not defined!");
return i;
}
protected static void setWriteSize(Class<? extends SyncEntity> type, int size){
writeSizes.put(type, size);
}
public <T extends SyncEntity> T setNet(float x, float y){
set(x, y);
interpolator.target.set(x, y);
interpolator.last.set(x, y);
interpolator.spacing = 1f;
interpolator.time = 0f;
return (T)this;
}
public static class Interpolator {
//used for movement
public Vector2 target = new Vector2();
public Vector2 last = new Vector2();
public float targetrot;
public float spacing = 1f;
public float time;
//current state
public Vector2 pos = new Vector2();
public float angle;
public void read(float cx, float cy, float x, float y, float angle, long sent){
targetrot = angle;
time = 0f;
last.set(cx, cy);
target.set(x, y);
spacing = Math.min(Math.max(((TimeUtils.timeSinceMillis(sent) / 1000f) * 60f), 4f), 10);
}
public void update(){
time += 1f / spacing * Math.min(Timers.delta(), 1f);
time = Mathf.clamp(time, 0, 2f);
Mathf.lerp2(pos.set(last), target, time);
angle = Mathf.slerpDelta(angle, targetrot, 0.6f);
if(target.dst(pos) > 128){
pos.set(target);
last.set(target);
time = 0f;
}
}
}
}

View File

@@ -1,15 +1,30 @@
package io.anuke.mindustry.entities;
import io.anuke.mindustry.graphics.Fx;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.net.NetEvents;
import io.anuke.mindustry.resource.Item;
import com.badlogic.gdx.math.GridPoint2;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ObjectSet;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.mindustry.content.fx.Fx;
import io.anuke.mindustry.entities.bullet.Bullet;
import io.anuke.mindustry.entities.traits.TargetTrait;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.gen.CallBlocks;
import io.anuke.mindustry.net.In;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Edges;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.types.Wall;
import io.anuke.mindustry.world.blocks.Wall;
import io.anuke.mindustry.world.consumers.Consume;
import io.anuke.mindustry.world.modules.ConsumeModule;
import io.anuke.mindustry.world.modules.InventoryModule;
import io.anuke.mindustry.world.modules.LiquidModule;
import io.anuke.mindustry.world.modules.PowerModule;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.Entity;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.impl.BaseEntity;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Timer;
@@ -20,134 +35,219 @@ import java.io.IOException;
import static io.anuke.mindustry.Vars.tileGroup;
import static io.anuke.mindustry.Vars.world;
public class TileEntity extends Entity{
public Tile tile;
public int[] items = new int[Item.getAllItems().size];
public Timer timer;
public float health;
public boolean dead = false;
public boolean added;
/**Sets this tile entity data to this tile, and adds it if necessary.*/
public TileEntity init(Tile tile, boolean added){
this.tile = tile;
this.added = added;
x = tile.worldx();
y = tile.worldy();
public class TileEntity extends BaseEntity implements TargetTrait{
public static final float timeToSleep = 60f * 4; //4 seconds to fall asleep
private static final ObjectSet<Tile> tmpTiles = new ObjectSet<>();
/**
* This value is only used for debugging.
*/
public static int sleepingEntities = 0;
public Tile tile;
public Timer timer;
public float health;
health = tile.block().health;
timer = new Timer(tile.block().timers);
if(added){
add();
}
return this;
}
public void write(DataOutputStream stream) throws IOException{
}
public void read(DataInputStream stream) throws IOException{
}
public PowerModule power;
public InventoryModule items;
public LiquidModule liquids;
public ConsumeModule cons;
public void readNetwork(DataInputStream stream, float elapsed) throws IOException{
read(stream);
}
public void onDeath(){
onDeath(false);
}
/**List of (cached) tiles with entities in proximity, used for outputting to*/
private Array<Tile> proximity = new Array<>(8);
private boolean dead = false;
private boolean sleeping;
private float sleepTime;
public void onDeath(boolean force){
if(Net.server()){
NetEvents.handleBlockDestroyed(this);
}
@Remote(called = Loc.server, in = In.blocks)
public static void onTileDamage(Tile tile, float health){
if(tile.entity != null){
tile.entity.health = health;
}
}
if(!Net.active() || Net.server() || force){
@Remote(called = Loc.server, in = In.blocks)
public static void onTileDestroyed(Tile tile){
if(tile.entity == null) return;
tile.entity.onDeath();
}
if(!dead) {
dead = true;
Block block = tile.block();
/**Sets this tile entity data to this tile, and adds it if necessary.*/
public TileEntity init(Tile tile, boolean added){
this.tile = tile;
x = tile.drawx();
y = tile.drawy();
block.onDestroyed(tile);
health = tile.block().health;
world.removeBlock(tile);
remove();
}
}
}
public void collision(Bullet other){
damage(other.getDamage());
}
public void damage(int damage){
if(dead) return;
int amount = tile.block().handleDamage(tile, damage);
health -= amount;
if(health <= 0) onDeath();
timer = new Timer(tile.block().timers);
if(Net.server()){
NetEvents.handleBlockDamaged(this);
}
}
public boolean collide(Bullet other){
return true;
}
@Override
public void update(){
synchronized (Tile.tileSetLock) {
if (health != 0 && health < tile.block().health && !(tile.block() instanceof Wall) &&
Mathf.chance(0.009f * Timers.delta() * (1f - health / tile.block().health))) {
if(added){
add();
}
Effects.effect(Fx.smoke, x + Mathf.range(4), y + Mathf.range(4));
}
return this;
}
if (health <= 0) {
onDeath();
}
/**
* Call when nothing is happening to the entity.
* This increments the internal sleep timer.
*/
public void sleep(){
sleepTime += Timers.delta();
if(!sleeping && sleepTime >= timeToSleep){
remove();
sleeping = true;
sleepingEntities++;
}
}
tile.block().update(tile);
}
}
public int totalItems(){
int sum = 0;
for(int i = 0; i < items.length; i ++){
sum += items[i];
}
return sum;
}
public int getItem(Item item){
return items[item.id];
}
public boolean hasItem(Item item){
return getItem(item) > 0;
}
public boolean hasItem(Item item, int amount){
return getItem(item) >= amount;
}
public void addItem(Item item, int amount){
items[item.id] += amount;
}
public void removeItem(Item item, int amount){
items[item.id] -= amount;
}
@Override
public TileEntity add(){
return add(tileGroup);
}
/**
* Call when something just happened to the entity.
* If the entity was sleeping, this enables it. This also resets the sleep timer.
*/
public void wakeUp(){
sleepTime = 0f;
if(sleeping){
add();
sleeping = false;
sleepingEntities--;
}
}
public boolean isSleeping(){
return sleeping;
}
public boolean isDead(){
return dead;
}
public void write(DataOutputStream stream) throws IOException{
}
public void read(DataInputStream stream) throws IOException{
}
private void onDeath(){
if(!dead){
dead = true;
Block block = tile.block();
block.onDestroyed(tile);
world.removeBlock(tile);
block.afterDestroyed(tile, this);
remove();
}
}
public boolean collide(Bullet other){
return true;
}
public void collision(Bullet other){
tile.block().handleBulletHit(this, other);
}
public void damage(float damage){
if(dead) return;
CallBlocks.onTileDamage(tile, health - tile.block().handleDamage(tile, damage));
if(health <= 0){
CallBlocks.onTileDestroyed(tile);
}
}
public Tile getTile(){
return tile;
}
public boolean consumed(Class<? extends Consume> type){
return tile.block().consumes.get(type).valid(tile.block(), this);
}
public void removeFromProximity(){
GridPoint2[] nearby = Edges.getEdges(tile.block().size);
for(GridPoint2 point : nearby){
Tile other = world.tile(tile.x + point.x, tile.y + point.y);
//remove this tile from all nearby tile's proximities
if(other != null){
other = other.target();
other.block().onProximityUpdate(other);
}
if(other != null && other.entity != null){
other.entity.proximity.removeValue(tile, true);
}
}
}
public void updateProximity(){
tmpTiles.clear();
proximity.clear();
GridPoint2[] nearby = Edges.getEdges(tile.block().size);
for(GridPoint2 point : nearby){
Tile other = world.tile(tile.x + point.x, tile.y + point.y);
if(other != null){
other.block().onProximityUpdate(other);
other = other.target();
}
if(other != null && other.entity != null){
tmpTiles.add(other);
//add this tile to proximity of nearby tiles
if(!other.entity.proximity.contains(tile, true)){
other.entity.proximity.add(tile);
}
}
}
//using a set to prevent duplicates
for(Tile tile : tmpTiles){
proximity.add(tile);
}
tile.block().onProximityUpdate(tile);
}
public Array<Tile> proximity(){
return proximity;
}
@Override
public Team getTeam(){
return tile.getTeam();
}
@Override
public Vector2 getVelocity(){
return Vector2.Zero;
}
@Override
public void update(){
synchronized(Tile.tileSetLock){
//TODO better smoke effect, this one is awful
if(health != 0 && health < tile.block().health && !(tile.block() instanceof Wall) &&
Mathf.chance(0.009f * Timers.delta() * (1f - health / tile.block().health))){
Effects.effect(Fx.smoke, x + Mathf.range(4), y + Mathf.range(4));
}
if(health <= 0){
onDeath();
}
tile.block().update(tile);
if(cons != null){
cons.update(this);
}
}
}
@Override
public EntityGroup targetGroup(){
return tileGroup;
}
}

View File

@@ -0,0 +1,352 @@
package io.anuke.mindustry.entities;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import io.anuke.mindustry.content.blocks.Blocks;
import io.anuke.mindustry.entities.traits.*;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.game.TeamInfo.TeamData;
import io.anuke.mindustry.net.Interpolator;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.StatusEffect;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.Floor;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.EntityPhysics;
import io.anuke.ucore.entities.impl.DestructibleEntity;
import io.anuke.ucore.entities.trait.DamageTrait;
import io.anuke.ucore.entities.trait.DrawTrait;
import io.anuke.ucore.entities.trait.SolidTrait;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.util.Geometry;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Translator;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import static io.anuke.mindustry.Vars.state;
import static io.anuke.mindustry.Vars.world;
public abstract class Unit extends DestructibleEntity implements SaveTrait, TargetTrait, SyncTrait, DrawTrait, TeamTrait, CarriableTrait, InventoryTrait{
/**
* total duration of hit flash effect
*/
public static final float hitDuration = 9f;
/**
* Percision divisor of velocity, used when writing. For example a value of '2' would mean the percision is 1/2 = 0.5-size chunks.
*/
public static final float velocityPercision = 8f;
/**
* Maximum absolute value of a velocity vector component.
*/
public static final float maxAbsVelocity = 127f / velocityPercision;
public static final float elevationScale = 4f;
private static final Vector2 moveVector = new Vector2();
public UnitInventory inventory = new UnitInventory(this);
public float rotation;
protected Interpolator interpolator = new Interpolator();
protected StatusController status = new StatusController();
protected Team team = Team.blue;
protected CarryTrait carrier;
protected Vector2 velocity = new Translator(0f, 0.0001f);
protected float hitTime;
protected float drownTime;
protected float elevation;
@Override
public UnitInventory getInventory(){
return inventory;
}
@Override
public float getRotation(){
return rotation;
}
@Override
public void setRotation(float rotation){
this.rotation = rotation;
}
@Override
public CarryTrait getCarrier(){
return carrier;
}
@Override
public void setCarrier(CarryTrait carrier){
this.carrier = carrier;
}
@Override
public Team getTeam(){
return team;
}
@Override
public void interpolate(){
interpolator.update();
x = interpolator.pos.x;
y = interpolator.pos.y;
if(interpolator.values.length > 0){
rotation = interpolator.values[0];
}
}
@Override
public Interpolator getInterpolator(){
return interpolator;
}
@Override
public void damage(float amount){
super.damage(calculateDamage(amount));
hitTime = hitDuration;
}
@Override
public boolean collides(SolidTrait other){
return other instanceof DamageTrait && other
instanceof TeamTrait && state.teams.areEnemies((((TeamTrait) other).getTeam()), team) && !isDead();
}
@Override
public void onDeath(){
inventory.clear();
drownTime = 0f;
status.clear();
}
@Override
public Vector2 getVelocity(){
return velocity;
}
@Override
public void writeSave(DataOutput stream) throws IOException{
writeSave(stream, false);
}
@Override
public void readSave(DataInput stream) throws IOException{
byte team = stream.readByte();
boolean dead = stream.readBoolean();
float x = stream.readFloat();
float y = stream.readFloat();
byte xv = stream.readByte();
byte yv = stream.readByte();
float rotation = stream.readShort() / 2f;
int health = stream.readShort();
this.status.readSave(stream);
this.inventory.readSave(stream);
this.dead = dead;
this.team = Team.all[team];
this.health = health;
this.x = x;
this.y = y;
this.velocity.set(xv / velocityPercision, yv / velocityPercision);
this.rotation = rotation;
}
public void writeSave(DataOutput stream, boolean net) throws IOException{
stream.writeByte(team.ordinal());
stream.writeBoolean(isDead());
stream.writeFloat(net ? interpolator.target.x : x);
stream.writeFloat(net ? interpolator.target.y : y);
stream.writeByte((byte) (Mathf.clamp(velocity.x, -maxAbsVelocity, maxAbsVelocity) * velocityPercision));
stream.writeByte((byte) (Mathf.clamp(velocity.y, -maxAbsVelocity, maxAbsVelocity) * velocityPercision));
stream.writeShort((short) (rotation * 2));
stream.writeShort((short) health);
status.writeSave(stream);
inventory.writeSave(stream);
}
public float calculateDamage(float amount){
return amount * Mathf.clamp(1f - getArmor() / 100f * status.getArmorMultiplier());
}
public float getDamageMultipler(){
return status.getDamageMultiplier();
}
public boolean hasEffect(StatusEffect effect){
return status.hasEffect(effect);
}
public TileEntity getClosestCore(){
if(state.teams.has(team)){
TeamData data = state.teams.get(team);
Tile tile = Geometry.findClosest(x, y, data.cores);
if(tile == null){
return null;
}else{
return tile.entity;
}
}else{
return null;
}
}
public Floor getFloorOn(){
Tile tile = world.tileWorld(x, y);
return tile == null ? (Floor) Blocks.air : tile.floor();
}
public void avoidOthers(float avoidRange){
EntityPhysics.getNearby(getGroup(), x, y, avoidRange * 2f, t -> {
if(t == this || (t instanceof Unit && (((Unit) t).isDead() || (((Unit) t).isFlying() != isFlying()) || ((Unit) t).getCarrier() == this) || getCarrier() == t))
return;
float dst = distanceTo(t);
if(dst > avoidRange) return;
velocity.add(moveVector.set(x, y).sub(t.getX(), t.getY()).setLength(1f * (1f - (dst / avoidRange))));
});
}
/**
* Updates velocity and status effects.
*/
public void updateVelocityStatus(float drag, float maxVelocity){
if(isCarried()){ //carried units do not take into account velocity normally
set(carrier.getX(), carrier.getY());
velocity.set(carrier.getVelocity());
return;
}
Floor floor = getFloorOn();
Tile tile = world.tileWorld(x, y);
status.update(this);
velocity.limit(maxVelocity).scl(status.getSpeedMultiplier());
if(isFlying()){
x += velocity.x / getMass() * Timers.delta();
y += velocity.y / getMass() * Timers.delta();
if(tile != null){
elevation = Mathf.lerpDelta(elevation, tile.elevation, 0.04f);
}
}else{
boolean onLiquid = floor.isLiquid;
if(tile != null){
tile.block().unitOn(tile, this);
if(tile.block() != Blocks.air){
onLiquid = false;
}
//on slope
if(tile.elevation == -1){
velocity.scl(0.7f);
}
}
if(onLiquid && velocity.len() > 0.4f && Timers.get(this, "flooreffect", 14 - (velocity.len() * floor.speedMultiplier) * 2f)){
Effects.effect(floor.walkEffect, floor.liquidColor, x, y);
}
status.handleApply(this, floor.status, floor.statusIntensity);
if(floor.damageTaken > 0f){
damagePeriodic(floor.damageTaken);
}
if(onLiquid && floor.drownTime > 0){
drownTime += Timers.delta() * 1f / floor.drownTime;
if(Timers.get(this, "drowneffect", 15)){
Effects.effect(floor.drownUpdateEffect, floor.liquidColor, x, y);
}
}else{
drownTime = Mathf.lerpDelta(drownTime, 0f, 0.03f);
}
drownTime = Mathf.clamp(drownTime);
if(drownTime >= 1f){
damage(health + 1);
}
float px = x, py = y;
move(velocity.x / getMass() * floor.speedMultiplier * Timers.delta(), velocity.y / getMass() * floor.speedMultiplier * Timers.delta());
if(Math.abs(px - x) <= 0.0001f) velocity.x = 0f;
if(Math.abs(py - y) <= 0.0001f) velocity.y = 0f;
}
velocity.scl(Mathf.clamp(1f - drag * floor.dragMultiplier * Timers.delta()));
}
public void applyEffect(StatusEffect effect, float intensity){
if(dead || Net.client()) return; //effects are synced and thus not applied through clients
status.handleApply(this, effect, intensity);
}
public void damagePeriodic(float amount){
damage(amount * Timers.delta(), Timers.get(this, "damageeffect", 20));
}
public void damage(float amount, boolean withEffect){
float pre = hitTime;
damage(amount);
if(!withEffect){
hitTime = pre;
}
}
public float getAmmoFraction(){
return inventory.totalAmmo() / (float) inventory.ammoCapacity();
}
public void drawUnder(){
}
public void drawOver(){
}
public void drawShadow(){
}
public void drawView(){
Fill.circle(x, y, getViewDistance());
}
public boolean isInfiniteAmmo(){
return false;
}
public float getViewDistance(){
return 135f;
}
public abstract TextureRegion getIconRegion();
public abstract int getItemCapacity();
public abstract int getAmmoCapacity();
public abstract float getArmor();
public abstract boolean acceptsAmmo(Item item);
public abstract void addAmmo(Item item);
public abstract float getMass();
public abstract boolean isFlying();
public abstract float getSize();
}

View File

@@ -0,0 +1,163 @@
package io.anuke.mindustry.entities;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.entities.traits.Saveable;
import io.anuke.mindustry.type.AmmoEntry;
import io.anuke.mindustry.type.AmmoType;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.ItemStack;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class UnitInventory implements Saveable{
private final Unit unit;
private Array<AmmoEntry> ammos = new Array<>();
private int totalAmmo;
private ItemStack item = new ItemStack(Items.stone, 0);
public UnitInventory(Unit unit){
this.unit = unit;
}
public boolean isFull(){
return item != null && item.amount >= unit.getItemCapacity();
}
@Override
public void writeSave(DataOutput stream) throws IOException{
stream.writeShort(item.amount);
stream.writeByte(item.item.id);
stream.writeShort(totalAmmo);
stream.writeByte(ammos.size);
for(int i = 0; i < ammos.size; i++){
stream.writeByte(ammos.get(i).type.id);
stream.writeShort(ammos.get(i).amount);
}
}
@Override
public void readSave(DataInput stream) throws IOException{
short iamount = stream.readShort();
byte iid = stream.readByte();
this.totalAmmo = stream.readShort();
byte ammoa = stream.readByte();
for(int i = 0; i < ammoa; i++){
byte aid = stream.readByte();
int am = stream.readShort();
ammos.add(new AmmoEntry(AmmoType.getByID(aid), am));
}
item.item = Item.getByID(iid);
item.amount = iamount;
}
/**
* Returns ammo range, or MAX_VALUE if this inventory has no ammo.
*/
public float getAmmoRange(){
return hasAmmo() ? getAmmo().getRange() : Float.MAX_VALUE;
}
public AmmoType getAmmo(){
return ammos.size == 0 ? null : ammos.peek().type;
}
public boolean hasAmmo(){
return totalAmmo > 0;
}
public void useAmmo(){
if(unit.isInfiniteAmmo()) return;
AmmoEntry entry = ammos.peek();
entry.amount--;
if(entry.amount == 0) ammos.pop();
totalAmmo--;
}
public int totalAmmo(){
return totalAmmo;
}
public int ammoCapacity(){
return unit.getAmmoCapacity();
}
public boolean canAcceptAmmo(AmmoType type){
return totalAmmo + type.quantityMultiplier <= unit.getAmmoCapacity();
}
public void addAmmo(AmmoType type){
if(type == null) return;
totalAmmo += type.quantityMultiplier;
//find ammo entry by type
for(int i = ammos.size - 1; i >= 0; i--){
AmmoEntry entry = ammos.get(i);
//if found, put it to the right
if(entry.type == type){
entry.amount += type.quantityMultiplier;
ammos.swap(i, ammos.size - 1);
return;
}
}
//must not be found
AmmoEntry entry = new AmmoEntry(type, (int) type.quantityMultiplier);
ammos.add(entry);
}
public int capacity(){
return unit.getItemCapacity();
}
public boolean isEmpty(){
return item.amount == 0;
}
public int itemCapacityUsed(Item type){
if(canAcceptItem(type)){
return !hasItem() ? unit.getItemCapacity() : (unit.getItemCapacity() - item.amount);
}else{
return unit.getItemCapacity();
}
}
public boolean canAcceptItem(Item type){
return (!hasItem() && 1 <= unit.getItemCapacity()) || (item.item == type && unit.getItemCapacity() - item.amount > 0);
}
public boolean canAcceptItem(Item type, int amount){
return (!hasItem() && amount <= unit.getItemCapacity()) || (item.item == type && item.amount + amount <= unit.getItemCapacity());
}
public void clear(){
item.amount = 0;
ammos.clear();
totalAmmo = 0;
}
public void clearItem(){
item.amount = 0;
}
public boolean hasItem(){
return item.amount > 0;
}
public boolean hasItem(Item i, int amount){
return item.item == i && item.amount >= amount;
}
public void addItem(Item item, int amount){
getItem().amount = getItem().item == item ? getItem().amount + amount : amount;
getItem().item = item;
}
public ItemStack getItem(){
return item;
}
}

View File

@@ -0,0 +1,307 @@
package io.anuke.mindustry.entities;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.ObjectSet;
import io.anuke.mindustry.entities.traits.TargetTrait;
import io.anuke.mindustry.entities.units.BaseUnit;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.EntityPhysics;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.function.Predicate;
import static io.anuke.mindustry.Vars.*;
/**
* Utility class for unit and team interactions.
*/
public class Units{
private static Rectangle rect = new Rectangle();
private static Rectangle hitrect = new Rectangle();
private static Unit result;
private static float cdist;
private static boolean boolResult;
/**
* Validates a target.
*
* @param target The target to validate
* @param team The team of the thing doing tha targeting
* @param x The X position of the thing doign the targeting
* @param y The Y position of the thing doign the targeting
* @param range The maximum distance from the target X/Y the targeter can be for it to be valid
* @return whether the target is invalid
*/
public static boolean invalidateTarget(TargetTrait target, Team team, float x, float y, float range){
return target == null || (range != Float.MAX_VALUE && target.distanceTo(x, y) > range) || target.getTeam() == team || !target.isValid();
}
/**
* See {@link #invalidateTarget(TargetTrait, Team, float, float, float)}
*/
public static boolean invalidateTarget(TargetTrait target, Team team, float x, float y){
return invalidateTarget(target, team, x, y, Float.MAX_VALUE);
}
/**
* See {@link #invalidateTarget(TargetTrait, Team, float, float, float)}
*/
public static boolean invalidateTarget(TargetTrait target, Unit targeter){
return invalidateTarget(target, targeter.team, targeter.x, targeter.y, targeter.inventory.getAmmoRange());
}
/**
* Returns whether there are any entities on this tile.
*/
public static boolean anyEntities(Tile tile){
Block type = tile.block();
rect.setSize(type.size * tilesize, type.size * tilesize);
rect.setCenter(tile.drawx(), tile.drawy());
boolResult = false;
Units.getNearby(rect, unit -> {
if(boolResult) return;
if(!unit.isFlying()){
unit.getHitbox(hitrect);
if(hitrect.overlaps(rect)){
boolResult = true;
}
}
});
return boolResult;
}
/**
* Returns whether there are any entities on this tile, with the hitbox expanded.
*/
public static boolean anyEntities(Tile tile, float expansion, Predicate<Unit> pred){
Block type = tile.block();
rect.setSize(type.size * tilesize + expansion, type.size * tilesize + expansion);
rect.setCenter(tile.drawx(), tile.drawy());
boolean[] value = new boolean[1];
Units.getNearby(rect, unit -> {
if(value[0] || !pred.test(unit) || unit.isDead()) return;
if(!unit.isFlying()){
unit.getHitbox(hitrect);
if(hitrect.overlaps(rect)){
value[0] = true;
}
}
});
return value[0];
}
/**
* Returns the neareset ally tile in a range.
*/
public static TileEntity findAllyTile(Team team, float x, float y, float range, Predicate<Tile> pred){
for(Team enemy : state.teams.alliesOf(team)){
TileEntity entity = world.indexer().findTile(enemy, x, y, range, pred);
if(entity != null){
return entity;
}
}
return null;
}
/**
* Returns the neareset enemy tile in a range.
*/
public static TileEntity findEnemyTile(Team team, float x, float y, float range, Predicate<Tile> pred){
for(Team enemy : state.teams.enemiesOf(team)){
TileEntity entity = world.indexer().findTile(enemy, x, y, range, pred);
if(entity != null){
return entity;
}
}
return null;
}
/**
* Iterates over all units on all teams, including players.
*/
public static void allUnits(Consumer<Unit> cons){
//check all unit groups first
for(EntityGroup<BaseUnit> group : unitGroups){
if(!group.isEmpty()){
for(BaseUnit unit : group.all()){
cons.accept(unit);
}
}
}
//then check all player groups
for(Player player : playerGroup.all()){
cons.accept(player);
}
}
/**
* Returns the closest target enemy. First, units are checked, then tile entities.
*/
public static TargetTrait getClosestTarget(Team team, float x, float y, float range){
Unit unit = getClosestEnemy(team, x, y, range, u -> true);
if(unit != null){
return unit;
}else{
return findEnemyTile(team, x, y, range, tile -> true);
}
}
/**
* Returns the closest enemy of this team. Filter by predicate.
*/
public static Unit getClosestEnemy(Team team, float x, float y, float range, Predicate<Unit> predicate){
result = null;
cdist = 0f;
rect.setSize(range * 2f).setCenter(x, y);
getNearbyEnemies(team, rect, e -> {
if(e.isDead() || !predicate.test(e))
return;
float dist = Vector2.dst(e.x, e.y, x, y);
if(dist < range){
if(result == null || dist < cdist){
result = e;
cdist = dist;
}
}
});
return result;
}
/**
* Returns the closest ally of this team. Filter by predicate.
*/
public static Unit getClosest(Team team, float x, float y, float range, Predicate<Unit> predicate){
result = null;
cdist = 0f;
rect.setSize(range * 2f).setCenter(x, y);
getNearby(team, rect, e -> {
if(!predicate.test(e))
return;
float dist = Vector2.dst(e.x, e.y, x, y);
if(dist < range){
if(result == null || dist < cdist){
result = e;
cdist = dist;
}
}
});
return result;
}
/**
* Iterates over all units in a rectangle.
*/
public static void getNearby(Team team, Rectangle rect, Consumer<Unit> cons){
EntityGroup<BaseUnit> group = unitGroups[team.ordinal()];
if(!group.isEmpty()){
EntityPhysics.getNearby(group, rect, entity -> cons.accept((Unit) entity));
}
//now check all players
EntityPhysics.getNearby(playerGroup, rect, player -> {
if(((Unit) player).team == team) cons.accept((Unit) player);
});
}
/**
* Iterates over all units in a circle around this position.
*/
public static void getNearby(Team team, float x, float y, float radius, Consumer<Unit> cons){
rect.setSize(radius * 2).setCenter(x, y);
EntityGroup<BaseUnit> group = unitGroups[team.ordinal()];
if(!group.isEmpty()){
EntityPhysics.getNearby(group, rect, entity -> {
if(entity.distanceTo(x, y) <= radius){
cons.accept((Unit) entity);
}
});
}
//now check all players
EntityPhysics.getNearby(playerGroup, rect, player -> {
if(((Unit) player).team == team && player.distanceTo(x, y) <= radius){
cons.accept((Unit) player);
}
});
}
/**
* Iterates over all units in a rectangle.
*/
public static void getNearby(Rectangle rect, Consumer<Unit> cons){
for(Team team : Team.all){
EntityGroup<BaseUnit> group = unitGroups[team.ordinal()];
if(!group.isEmpty()){
EntityPhysics.getNearby(group, rect, entity -> cons.accept((Unit) entity));
}
}
//now check all enemy players
EntityPhysics.getNearby(playerGroup, rect, player -> cons.accept((Unit) player));
}
/**
* Iterates over all units that are enemies of this team.
*/
public static void getNearbyEnemies(Team team, Rectangle rect, Consumer<Unit> cons){
ObjectSet<Team> targets = state.teams.enemiesOf(team);
for(Team other : targets){
EntityGroup<BaseUnit> group = unitGroups[other.ordinal()];
if(!group.isEmpty()){
EntityPhysics.getNearby(group, rect, entity -> cons.accept((Unit) entity));
}
}
//now check all enemy players
EntityPhysics.getNearby(playerGroup, rect, player -> {
if(targets.contains(((Player) player).team)){
cons.accept((Unit) player);
}
});
}
/**
* Iterates over all units.
*/
public static void getAllUnits(Consumer<Unit> cons){
for(Team team : Team.all){
EntityGroup<BaseUnit> group = unitGroups[team.ordinal()];
for(Unit unit : group.all()){
cons.accept(unit);
}
}
//now check all enemy players
for(Unit unit : playerGroup.all()){
cons.accept(unit);
}
}
}

View File

@@ -0,0 +1,41 @@
package io.anuke.mindustry.entities.bullet;
import io.anuke.mindustry.content.fx.BulletFx;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.graphics.Draw;
//TODO scale velocity depending on fslope()
public class ArtilleryBulletType extends BasicBulletType{
protected Effect trailEffect = BulletFx.artilleryTrail;
public ArtilleryBulletType(float speed, float damage, String bulletSprite){
super(speed, damage, bulletSprite);
collidesTiles = false;
collides = false;
hitShake = 1f;
}
@Override
public void update(Bullet b){
super.update(b);
if(b.timer.get(0, 3 + b.fslope() * 2f)){
Effects.effect(trailEffect, backColor, b.x, b.y, b.fslope() * 4f);
}
}
@Override
public void draw(Bullet b){
float baseScale = 0.7f;
float scale = (baseScale + b.fslope() * (1f - baseScale));
float height = bulletHeight * ((1f - bulletShrink) + bulletShrink * b.fout());
Draw.color(backColor);
Draw.rect(backRegion, b.x, b.y, bulletWidth * scale, height * scale, b.angle() - 90);
Draw.color(frontColor);
Draw.rect(frontRegion, b.x, b.y, bulletWidth * scale, height * scale, b.angle() - 90);
Draw.color();
}
}

View File

@@ -0,0 +1,109 @@
package io.anuke.mindustry.entities.bullet;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import io.anuke.mindustry.entities.Damage;
import io.anuke.mindustry.entities.Unit;
import io.anuke.mindustry.entities.Units;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
/**
* A BulletType for most ammo-based bullets shot from turrets and units.
*/
public class BasicBulletType extends BulletType{
public Color backColor = Palette.bulletYellowBack, frontColor = Palette.bulletYellow;
public float bulletWidth = 5f, bulletHeight = 7f;
public float bulletShrink = 0.5f;
public String bulletSprite;
public int fragBullets = 9;
public float fragVelocityMin = 0.2f, fragVelocityMax = 1f;
public BulletType fragBullet = null;
/**
* Use a negative value to disable splash damage.
*/
public float splashDamageRadius = -1f;
public float splashDamage = 6f;
public int incendAmount = 0;
public float incendSpread = 8f;
public float incendChance = 1f;
public float homingPower = 0f;
public float homingRange = 40f;
public TextureRegion backRegion;
public TextureRegion frontRegion;
public float hitShake = 0f;
public BasicBulletType(float speed, float damage, String bulletSprite){
super(speed, damage);
this.bulletSprite = bulletSprite;
}
@Override
public void load(){
backRegion = Draw.region(bulletSprite + "-back");
frontRegion = Draw.region(bulletSprite);
}
@Override
public void draw(Bullet b){
float height = bulletHeight * ((1f - bulletShrink) + bulletShrink * b.fout());
Draw.color(backColor);
Draw.rect(backRegion, b.x, b.y, bulletWidth, height, b.angle() - 90);
Draw.color(frontColor);
Draw.rect(frontRegion, b.x, b.y, bulletWidth, height, b.angle() - 90);
Draw.color();
}
@Override
public void update(Bullet b){
super.update(b);
if(homingPower > 0.0001f){
Unit target = Units.getClosestEnemy(b.getTeam(), b.x, b.y, homingRange, unit -> true);
if(target != null){
b.getVelocity().setAngle(Angles.moveToward(b.getVelocity().angle(), b.angleTo(target), homingPower * Timers.delta()));
}
}
}
@Override
public void hit(Bullet b, float x, float y){
super.hit(b, x, y);
Effects.shake(hitShake, hitShake, b);
if(fragBullet != null){
for(int i = 0; i < fragBullets; i++){
float len = Mathf.random(1f, 7f);
float a = Mathf.random(360f);
Bullet.create(fragBullet, b, x + Angles.trnsx(a, len), y + Angles.trnsy(a, len), a, Mathf.random(fragVelocityMin, fragVelocityMax));
}
}
if(Mathf.chance(incendChance)){
Damage.createIncend(x, y, incendSpread, incendAmount);
}
if(splashDamageRadius > 0){
Damage.damage(b.getTeam(), x, y, splashDamageRadius, splashDamage);
}
}
@Override
public void despawned(Bullet b){
if(fragBullet != null || splashDamageRadius > 0){
hit(b);
}
}
}

View File

@@ -0,0 +1,16 @@
package io.anuke.mindustry.entities.bullet;
public class BombBulletType extends BasicBulletType{
public BombBulletType(float damage, float radius, String sprite){
super(0.7f, 0, sprite);
splashDamageRadius = radius;
splashDamage = damage;
collidesTiles = false;
collides = false;
bulletShrink = 0.7f;
lifetime = 30f;
drag = 0.05f;
keepVelocity = false;
}
}

View File

@@ -0,0 +1,225 @@
package io.anuke.mindustry.entities.bullet;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.TimeUtils;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.mindustry.entities.Unit;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.entities.traits.TeamTrait;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.net.In;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.impl.BulletEntity;
import io.anuke.ucore.entities.trait.Entity;
import io.anuke.ucore.entities.trait.SolidTrait;
import io.anuke.ucore.entities.trait.VelocityTrait;
import io.anuke.ucore.util.Pooling;
import io.anuke.ucore.util.Timer;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import static io.anuke.mindustry.Vars.bulletGroup;
import static io.anuke.mindustry.Vars.world;
public class Bullet extends BulletEntity<BulletType> implements TeamTrait, SyncTrait{
private static Vector2 vector = new Vector2();
public Timer timer = new Timer(3);
private Team team;
private Object data;
private boolean supressCollision;
/**
* Internal use only!
*/
public Bullet(){
}
public static void create(BulletType type, TeamTrait owner, float x, float y, float angle){
create(type, owner, owner.getTeam(), x, y, angle);
}
public static void create(BulletType type, Entity owner, Team team, float x, float y, float angle){
create(type, owner, team, x, y, angle, 1f);
}
public static void create(BulletType type, Entity owner, Team team, float x, float y, float angle, float velocityScl){
create(type, owner, team, x, y, angle, velocityScl, null);
}
public static void create(BulletType type, Entity owner, Team team, float x, float y, float angle, float velocityScl, Object data){
Bullet bullet = Pooling.obtain(Bullet.class);
bullet.type = type;
bullet.owner = owner;
bullet.data = data;
bullet.velocity.set(0, type.speed).setAngle(angle).scl(velocityScl);
if(type.keepVelocity){
bullet.velocity.add(owner instanceof VelocityTrait ? ((VelocityTrait) owner).getVelocity() : Vector2.Zero);
}
bullet.hitbox.setSize(type.hitsize);
bullet.team = team;
bullet.type = type;
//translate bullets backwards, purely for visual reasons
float backDelta = Timers.delta();
bullet.lastPosition().set(x - bullet.velocity.x * backDelta, y - bullet.velocity.y * backDelta, bullet.angle());
bullet.setLastUpdated(TimeUtils.millis());
bullet.setUpdateSpacing((long) ((Timers.delta() / 60f) * 1000));
bullet.set(x - bullet.velocity.x * backDelta, y - bullet.velocity.y * backDelta);
bullet.add();
}
public static void create(BulletType type, Bullet parent, float x, float y, float angle){
create(type, parent.owner, parent.team, x, y, angle);
}
public static void create(BulletType type, Bullet parent, float x, float y, float angle, float velocityScl){
create(type, parent.owner, parent.team, x, y, angle, velocityScl);
}
@Remote(called = Loc.server, in = In.entities)
public static void createBullet(BulletType type, float x, float y, float angle){
create(type, null, Team.none, x, y, angle);
}
public boolean collidesTiles(){
return type.collidesTiles;
}
public void supressCollision(){
supressCollision = true;
}
public void resetOwner(Entity entity, Team team){
this.owner = entity;
this.team = team;
}
public void scaleTime(float add){
time += add;
}
public Object getData(){
return data;
}
@Override
public float getDamage(){
if(owner instanceof Unit){
return super.getDamage() * ((Unit) owner).getDamageMultipler();
}
return super.getDamage();
}
@Override
public boolean isSyncing(){
return type.syncable;
}
@Override
public void write(DataOutput data) throws IOException{
data.writeFloat(x);
data.writeFloat(y);
data.writeFloat(velocity.x);
data.writeFloat(velocity.y);
data.writeByte(team.ordinal());
data.writeByte(type.id);
}
@Override
public void read(DataInput data, long time) throws IOException{
x = data.readFloat();
y = data.readFloat();
velocity.x = data.readFloat();
velocity.y = data.readFloat();
team = Team.all[data.readByte()];
type = BulletType.getByID(data.readByte());
}
@Override
public Team getTeam(){
return team;
}
@Override
public void draw(){
type.draw(this);
}
@Override
public float drawSize(){
return 8;
}
@Override
public boolean collides(SolidTrait other){
return type.collides && super.collides(other);
}
@Override
public void collision(SolidTrait other, float x, float y){
super.collision(other, x, y);
if(other instanceof Unit){
Unit unit = (Unit) other;
unit.getVelocity().add(vector.set(other.getX(), other.getY()).sub(x, y).setLength(type.knockback / unit.getMass()));
unit.applyEffect(type.status, type.statusIntensity);
}
}
@Override
public void update(){
super.update();
if(type.hitTiles && collidesTiles() && !supressCollision){
world.raycastEach(world.toTile(lastPosition().x), world.toTile(lastPosition().y), world.toTile(x), world.toTile(y), (x, y) -> {
Tile tile = world.tile(x, y);
if(tile == null) return false;
tile = tile.target();
if(tile.entity != null && tile.entity.collide(this) && !tile.entity.isDead() && tile.entity.tile.getTeam() != team){
tile.entity.collision(this);
if(!supressCollision){
type.hit(this);
remove();
}
return true;
}
return false;
});
}
supressCollision = false;
}
@Override
public void reset(){
super.reset();
timer.clear();
team = null;
data = null;
}
@Override
public void removed(){
Pooling.free(this);
}
@Override
public EntityGroup targetGroup(){
return bulletGroup;
}
}

View File

@@ -0,0 +1,91 @@
package io.anuke.mindustry.entities.bullet;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.content.StatusEffects;
import io.anuke.mindustry.content.fx.BulletFx;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.type.StatusEffect;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.entities.impl.BaseBulletType;
public abstract class BulletType extends BaseBulletType<Bullet> implements Content{
private static int lastid = 0;
private static Array<BulletType> types = new Array<>();
public final int id;
/**
* Knockback in velocity.
*/
public float knockback;
/**
* Whether this bullet hits tiles.
*/
public boolean hitTiles = true;
/**
* Status effect applied on hit.
*/
public StatusEffect status = StatusEffects.none;
/**
* Intensity of applied status effect in terms of duration.
*/
public float statusIntensity = 0.5f;
/**
* What fraction of armor is pierced, 0-1
*/
public float armorPierce = 0f;
/**
* Whether to sync this bullet to clients.
*/
public boolean syncable;
/**
* Whether this bullet type collides with tiles.
*/
public boolean collidesTiles = true;
/**
* Whether this bullet types collides with anything at all.
*/
public boolean collides = true;
/**
* Whether velocity is inherited from the shooter.
*/
public boolean keepVelocity = true;
public BulletType(float speed, float damage){
this.id = lastid++;
this.speed = speed;
this.damage = damage;
lifetime = 40f;
hiteffect = BulletFx.hitBulletSmall;
despawneffect = BulletFx.despawn;
types.add(this);
}
public static BulletType getByID(int id){
return types.get(id);
}
public static Array<BulletType> all(){
return types;
}
@Override
public void hit(Bullet b, float hitx, float hity){
Effects.effect(hiteffect, hitx, hity, b.angle());
}
@Override
public void despawned(Bullet b){
Effects.effect(despawneffect, b.x, b.y, b.angle());
}
@Override
public String getContentTypeName(){
return "bullettype";
}
@Override
public Array<? extends Content> getAll(){
return types;
}
}

View File

@@ -0,0 +1,53 @@
package io.anuke.mindustry.entities.bullet;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.GridPoint2;
import io.anuke.mindustry.content.fx.BulletFx;
import io.anuke.mindustry.content.fx.Fx;
import io.anuke.mindustry.entities.effect.Fire;
import io.anuke.mindustry.entities.effect.Puddle;
import io.anuke.mindustry.type.Liquid;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.util.Geometry;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.tilesize;
import static io.anuke.mindustry.Vars.world;
public class LiquidBulletType extends BulletType{
Liquid liquid;
public LiquidBulletType(Liquid liquid){
super(2.5f, 0);
this.liquid = liquid;
lifetime = 70f;
despawneffect = Fx.none;
hiteffect = BulletFx.hitLiquid;
drag = 0.01f;
knockback = 0.5f;
}
@Override
public void draw(Bullet b){
Draw.color(liquid.color, Color.WHITE, b.fout() / 100f + Mathf.randomSeedRange(b.id, 0.1f));
Fill.circle(b.x, b.y, 0.5f + b.fout() * 2.5f);
}
@Override
public void hit(Bullet b, float hitx, float hity){
Effects.effect(hiteffect, liquid.color, hitx, hity);
Puddle.deposit(world.tileWorld(hitx, hity), liquid, 5f);
if(liquid.temperature <= 0.5f && liquid.flammability < 0.3f){
float intensity = 400f;
Fire.extinguish(world.tileWorld(hitx, hity), intensity);
for(GridPoint2 p : Geometry.d4){
Fire.extinguish(world.tileWorld(hitx + p.x * tilesize, hity + p.y * tilesize), intensity);
}
}
}
}

View File

@@ -0,0 +1,24 @@
package io.anuke.mindustry.entities.bullet;
import io.anuke.mindustry.content.fx.BulletFx;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.ucore.core.Effects;
public class MissileBulletType extends BasicBulletType{
public MissileBulletType(float speed, float damage, String bulletSprite){
super(speed, damage, bulletSprite);
backColor = Palette.missileYellowBack;
frontColor = Palette.missileYellow;
homingPower = 6f;
}
@Override
public void update(Bullet b){
super.update(b);
if(b.timer.get(0, 4f)){
Effects.effect(BulletFx.missileTrail, b.x, b.y, 2f);
}
}
}

View File

@@ -1,111 +0,0 @@
package io.anuke.mindustry.entities.effect;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.entities.DestructibleEntity;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.Entity;
import io.anuke.ucore.entities.SolidEntity;
import io.anuke.ucore.function.Consumer;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Physics;
import io.anuke.ucore.util.Translator;
import static io.anuke.mindustry.Vars.*;
public class DamageArea{
private static Rectangle rect = new Rectangle();
private static Translator tr = new Translator();
//only for entities, not tiles (yet!)
public static void damageLine(Entity owner, Effect effect, float x, float y, float angle, float length, int damage){
tr.trns(angle, length);
rect.setPosition(x, y).setSize(tr.x, tr.y);
float x2 = tr.x + x, y2 = tr.y + y;
if(rect.width < 0){
rect.x += rect.width;
rect.width *= -1;
}
if(rect.height < 0){
rect.y += rect.height;
rect.height *= -1;
}
float expand = 3f;
rect.y -= expand;
rect.x -= expand;
rect.width += expand*2;
rect.height += expand*2;
Consumer<SolidEntity> cons = e -> {
if(e == owner || (e instanceof Player && ((Player)e).isAndroid)) return;
DestructibleEntity enemy = (DestructibleEntity) e;
Rectangle other = enemy.hitbox.getRect(enemy.x, enemy.y);
other.y -= expand;
other.x -= expand;
other.width += expand * 2;
other.height += expand * 2;
Vector2 vec = Physics.raycastRect(x, y, x2, y2, other);
if (vec != null) {
Effects.effect(effect, vec.x, vec.y);
enemy.damage(damage);
}
};
Entities.getNearby(enemyGroup, rect, cons);
if(state.friendlyFire) Entities.getNearby(playerGroup, rect, cons);
}
public static void damageEntities(float x, float y, float radius, int damage){
damage(true, x, y, radius, damage);
for(Player player : playerGroup.all()){
if(player.isAndroid) continue;
int amount = calculateDamage(x, y, player.x, player.y, radius, damage);
player.damage(amount);
}
}
public static void damage(boolean enemies, float x, float y, float radius, int damage){
Consumer<SolidEntity> cons = entity -> {
DestructibleEntity enemy = (DestructibleEntity)entity;
if(enemy.distanceTo(x, y) > radius || (entity instanceof Player && ((Player)entity).isAndroid)){
return;
}
int amount = calculateDamage(x, y, enemy.x, enemy.y, radius, damage);
enemy.damage(amount);
};
if(enemies){
Entities.getNearby(enemyGroup, x, y, radius*2, cons);
}else{
int trad = (int)(radius / tilesize);
for(int dx = -trad; dx <= trad; dx ++){
for(int dy= -trad; dy <= trad; dy ++){
Tile tile = world.tile(Mathf.scl2(x, tilesize) + dx, Mathf.scl2(y, tilesize) + dy);
if(tile != null && tile.entity != null && Vector2.dst(dx, dy, 0, 0) <= trad){
int amount = calculateDamage(x, y, tile.worldx(), tile.worldy(), radius, damage);
tile.entity.damage(amount);
}
}
}
Entities.getNearby(playerGroup, x, y, radius*2, cons);
}
}
static int calculateDamage(float x, float y, float tx, float ty, float radius, int damage){
float dist = Vector2.dst(x, y, tx, ty);
float scaled = 1f - dist/radius;
return (int)(damage * scaled);
}
}

View File

@@ -0,0 +1,42 @@
package io.anuke.mindustry.entities.effect;
import com.badlogic.gdx.graphics.Color;
import io.anuke.mindustry.entities.traits.BelowLiquidTrait;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.impl.TimedEntity;
import io.anuke.ucore.entities.trait.DrawTrait;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.groundEffectGroup;
/**
* Class for creating block rubble on the ground.
*/
public abstract class Decal extends TimedEntity implements BelowLiquidTrait, DrawTrait{
private static final Color color = Color.valueOf("52504e");
@Override
public float lifetime(){
return 8200f;
}
@Override
public void draw(){
Draw.color(color.r, color.g, color.b, 1f - Mathf.curve(fin(), 0.98f));
drawDecal();
Draw.color();
}
@Override
public EntityGroup targetGroup(){
return groundEffectGroup;
}
@Override
public float fin() {
return time / lifetime();
}
abstract void drawDecal();
}

View File

@@ -1,120 +0,0 @@
package io.anuke.mindustry.entities.effect;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.graphics.Fx;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.types.PowerAcceptor;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.entities.TimedEntity;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Translator;
import static io.anuke.mindustry.Vars.tilesize;
import static io.anuke.mindustry.Vars.world;
public class EMP extends TimedEntity{
static final int maxTargets = 8;
static Array<Tile> array = new Array<>();
static Translator tr = new Translator();
int radius = 4;
int damage = 6;
Array<Tile> targets = new Array<>(maxTargets);
public EMP(float x, float y, int damage){
this.damage = damage;
set(x, y);
lifetime = 30f;
int worldx = Mathf.scl2(x, tilesize);
int worldy = Mathf.scl2(y, tilesize);
array.clear();
for(int dx = -radius; dx <= radius; dx ++){
for(int dy = -radius; dy <= radius; dy ++){
if(Vector2.dst(dx, dy, 0, 0) < radius){
Tile tile = world.tile(worldx + dx, worldy + dy);
if(tile != null && tile.block().destructible){
array.add(tile);
}
}
}
}
array.shuffle();
for(int i = 0; i < array.size && i < maxTargets; i ++){
Tile tile = array.get(i);
targets.add(tile);
if(tile != null && tile.block() instanceof PowerAcceptor){
PowerAcceptor p = (PowerAcceptor)tile.block();
p.setPower(tile, 0f);
tile.entity.damage((int)(damage*2f)); //extra damage
}
if(tile == null) continue;
//entity may be null here, after the block is dead!
Effects.effect(Fx.empspark, tile.worldx(), tile.worldy());
if(tile.entity != null) tile.entity.damage(damage);
}
}
@Override
public void drawOver(){
Draw.color(Color.SKY);
for(int i = 0; i < targets.size; i ++){
Tile target = targets.get(i);
drawLine(target.worldx(), target.worldy());
float rad = 5f*fout();
Draw.rect("circle", target.worldx(), target.worldy(), rad, rad);
}
for(int i = 0; i < 14 - targets.size; i ++){
tr.trns(Mathf.randomSeed(i + id*77)*360f, radius * tilesize);
drawLine(x + tr.x, y + tr.y);
}
Lines.stroke(fout()*2f);
Lines.poly(x, y, 34, radius * tilesize);
Draw.reset();
}
private void drawLine(float targetx, float targety){
int joints = 3;
float r = 3f;
float lastx = x, lasty = y;
for(int seg = 0; seg < joints; seg ++){
float dx = Mathf.range(r),
dy = Mathf.range(r);
float frac = (seg+1f)/joints;
float tx = (targetx - x)*frac + x + dx,
ty = (targety - y)*frac + y + dy;
drawLaser(lastx, lasty, tx, ty);
lastx = tx;
lasty = ty;
}
}
private void drawLaser(float x, float y, float x2, float y2){
Lines.stroke(fout() * 2f);
Lines.line(x, y, x2, y2);
}
}

View File

@@ -0,0 +1,211 @@
package io.anuke.mindustry.entities.effect;
import com.badlogic.gdx.math.GridPoint2;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.Pool.Poolable;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.mindustry.content.StatusEffects;
import io.anuke.mindustry.content.bullets.TurretBullets;
import io.anuke.mindustry.content.fx.EnvironmentFx;
import io.anuke.mindustry.entities.Damage;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.entities.traits.SaveTrait;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.gen.CallEntity;
import io.anuke.mindustry.net.In;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.impl.TimedEntity;
import io.anuke.ucore.util.Geometry;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Pooling;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import static io.anuke.mindustry.Vars.*;
public class Fire extends TimedEntity implements SaveTrait, SyncTrait, Poolable{
private static final IntMap<Fire> map = new IntMap<>();
private static final float baseLifetime = 1000f;
private int loadedPosition = -1;
private Tile tile;
private Block block;
private float baseFlammability = -1, puddleFlammability;
private float lifetime;
/**
* Deserialization use only!
*/
public Fire(){
}
/**
* Start a fire on the tile. If there already is a file there, refreshes its lifetime.
*/
public static void create(Tile tile){
if(Net.client() || tile == null) return; //not clientside.
Fire fire = map.get(tile.packedPosition());
if(fire == null){
fire = Pooling.obtain(Fire.class);
fire.tile = tile;
fire.lifetime = baseLifetime;
fire.set(tile.worldx(), tile.worldy());
fire.add();
map.put(tile.packedPosition(), fire);
}else{
fire.lifetime = baseLifetime;
fire.time = 0f;
}
}
/**
* Attempts to extinguish a fire by shortening its life. If there is no fire here, does nothing.
*/
public static void extinguish(Tile tile, float intensity){
if(tile != null && map.containsKey(tile.packedPosition())){
map.get(tile.packedPosition()).time += intensity * Timers.delta();
}
}
@Remote(called = Loc.server, in = In.entities)
public static void onFireRemoved(int fireid){
fireGroup.removeByID(fireid);
}
@Override
public float lifetime(){
return lifetime;
}
@Override
public void update(){
if(Mathf.chance(0.1 * Timers.delta())){
Effects.effect(EnvironmentFx.fire, x + Mathf.range(4f), y + Mathf.range(4f));
}
if(Mathf.chance(0.05 * Timers.delta())){
Effects.effect(EnvironmentFx.smoke, x + Mathf.range(4f), y + Mathf.range(4f));
}
if(Net.client()){
return;
}
time = Mathf.clamp(time + Timers.delta(), 0, lifetime());
if(time >= lifetime() || tile == null){
CallEntity.onFireRemoved(getID());
remove();
return;
}
TileEntity entity = tile.target().entity;
boolean damage = entity != null;
float flammability = baseFlammability + puddleFlammability;
if(!damage && flammability <= 0){
time += Timers.delta() * 8;
}
if(baseFlammability < 0 || block != tile.block()){
baseFlammability = tile.block().getFlammability(tile);
block = tile.block();
}
if(damage){
lifetime += Mathf.clamp(flammability / 8f, 0f, 0.6f) * Timers.delta();
}
if(flammability > 1f && Mathf.chance(0.03 * Timers.delta() * Mathf.clamp(flammability / 5f, 0.3f, 2f))){
GridPoint2 p = Mathf.select(Geometry.d4);
Tile other = world.tile(tile.x + p.x, tile.y + p.y);
create(other);
if(Mathf.chance(0.05 * Timers.delta() * Mathf.clamp(flammability / 10.0))){
CallEntity.createBullet(TurretBullets.fireball, x, y, Mathf.random(360f));
}
}
if(Mathf.chance(0.1 * Timers.delta())){
Puddle p = Puddle.getPuddle(tile);
if(p != null){
puddleFlammability = p.getFlammability() / 3f;
}else{
puddleFlammability = 0;
}
if(damage){
entity.damage(0.4f);
}
Damage.damageUnits(null, tile.worldx(), tile.worldy(), tilesize, 3f, unit -> !unit.isFlying(), unit -> unit.applyEffect(StatusEffects.burning, 0.8f));
}
}
@Override
public void writeSave(DataOutput stream) throws IOException{
stream.writeInt(tile.packedPosition());
stream.writeFloat(lifetime);
stream.writeFloat(time);
}
@Override
public void readSave(DataInput stream) throws IOException{
this.loadedPosition = stream.readInt();
this.lifetime = stream.readFloat();
this.time = stream.readFloat();
add();
}
@Override
public void write(DataOutput data) throws IOException{
data.writeFloat(x);
data.writeFloat(y);
}
@Override
public void read(DataInput data, long time) throws IOException{
x = data.readFloat();
y = data.readFloat();
}
@Override
public void reset(){
loadedPosition = -1;
tile = null;
baseFlammability = -1;
puddleFlammability = 0f;
}
@Override
public void added(){
if(loadedPosition != -1){
map.put(loadedPosition, this);
tile = world.tile(loadedPosition);
set(tile.worldx(), tile.worldy());
}
}
@Override
public void removed(){
if(tile != null){
map.remove(tile.packedPosition());
}
reset();
}
@Override
public EntityGroup targetGroup(){
return fireGroup;
}
}

View File

@@ -0,0 +1,90 @@
package io.anuke.mindustry.entities.effect;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.impl.EffectEntity;
import io.anuke.ucore.function.EffectRenderer;
import io.anuke.ucore.util.Mathf;
/**
* A ground effect contains an effect that is rendered on the ground layer as opposed to the top layer.
*/
public class GroundEffectEntity extends EffectEntity{
private boolean once;
@Override
public void update(){
GroundEffect effect = (GroundEffect) this.effect;
if(effect.isStatic){
time += Timers.delta();
time = Mathf.clamp(time, 0, effect.staticLife);
if(!once && time >= lifetime()){
once = true;
time = 0f;
Tile tile = Vars.world.tileWorld(x, y);
if(tile != null && tile.floor().isLiquid){
remove();
}
}else if(once && time >= effect.staticLife){
remove();
}
}else{
super.update();
}
}
@Override
public void draw(){
GroundEffect effect = (GroundEffect) this.effect;
if(once && effect.isStatic)
Effects.renderEffect(id, effect, color, lifetime(), rotation, x, y, data);
else
Effects.renderEffect(id, effect, color, time, rotation, x, y, data);
}
@Override
public void reset(){
super.reset();
once = false;
}
/**
* An effect that is rendered on the ground layer as opposed to the top layer.
*/
public static class GroundEffect extends Effect{
/**
* How long this effect stays on the ground when static.
*/
public final float staticLife;
/**
* If true, this effect will stop and lie on the ground for a specific duration,
* after its initial lifetime is over.
*/
public final boolean isStatic;
public GroundEffect(float life, float staticLife, EffectRenderer draw){
super(life, draw);
this.staticLife = staticLife;
this.isStatic = true;
}
public GroundEffect(boolean isStatic, float life, EffectRenderer draw){
super(life, draw);
this.staticLife = 0f;
this.isStatic = isStatic;
}
public GroundEffect(float life, EffectRenderer draw){
super(life, draw);
this.staticLife = 0f;
this.isStatic = false;
}
}
}

View File

@@ -0,0 +1,246 @@
package io.anuke.mindustry.entities.effect;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Pool.Poolable;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.mindustry.content.fx.UnitFx;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.entities.Unit;
import io.anuke.mindustry.entities.traits.SaveTrait;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.entities.traits.TargetTrait;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.gen.CallEntity;
import io.anuke.mindustry.net.In;
import io.anuke.mindustry.net.Interpolator;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.impl.SolidEntity;
import io.anuke.ucore.entities.trait.DrawTrait;
import io.anuke.ucore.entities.trait.SolidTrait;
import io.anuke.ucore.entities.trait.TimeTrait;
import io.anuke.ucore.entities.trait.VelocityTrait;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.util.Mathf;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import static io.anuke.mindustry.Vars.*;
public class ItemDrop extends SolidEntity implements SaveTrait, SyncTrait, DrawTrait, VelocityTrait, TimeTrait, TargetTrait, Poolable{
private static final float sinkLifetime = 80f;
private Interpolator interpolator = new Interpolator();
private Item item;
private int amount;
private Vector2 velocity = new Vector2();
private float time;
private float sinktime;
/**
* Internal use only!
*/
public ItemDrop(){
hitbox.setSize(5f);
hitboxTile.setSize(5f);
}
public static ItemDrop create(Item item, int amount, float x, float y, float angle){
ItemDrop drop = new ItemDrop();
drop.item = item;
drop.amount = amount;
drop.velocity.set(4f, 0f).rotate(angle);
drop.setNet(x, y);
drop.add();
return drop;
}
public static void create(Item item, int amount, float x, float y, float velocityX, float velocityY){
create(item, amount, x, y, 0).getVelocity().set(velocityX, velocityY);
}
@Remote(called = Loc.server, in = In.entities)
public static void onPickup(int itemid){
ItemDrop drop = itemGroup.getByID(itemid);
if(drop != null){
Effects.effect(UnitFx.pickup, drop);
}
itemGroup.removeByID(itemid);
if(Net.client()){
netClient.addRemovedEntity(itemid);
}
}
public Item getItem(){
return item;
}
public int getAmount(){
return amount;
}
@Override
public boolean isDead(){
return !isAdded();
}
@Override
public Team getTeam(){
return Team.none;
}
@Override
public float lifetime(){
return 60 * 60;
}
@Override
public void time(float time){
this.time = time;
}
@Override
public float time(){
return time;
}
@Override
public Vector2 getVelocity(){
return velocity;
}
@Override
public boolean collides(SolidTrait other){
return other instanceof Player && time > 20f;
}
@Override
public void collision(SolidTrait other, float x, float y){
Unit player = (Unit) other;
if(player.inventory.canAcceptItem(item, 1)){
int used = Math.min(amount, player.inventory.capacity() - player.inventory.getItem().amount);
player.inventory.addItem(item, used);
amount -= used;
if(amount <= 0){
CallEntity.onPickup(getID());
}
}
}
@Override
public void draw(){
float size = itemSize * (1f - sinktime / sinkLifetime) * (1f - Mathf.curve(fin(), 0.98f));
Tile tile = world.tileWorld(x, y);
Draw.color(Color.WHITE, tile == null || !tile.floor().isLiquid ? Color.WHITE : tile.floor().liquidColor, sinktime / sinkLifetime);
Draw.rect(item.region, x, y, size, size);
int stored = Mathf.clamp(amount / 6, 1, 8);
for(int i = 0; i < stored; i++){
float px = stored == 1 ? 0 : Mathf.randomSeedRange(i + 1, 4f);
float py = stored == 1 ? 0 : Mathf.randomSeedRange(i + 2, 4f);
Draw.rect(item.region, x + px, y + py, size, size);
}
Draw.color();
}
@Override
public void update(){
if(Net.client()){
interpolate();
}else{
updateVelocity(0.2f);
updateTime();
if(time >= lifetime()){
CallEntity.onPickup(getID());
}
}
Tile tile = world.tileWorld(x, y);
if(tile != null && tile.solid()){
CallEntity.onPickup(getID());
}
if(tile != null && tile.floor().isLiquid){
sinktime += Timers.delta();
if(Mathf.chance(0.04 * Timers.delta())){
Effects.effect(tile.floor().drownUpdateEffect, tile.floor().liquidColor, x, y);
}
if(sinktime >= sinkLifetime){
remove();
}
}else{
sinktime = 0f;
}
}
@Override
public void reset(){
time = 0f;
interpolator.reset();
}
@Override
public Interpolator getInterpolator(){
return interpolator;
}
@Override
public float drawSize(){
return 10;
}
@Override
public EntityGroup targetGroup(){
return itemGroup;
}
@Override
public void writeSave(DataOutput data) throws IOException{
data.writeFloat(x);
data.writeFloat(y);
data.writeByte(item.id);
data.writeShort(amount);
}
@Override
public void readSave(DataInput data) throws IOException{
x = data.readFloat();
y = data.readFloat();
item = Item.getByID(data.readByte());
amount = data.readShort();
add();
}
@Override
public void write(DataOutput data) throws IOException{
data.writeFloat(x);
data.writeFloat(y);
data.writeByte(item.id);
data.writeShort(amount);
}
@Override
public void read(DataInput data, long time) throws IOException{
interpolator.read(x, y, data.readFloat(), data.readFloat(), time);
item = Item.getByID(data.readByte());
amount = data.readShort();
}
}

View File

@@ -0,0 +1,142 @@
package io.anuke.mindustry.entities.effect;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.math.Vector2;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.mindustry.entities.Unit;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.net.In;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.impl.TimedEntity;
import io.anuke.ucore.entities.trait.DrawTrait;
import io.anuke.ucore.entities.trait.PosTrait;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Pooling;
import static io.anuke.mindustry.Vars.effectGroup;
import static io.anuke.mindustry.Vars.threads;
public class ItemTransfer extends TimedEntity implements DrawTrait{
private Vector2 from = new Vector2();
private Vector2 current = new Vector2();
private Vector2 tovec = new Vector2();
private Item item;
private float seed;
private PosTrait to;
private Runnable done;
public ItemTransfer(){
}
@Remote(in = In.entities, called = Loc.server, unreliable = true)
public static void transferAmmo(Item item, float x, float y, Unit to){
if(to == null) return;
to.addAmmo(item);
create(item, x, y, to, () -> {
});
}
@Remote(in = In.entities, called = Loc.server, unreliable = true)
public static void transferItemEffect(Item item, float x, float y, Unit to){
if(to == null) return;
create(item, x, y, to, () -> {
});
}
@Remote(in = In.entities, called = Loc.server, unreliable = true)
public static void transferItemToUnit(Item item, float x, float y, Unit to){
if(to == null) return;
create(item, x, y, to, () -> to.inventory.addItem(item, 1));
}
@Remote(in = In.entities, called = Loc.server)
public static void transferItemTo(Item item, int amount, float x, float y, Tile tile){
if(tile == null) return;
for(int i = 0; i < Mathf.clamp(amount / 3, 1, 8); i++){
Timers.run(i * 3, () -> create(item, x, y, tile, () -> {
}));
}
tile.entity.items.add(item, amount);
}
public static void create(Item item, float fromx, float fromy, PosTrait to, Runnable done){
ItemTransfer tr = Pooling.obtain(ItemTransfer.class);
tr.item = item;
tr.from.set(fromx, fromy);
tr.to = to;
tr.done = done;
tr.seed = Mathf.range(1f);
tr.add();
}
@Override
public float lifetime(){
return 60;
}
@Override
public void reset(){
super.reset();
item = null;
to = null;
done = null;
from.setZero();
current.setZero();
tovec.setZero();
}
@Override
public void removed(){
if(done != null){
threads.run(done);
}
Pooling.free(this);
}
@Override
public void update(){
if(to == null){
remove();
return;
}
super.update();
current.set(from).interpolate(tovec.set(to.getX(), to.getY()), fin(), Interpolation.pow3);
current.add(tovec.set(to.getX(), to.getY()).sub(from).nor().rotate90(1).scl(seed * fslope() * 10f));
set(current.x, current.y);
}
@Override
public void draw(){
float length = fslope() * 6f;
float angle = current.set(x, y).sub(from).angle();
Draw.color(Palette.accent);
Lines.stroke(fslope() * 2f);
Lines.circle(x, y, fslope() * 2f);
Lines.lineAngleCenter(x, y, angle, length);
Lines.lineAngle(x, y, angle, fout() * 6f);
Draw.color(item.color);
Fill.circle(x, y, fslope() * 1.5f);
Draw.reset();
}
@Override
public EntityGroup targetGroup(){
return effectGroup;
}
@Override
public float fin(){
return time() / lifetime();
}
}

View File

@@ -0,0 +1,175 @@
package io.anuke.mindustry.entities.effect;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Pool.Poolable;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.mindustry.content.StatusEffects;
import io.anuke.mindustry.entities.Units;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.gen.CallEntity;
import io.anuke.mindustry.graphics.Palette;
import io.anuke.mindustry.net.In;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Effects.Effect;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.impl.TimedEntity;
import io.anuke.ucore.entities.trait.DrawTrait;
import io.anuke.ucore.entities.trait.SolidTrait;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Pooling;
import io.anuke.ucore.util.SeedRandom;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import static io.anuke.mindustry.Vars.bulletGroup;
//TODO utterly broken
public class Lightning extends TimedEntity implements Poolable, DrawTrait, SyncTrait{
private static Array<SolidTrait> entities = new Array<>();
private static Rectangle rect = new Rectangle();
private static Rectangle hitrect = new Rectangle();
private static int lastSeed = 0;
private static float angle;
private static float wetDamageMultiplier = 2;
private Array<Vector2> lines = new Array<>();
private Color color = Palette.lancerLaser;
private SeedRandom random = new SeedRandom();
/**
* For pooling use only. Do not call directly!
*/
public Lightning(){
}
/**
* Create a lighting branch at a location. Use Team.none to damage everyone.
*/
public static void create(Team team, Effect effect, Color color, float damage, float x, float y, float targetAngle, int length){
CallEntity.createLighting(lastSeed++, team, effect, color, damage, x, y, targetAngle, length);
}
@Remote(called = Loc.server, in = In.entities)
public static void createLighting(int seed, Team team, Effect effect, Color color, float damage, float x, float y, float targetAngle, int length){
Lightning l = Pooling.obtain(Lightning.class);
l.x = x;
l.y = y;
l.random.setSeed(seed);
l.color = color;
float step = 3f;
float range = 6f;
float attractRange = 20f;
angle = targetAngle;
entities.clear();
Units.getNearbyEnemies(team, rect, entities::add);
for(int i = 0; i < length; i++){
l.lines.add(new Vector2(x, y));
float fx = x, fy = y;
float x2 = x + Angles.trnsx(angle, step);
float y2 = y + Angles.trnsy(angle, step);
float fangle = angle;
angle += Mathf.range(30f);
rect.setSize(attractRange).setCenter(x, y);
Units.getNearbyEnemies(team, rect, entity -> {
float dst = entity.distanceTo(x2, y2);
if(dst < attractRange){
angle = Mathf.slerp(angle, Angles.angle(x2, y2, entity.x, entity.y), (attractRange - dst) / attractRange / 4f);
}
entity.getHitbox(hitrect);
hitrect.x -= range / 2f;
hitrect.y -= range / 2f;
hitrect.width += range / 2f;
hitrect.height += range / 2f;
if(hitrect.contains(x2, y2) || hitrect.contains(fx, fy)){
float result = damage;
if(entity.hasEffect(StatusEffects.wet))
result = (result * wetDamageMultiplier);
entity.damage(result);
Effects.effect(effect, x2, y2, fangle);
}
});
if(l.random.chance(0.1f)){
createLighting(l.random.nextInt(), team, effect, color, damage, x2, y2, angle + l.random.range(100f), length / 3);
}
x = x2;
y = y2;
}
l.lines.add(new Vector2(x, y));
l.add();
}
@Override
public boolean isSyncing(){
return false;
}
@Override
public void write(DataOutput data) throws IOException{
}
@Override
public void read(DataInput data, long time) throws IOException{
}
@Override
public float lifetime(){
return 10;
}
@Override
public void reset(){
color = Palette.lancerLaser;
lines.clear();
}
@Override
public void removed(){
Pooling.free(this);
}
@Override
public void draw(){
float lx = x, ly = y;
Draw.color(color, Color.WHITE, fin());
for(int i = 0; i < lines.size; i++){
Vector2 v = lines.get(i);
Lines.stroke(fout() * 3f + 1f - (float) i / lines.size);
Lines.line(lx, ly, v.x, v.y);
lx = v.x;
ly = v.y;
}
Draw.color();
}
@Override
public EntityGroup targetGroup(){
return bulletGroup;
}
}

View File

@@ -0,0 +1,314 @@
package io.anuke.mindustry.entities.effect;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.GridPoint2;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.Pool.Poolable;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.content.blocks.Blocks;
import io.anuke.mindustry.content.bullets.TurretBullets;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.content.fx.EnvironmentFx;
import io.anuke.mindustry.entities.Units;
import io.anuke.mindustry.entities.traits.SaveTrait;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.gen.CallEntity;
import io.anuke.mindustry.net.In;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.type.Liquid;
import io.anuke.mindustry.world.Tile;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.entities.impl.BaseEntity;
import io.anuke.ucore.entities.trait.DrawTrait;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Fill;
import io.anuke.ucore.graphics.Hue;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Geometry;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Pooling;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import static io.anuke.mindustry.Vars.puddleGroup;
import static io.anuke.mindustry.Vars.world;
public class Puddle extends BaseEntity implements SaveTrait, Poolable, DrawTrait, SyncTrait{
private static final IntMap<Puddle> map = new IntMap<>();
private static final float maxLiquid = 70f;
private static final int maxGeneration = 2;
private static final Color tmp = new Color();
private static final Rectangle rect = new Rectangle();
private static final Rectangle rect2 = new Rectangle();
private static int seeds;
private int loadedPosition = -1;
private float updateTime;
private Tile tile;
private Liquid liquid;
private float amount, targetAmount;
private float accepting;
private byte generation;
/**
* Deserialization use only!
*/
public Puddle(){
}
/**
* Deposists a puddle between tile and source.
*/
public static void deposit(Tile tile, Tile source, Liquid liquid, float amount){
deposit(tile, source, liquid, amount, 0);
}
/**
* Deposists a puddle at a tile.
*/
public static void deposit(Tile tile, Liquid liquid, float amount){
deposit(tile, tile, liquid, amount, 0);
}
/**
* Returns the puddle on the specified tile. May return null.
*/
public static Puddle getPuddle(Tile tile){
return map.get(tile.packedPosition());
}
private static void deposit(Tile tile, Tile source, Liquid liquid, float amount, int generation){
if(tile.floor().isLiquid && !canStayOn(liquid, tile.floor().liquidDrop)){
reactPuddle(tile.floor().liquidDrop, liquid, amount, tile,
(tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f);
if(generation == 0 && Timers.get(tile, "ripple", 50)){
Effects.effect(BlockFx.ripple, tile.floor().liquidDrop.color,
(tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f);
}
return;
}
Puddle p = map.get(tile.packedPosition());
if(p == null){
if(Net.client()) return; //not clientside.
Puddle puddle = Pooling.obtain(Puddle.class);
puddle.tile = tile;
puddle.liquid = liquid;
puddle.amount = amount;
puddle.generation = (byte) generation;
puddle.set((tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f);
puddle.add();
map.put(tile.packedPosition(), puddle);
}else if(p.liquid == liquid){
p.accepting = Math.max(amount, p.accepting);
if(generation == 0 && Timers.get(p, "ripple2", 50) && p.amount >= maxLiquid / 2f){
Effects.effect(BlockFx.ripple, p.liquid.color, (tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f);
}
}else{
p.amount -= reactPuddle(p.liquid, liquid, amount, p.tile, p.x, p.y);
}
}
/**
* Returns whether the first liquid can 'stay' on the second one.
* Currently, the only place where this can happen is oil on water.
*/
private static boolean canStayOn(Liquid liquid, Liquid other){
return liquid == Liquids.oil && other == Liquids.water;
}
/**
* Reacts two liquids together at a location.
*/
private static float reactPuddle(Liquid dest, Liquid liquid, float amount, Tile tile, float x, float y){
if((dest.flammability > 0.3f && liquid.temperature > 0.7f) ||
(liquid.flammability > 0.3f && dest.temperature > 0.7f)){ //flammable liquid + hot liquid
Fire.create(tile);
if(Mathf.chance(0.006 * amount)){
CallEntity.createBullet(TurretBullets.fireball, x, y, Mathf.random(360f));
}
}else if(dest.temperature > 0.7f && liquid.temperature < 0.55f){ //cold liquid poured onto hot puddle
if(Mathf.chance(0.5f * amount)){
Effects.effect(EnvironmentFx.steam, x, y);
}
return -0.1f * amount;
}else if(liquid.temperature > 0.7f && dest.temperature < 0.55f){ //hot liquid poured onto cold puddle
if(Mathf.chance(0.8f * amount)){
Effects.effect(EnvironmentFx.steam, x, y);
}
return -0.4f * amount;
}
return 0f;
}
@Remote(called = Loc.server, in = In.entities)
public static void onPuddleRemoved(int puddleid){
puddleGroup.removeByID(puddleid);
}
public float getFlammability(){
return liquid.flammability * amount;
}
@Override
public void update(){
//no updating happens clientside
if(Net.client()){
amount = Mathf.lerpDelta(amount, targetAmount, 0.15f);
}else{
//update code
float addSpeed = accepting > 0 ? 3f : 0f;
amount -= Timers.delta() * (1f - liquid.viscosity) / (5f + addSpeed);
amount += accepting;
accepting = 0f;
if(amount >= maxLiquid / 1.5f && generation < maxGeneration){
float deposited = Math.min((amount - maxLiquid / 1.5f) / 4f, 0.3f) * Timers.delta();
for(GridPoint2 point : Geometry.d4){
Tile other = world.tile(tile.x + point.x, tile.y + point.y);
if(other.block() == Blocks.air && other.cliffs == 0){
deposit(other, tile, liquid, deposited, generation + 1);
amount -= deposited / 2f; //tweak to speed up/slow down puddle propagation
}
}
}
amount = Mathf.clamp(amount, 0, maxLiquid);
if(amount <= 0f){
CallEntity.onPuddleRemoved(getID());
}
}
//effects-only code
if(amount >= maxLiquid / 2f && updateTime <= 0f){
Units.getNearby(rect.setSize(Mathf.clamp(amount / (maxLiquid / 1.5f)) * 10f).setCenter(x, y), unit -> {
if(unit.isFlying()) return;
unit.getHitbox(rect2);
if(!rect.overlaps(rect2)) return;
unit.applyEffect(liquid.effect, 0.5f);
if(unit.getVelocity().len() > 0.1){
Effects.effect(BlockFx.ripple, liquid.color, unit.x, unit.y);
}
});
if(liquid.temperature > 0.7f && tile.entity != null && Mathf.chance(0.3 * Timers.delta())){
Fire.create(tile);
}
updateTime = 20f;
}
updateTime -= Timers.delta();
}
@Override
public void draw(){
seeds = id;
boolean onLiquid = tile.floor().isLiquid;
float f = Mathf.clamp(amount / (maxLiquid / 1.5f));
float smag = onLiquid ? 0.8f : 0f;
float sscl = 20f;
Draw.color(Hue.shift(tmp.set(liquid.color), 2, -0.05f));
Fill.circle(x + Mathf.sin(Timers.time() + seeds * 532, sscl, smag), y + Mathf.sin(Timers.time() + seeds * 53, sscl, smag), f * 8f);
Angles.randLenVectors(id, 3, f * 6f, (ex, ey) -> {
Fill.circle(x + ex + Mathf.sin(Timers.time() + seeds * 532, sscl, smag),
y + ey + Mathf.sin(Timers.time() + seeds * 53, sscl, smag), f * 5f);
seeds++;
});
Draw.color();
}
@Override
public float drawSize(){
return 20;
}
@Override
public void writeSave(DataOutput stream) throws IOException{
stream.writeInt(tile.packedPosition());
stream.writeFloat(x);
stream.writeFloat(y);
stream.writeByte(liquid.id);
stream.writeFloat(amount);
stream.writeByte(generation);
}
@Override
public void readSave(DataInput stream) throws IOException{
this.loadedPosition = stream.readInt();
this.x = stream.readFloat();
this.y = stream.readFloat();
this.liquid = Liquid.getByID(stream.readByte());
this.amount = stream.readFloat();
this.generation = stream.readByte();
add();
}
@Override
public void reset(){
loadedPosition = -1;
tile = null;
liquid = null;
amount = 0;
generation = 0;
accepting = 0;
}
@Override
public void added(){
if(loadedPosition != -1){
map.put(loadedPosition, this);
tile = world.tile(loadedPosition);
}
}
@Override
public void removed(){
map.remove(tile.packedPosition());
reset();
}
@Override
public void write(DataOutput data) throws IOException{
data.writeFloat(x);
data.writeFloat(y);
data.writeByte(liquid.id);
data.writeShort((short) (amount * 4));
data.writeInt(tile.packedPosition());
}
@Override
public void read(DataInput data, long time) throws IOException{
x = data.readFloat();
y = data.readFloat();
liquid = Liquid.getByID(data.readByte());
targetAmount = data.readShort() / 4f;
tile = world.tile(data.readInt());
map.put(tile.packedPosition(), this);
}
@Override
public EntityGroup targetGroup(){
return puddleGroup;
}
}

Some files were not shown because too many files have changed in this diff Show More