Merge branch 'master' into GH-v80

This commit is contained in:
(G_H)
2019-06-23 21:07:06 +08:00
committed by GitHub
1849 changed files with 96454 additions and 39768 deletions

View File

@@ -1,42 +1,69 @@
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.arc.*;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Log;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.core.*;
import io.anuke.mindustry.io.BlockLoader;
import io.anuke.mindustry.game.EventType.GameLoadEvent;
import io.anuke.mindustry.io.BundleLoader;
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 ApplicationCore{
@Override
public void init(){
debug = Platform.instance.isDebug();
@Override
public void setup(){
Time.setDeltaProvider(() -> {
float result = Core.graphics.getDeltaTime() * 60f;
return (Float.isNaN(result) || Float.isInfinite(result)) ? 1f : Mathf.clamp(result, 0.0001f, 60f / 10f);
});
Log.setUseColors(false);
BundleLoader.load();
BlockLoader.load();
Time.mark();
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());
}
Vars.init();
@Override
public void render(){
super.render();
threads.handleRender();
}
Log.setUseColors(false);
BundleLoader.load();
content.load();
content.loadColors();
add(logic = new Logic());
add(world = new World());
add(control = new Control());
add(renderer = new Renderer());
add(ui = new UI());
add(netServer = new NetServer());
add(netClient = new NetClient());
}
@Override
public void init(){
super.init();
Log.info("Time to load [total]: {0}", Time.elapsed());
Events.fire(new GameLoadEvent());
}
@Override
public void update(){
long lastFrameTime = Time.nanos();
super.update();
int fpsCap = Core.settings.getInt("fpscap", 125);
if(fpsCap <= 120){
long target = (1000 * 1000000) / fpsCap; //target in nanos
long elapsed = Time.timeSinceNanos(lastFrameTime);
if(elapsed < target){
try{
Thread.sleep((target - elapsed) / 1000000, (int)((target - elapsed) % 1000000));
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
}

View File

@@ -1,157 +1,222 @@
package io.anuke.mindustry;
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.arc.Application.ApplicationType;
import io.anuke.arc.Core;
import io.anuke.arc.files.FileHandle;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.util.Structs;
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.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.ucore.entities.Entities;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.scene.ui.layout.Unit;
import io.anuke.ucore.util.OS;
import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.bullet.Bullet;
import io.anuke.mindustry.entities.effect.Fire;
import io.anuke.mindustry.entities.effect.Puddle;
import io.anuke.mindustry.entities.impl.EffectEntity;
import io.anuke.mindustry.entities.traits.DrawTrait;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.gen.Serialization;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.world.blocks.defense.ForceProjector.ShieldEntity;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Locale;
@SuppressWarnings("unchecked")
public class Vars{
/** Whether to load locales.*/
public static boolean loadLocales = true;
/** IO buffer size. */
public static final int bufferSize = 8192;
/** global charset */
public static final Charset charset = Charset.forName("UTF-8");
/** main application name, capitalized */
public static final String appName = "Mindustry";
/** URL for itch.io donations. */
public static final String donationURL = "https://anuke.itch.io/mindustry/purchase";
/** URL for discord invite. */
public static final String discordURL = "https://discord.gg/mindustry";
/** URL for Github API for releases */
public static final String releasesURL = "https://api.github.com/repos/Anuken/Mindustry/releases";
/** URL for sending crash reports to */
public static final String crashReportURL = "http://mins.us.to/report";
/** maximum distance between mine and core that supports automatic transferring */
public static final float mineTransferRange = 220f;
/** team of the player by default */
public static final Team defaultTeam = Team.blue;
/** team of the enemy in waves/sectors */
public static final Team waveTeam = Team.red;
/** whether to enable editing of units in the editor */
public static final boolean enableUnitEditing = false;
/** max chat message length */
public static final int maxTextLength = 150;
/** max player name length in bytes */
public static final int maxNameLength = 40;
/** displayed item size when ingame, TODO remove. */
public static final float itemSize = 5f;
/** extra padding around the world; units outside this bound will begin to self-destruct. */
public static final float worldBounds = 100f;
/** default size of UI icons.*/
public static final int iconsize = 48;
/** size of UI icons (small)*/
public static final int iconsizesmall = 32;
/** units outside of this bound will simply die instantly */
public static final float finalWorldBounds = worldBounds + 500;
/** ticks spent out of bound until self destruct. */
public static final float boundsCountdown = 60 * 7;
/** for map generator dialog */
public static boolean updateEditorOnChange = false;
/** size of tiles in units */
public static final int tilesize = 8;
/** all choosable player colors in join/host dialog */
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"),
};
/** default server port */
public static final int port = 6567;
/** multicast discovery port.*/
public static final int multicastPort = 20151;
/** multicast group for discovery.*/
public static final String multicastGroup = "227.2.7.7";
/** if true, UI is not drawn */
public static boolean disableUI;
/** if true, game is set up in mobile mode, even on desktop. used for debugging */
public static boolean testMobile;
/** whether the game is running on a mobile device */
public static boolean mobile;
/** whether the game is running on an iOS device */
public static boolean ios;
/** whether the game is running on an Android device */
public static boolean android;
/** whether the game is running on a headless server */
public static boolean headless;
/** application data directory, equivalent to {@link io.anuke.arc.Settings#getDataDirectory()} */
public static FileHandle dataDirectory;
/** data subdirectory used for screenshots */
public static FileHandle screenshotDirectory;
/** data subdirectory used for custom mmaps */
public static FileHandle customMapDirectory;
/** tmp subdirectory for map conversion */
public static FileHandle tmpDirectory;
/** data subdirectory used for saves */
public static FileHandle saveDirectory;
/** old map file extension, for conversion */
public static final String oldMapExtension = "mmap";
/** map file extension */
public static final String mapExtension = "msav";
/** save file extension */
public static final String saveExtension = "msav";
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";
/** list of all locales that can be switched to */
public static Locale[] locales;
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;
//whether to show block debug
public static boolean showBlockDebug = false;
public static ContentLoader content;
public static GameState state;
public static GlobalData data;
public static EntityCollisions collisions;
public static DefaultWaves defaultWaves;
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 EntityGroup<Player> playerGroup;
public static EntityGroup<TileEntity> tileGroup;
public static EntityGroup<Bullet> bulletGroup;
public static EntityGroup<EffectEntity> effectGroup;
public static EntityGroup<DrawTrait> groundEffectGroup;
public static EntityGroup<ShieldEntity> shieldGroup;
public static EntityGroup<Puddle> puddleGroup;
public static EntityGroup<Fire> fireGroup;
public static EntityGroup<BaseUnit>[] unitGroups;
public static float baseControllerSpeed = 11f;
/** all local players, currently only has one player. may be used for local co-op in the future */
public static Player player;
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 void init(){
Serialization.init();
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")};
if(loadLocales){
//load locales
String[] stra = Core.files.internal("locales").readString().split("\n");
locales = new Locale[stra.length];
for(int i = 0; i < locales.length; i++){
String code = stra[i];
if(code.contains("_")){
locales[i] = new Locale(code.split("_")[0], code.split("_")[1]);
}else{
locales[i] = new Locale(code);
}
}
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"),
};
Arrays.sort(locales, Structs.comparing(l -> l.getDisplayName(l), String.CASE_INSENSITIVE_ORDER));
}
//server port
public static final int port = 6567;
public static final int webPort = 6568;
Version.init();
public static final GameState state = new GameState();
public static final ThreadHandler threads = new ThreadHandler(Platform.instance.getThreadProvider());
content = new ContentLoader();
if(!headless){
content.setVerbose();
}
public static final ServerDebug serverDebug = new ServerDebug();
public static final ClientDebug clientDebug = new ClientDebug();
defaultWaves = new DefaultWaves();
collisions = new EntityCollisions();
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;
playerGroup = Entities.addGroup(Player.class).enableMapping();
tileGroup = Entities.addGroup(TileEntity.class, false);
bulletGroup = Entities.addGroup(Bullet.class).enableMapping();
effectGroup = Entities.addGroup(EffectEntity.class, false);
groundEffectGroup = Entities.addGroup(DrawTrait.class, false);
puddleGroup = Entities.addGroup(Puddle.class).enableMapping();
shieldGroup = Entities.addGroup(ShieldEntity.class, false);
fireGroup = Entities.addGroup(Fire.class).enableMapping();
unitGroups = new EntityGroup[Team.all.length];
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);
for(Team team : Team.all){
unitGroups[team.ordinal()] = Entities.addGroup(BaseUnit.class).enableMapping();
}
for(EntityGroup<?> group : Entities.getAllGroups()){
group.setRemoveListener(entity -> {
if(entity instanceof SyncTrait && Net.client()){
netClient.addRemovedEntity((entity).getID());
}
});
}
state = new GameState();
data = new GlobalData();
mobile = Core.app.getType() == ApplicationType.Android || Core.app.getType() == ApplicationType.iOS || testMobile;
ios = Core.app.getType() == ApplicationType.iOS;
android = Core.app.getType() == ApplicationType.Android;
Core.settings.setAppName(appName);
dataDirectory = Core.settings.getDataDirectory();
screenshotDirectory = dataDirectory.child("screenshots/");
customMapDirectory = dataDirectory.child("maps/");
saveDirectory = dataDirectory.child("saves/");
tmpDirectory = dataDirectory.child("tmp/");
}
}

View File

@@ -0,0 +1,352 @@
package io.anuke.mindustry.ai;
import io.anuke.arc.Events;
import io.anuke.arc.collection.*;
import io.anuke.arc.function.Predicate;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Geometry;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.entities.type.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.Teams.TeamData;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.meta.BlockFlag;
import static io.anuke.mindustry.Vars.*;
/** Class used for indexing special target blocks for AI. */
@SuppressWarnings("unchecked")
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(Item.getAllOres().toArray(Item.class));
private final ObjectSet<Item> itemSet = new ObjectSet<>();
/** Stores all ore quadtrants on the map. */
private ObjectMap<Item, ObjectSet<Tile>> ores;
/** Tags all quadrants. */
private GridBits[] structQuadrants;
/** Stores all damaged tile entities by team. */
private ObjectSet<Tile>[] damagedTiles = new ObjectSet[Team.all.length];
/**All ores available on this map.*/
private ObjectSet<Item> allOres = new ObjectSet<>();
/** Maps teams to a map of flagged tiles by type. */
private ObjectSet<Tile>[][] flagMap = new ObjectSet[Team.all.length][BlockFlag.all.length];
/** Maps tile positions to their last known tile index data. */
private IntMap<TileIndex> typeMap = new IntMap<>();
/** Empty set used for returning. */
private ObjectSet<Tile> emptySet = new ObjectSet<>();
/** Array used for returning and reusing. */
private Array<Tile> returnArray = new Array<>();
public BlockIndexer(){
Events.on(TileChangeEvent.class, event -> {
if(typeMap.get(event.tile.pos()) != null){
TileIndex index = typeMap.get(event.tile.pos());
for(BlockFlag flag : index.flags){
getFlagged(index.team)[flag.ordinal()].remove(event.tile);
}
}
process(event.tile);
updateQuadrant(event.tile);
});
Events.on(WorldLoadEvent.class, event -> {
damagedTiles = new ObjectSet[Team.all.length];
flagMap = new ObjectSet[Team.all.length][BlockFlag.all.length];
for(int i = 0; i < flagMap.length; i++){
for(int j = 0; j < BlockFlag.all.length; j++){
flagMap[i][j] = new ObjectSet<>();
}
}
typeMap.clear();
allOres.clear();
ores = null;
//create bitset for each team type that contains each quadrant
structQuadrants = new GridBits[Team.all.length];
for(int i = 0; i < Team.all.length; i++){
structQuadrants[i] = new GridBits(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++){
Tile tile = world.tile(x, y);
process(tile);
if(tile.entity != null && tile.entity.damaged()){
notifyTileDamaged(tile.entity);
}
if(tile.drop() != null) allOres.add(tile.drop());
}
}
for(int x = 0; x < quadWidth(); x++){
for(int y = 0; y < quadHeight(); y++){
updateQuadrant(world.tile(x * structQuadrantSize, y * structQuadrantSize));
}
}
scanOres();
});
}
private ObjectSet<Tile>[] getFlagged(Team team){
return flagMap[team.ordinal()];
}
/** @return whether this item is present on this map.*/
public boolean hasOre(Item item){
return allOres.contains(item);
}
/** Returns all damaged tiles by team. */
public ObjectSet<Tile> getDamaged(Team team){
returnArray.clear();
if(damagedTiles[team.ordinal()] == null){
damagedTiles[team.ordinal()] = new ObjectSet<>();
}
ObjectSet<Tile> set = damagedTiles[team.ordinal()];
for(Tile tile : set){
if(tile.entity == null || tile.entity.getTeam() != team || !tile.entity.damaged()){
returnArray.add(tile);
}
}
for(Tile tile : returnArray){
set.remove(tile);
}
return set;
}
/** Get all allied blocks with a flag. */
public ObjectSet<Tile> getAllied(Team team, BlockFlag type){
return flagMap[team.ordinal()][type.ordinal()];
}
/** Get all enemy blocks with a flag. */
public Array<Tile> getEnemy(Team team, BlockFlag type){
returnArray.clear();
for(Team enemy : state.teams.enemiesOf(team)){
if(state.teams.isActive(enemy)){
for(Tile tile : getFlagged(enemy)[type.ordinal()]){
returnArray.add(tile);
}
}
}
return returnArray;
}
public void notifyTileDamaged(TileEntity entity){
if(damagedTiles[entity.getTeam().ordinal()] == null){
damagedTiles[entity.getTeam().ordinal()] = new ObjectSet<>();
}
ObjectSet<Tile> set = damagedTiles[entity.getTeam().ordinal()];
set.add(entity.tile);
}
public TileEntity findTile(Team team, float x, float y, float range, Predicate<Tile> pred){
TileEntity 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.ltile(tx, ty);
if(other == null) continue;
if(other.entity == null || other.getTeam() != team || !pred.test(other) || !other.block().targetable)
continue;
TileEntity e = other.entity;
float ndst = Mathf.dst(x, y, e.x, e.y);
if(ndst < range && (closest == null || ndst < dst)){
dst = ndst;
closest = e;
}
}
}
}
}
return 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, emptySet);
}
/** 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.drop() == item){
return res;
}
}
}
return null;
}
private void process(Tile tile){
if(tile.block().flags.size() > 0 &&
tile.getTeam() != Team.none){
ObjectSet<Tile>[] map = getFlagged(tile.getTeam());
for(BlockFlag flag : tile.block().flags){
ObjectSet<Tile> arr = map[flag.ordinal()];
arr.add(tile);
map[flag.ordinal()] = arr;
}
typeMap.put(tile.pos(), 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 == null || result.drop() == null || !scanOres.contains(result.drop())) continue;
itemSet.add(result.drop());
}
}
//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){
if(structQuadrants == null) return;
//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();
for(Team team : Team.all){
TeamData data = state.teams.get(team);
//fast-set this quadrant to 'occupied' if the tile just placed is already of this team
if(tile.getTeam() == data.team && tile.entity != null && tile.block().targetable){
structQuadrants[data.team.ordinal()].set(quadrantX, quadrantY);
continue; //no need to process futher
}
structQuadrants[data.team.ordinal()].set(quadrantX, quadrantY, false);
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.ltile(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(quadrantX, quadrantY);
break outer;
}
}
}
}
}
private boolean getQuad(Team team, int quadrantX, int quadrantY){
return structQuadrants[team.ordinal()].get(quadrantX, quadrantY);
}
private int quadWidth(){
return Mathf.ceil(world.width() / (float)structQuadrantSize);
}
private int quadHeight(){
return Mathf.ceil(world.height() / (float)structQuadrantSize);
}
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.drop() != null && scanOres.contains(tile.drop()) && tile.block() == Blocks.air){
ores.get(tile.drop()).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,211 @@
package io.anuke.mindustry.ai;
import io.anuke.arc.Events;
import io.anuke.arc.collection.IntArray;
import io.anuke.arc.collection.IntQueue;
import io.anuke.arc.math.geom.Geometry;
import io.anuke.arc.math.geom.Point2;
import io.anuke.arc.util.*;
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.Teams.TeamData;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.world.Pos;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.meta.BlockFlag;
import static io.anuke.mindustry.Vars.state;
import static io.anuke.mindustry.Vars.world;
public class Pathfinder{
private static final long maxUpdate = Time.millisToNanos(4);
private PathData[] paths;
private IntArray blocked = new IntArray();
public Pathfinder(){
Events.on(WorldLoadEvent.class, event -> clear());
Events.on(TileChangeEvent.class, event -> {
if(Net.client()) return;
for(Team team : Team.all){
TeamData data = state.teams.get(team);
if(state.teams.isActive(team) && data.team != event.tile.getTeam()){
update(event.tile, data.team);
}
}
update(event.tile, event.tile.getTeam());
});
}
public void updateSolid(Tile tile){
update(tile, tile.getTeam());
}
public void update(){
if(Net.client() || paths == null) return;
for(Team team : Team.all){
if(state.teams.isActive(team)){
updateFrontier(team, maxUpdate);
}
}
}
public Tile getTargetTile(Team team, Tile tile){
float[][] values = paths[team.ordinal()].weights;
if(values == null || tile == null) return tile;
float value = values[tile.x][tile.y];
Tile target = null;
float tl = 0f;
for(Point2 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() && other.floor().drownTime <= 0 &&
!(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 getValueforTeam(Team team, int x, int y){
return paths == null || paths[team.ordinal()].weights == null || team.ordinal() >= paths.length ? 0 : Structs.inBounds(x, y, paths[team.ordinal()].weights) ? paths[team.ordinal()].weights[x][y] : 0;
}
private boolean passable(Tile tile, Team team){
return (!tile.solid()) || (tile.breakable() && (tile.getTeam() != team));
}
/**
* Clears the frontier, increments the search and sets up all flow sources.
* This only occurs for active teams.
*/
private void update(Tile tile, Team team){
//make sure team exists
if(paths != null && paths[team.ordinal()] != null && paths[team.ordinal()].weights != null){
PathData path = paths[team.ordinal()];
if(path.weights[tile.x][tile.y] <= 0.1f){
//this was a previous target
path.frontier.clear();
}else if(!path.frontier.isEmpty()){
return;
}
//impassable tiles have a weight of float.max
if(!passable(tile, team)){
path.weights[tile.x][tile.y] = Float.MAX_VALUE;
}
//increment search, clear frontier
path.search++;
path.frontier.clear();
path.lastSearchTime = Time.millis();
//add all targets to the frontier
for(Tile other : world.indexer.getEnemy(team, BlockFlag.target)){
path.weights[other.x][other.y] = 0;
path.searches[other.x][other.y] = (short)path.search;
path.frontier.addFirst(other.pos());
}
}
}
private void createFor(Team team){
PathData path = new PathData();
path.weights = new float[world.width()][world.height()];
path.searches = new short[world.width()][world.height()];
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(state.teams.areEnemies(tile.getTeam(), team)
&& tile.block().flags.contains(BlockFlag.target)){
path.frontier.addFirst(tile.pos());
path.weights[x][y] = 0;
path.searches[x][y] = (short)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 = Time.nanos();
while(path.frontier.size > 0 && (nsToRun < 0 || Time.timeSinceNanos(start) <= nsToRun)){
Tile tile = world.tile(path.frontier.removeLast());
if(tile == null || path.weights == null) return; //something went horribly wrong, bail
float cost = path.weights[tile.x][tile.y];
//pathfinding overflowed for some reason, time to bail. the next block update will handle this, hopefully
if(path.frontier.size >= world.width() * world.height()){
path.frontier.clear();
return;
}
if(cost < Float.MAX_VALUE){
for(Point2 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 + other.cost || path.searches[dx][dy] < path.search)
&& passable(other, team)){
if(other.cost < 0) throw new IllegalArgumentException("Tile cost cannot be negative! " + other);
path.frontier.addFirst(Pos.get(dx, dy));
path.weights[dx][dy] = cost + other.cost;
path.searches[dx][dy] = (short)path.search;
}
}
}
}
}
private void clear(){
Time.mark();
paths = new PathData[Team.all.length];
blocked.clear();
for(Team team : Team.all){
PathData path = new PathData();
paths[team.ordinal()] = path;
if(state.teams.isActive(team)){
createFor(team);
}
}
}
class PathData{
float[][] weights;
short[][] searches;
int search = 0;
long lastSearchTime;
IntQueue frontier = new IntQueue();
}
}

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,160 @@
package io.anuke.mindustry.ai;
import io.anuke.arc.Events;
import io.anuke.arc.collection.Array;
import io.anuke.arc.function.PositionConsumer;
import io.anuke.arc.math.Angles;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Time;
import io.anuke.arc.util.Tmp;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.content.Fx;
import io.anuke.mindustry.entities.Damage;
import io.anuke.mindustry.entities.Effects;
import io.anuke.mindustry.entities.type.BaseUnit;
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
import io.anuke.mindustry.game.SpawnGroup;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.world.Tile;
import static io.anuke.mindustry.Vars.*;
public class WaveSpawner{
private static final float margin = 40f, coreMargin = tilesize * 3; //how far away from the edge flying units spawn
private Array<FlyerSpawn> flySpawns = new Array<>();
private Array<Tile> groundSpawns = new Array<>();
private boolean spawning = false;
public WaveSpawner(){
Events.on(WorldLoadEvent.class, e -> reset());
}
public int countSpawns(){
return groundSpawns.size;
}
public Array<Tile> getGroundSpawns(){
return groundSpawns;
}
/** @return true if the player is near a ground spawn point. */
public boolean playerNear(){
return groundSpawns.contains(g -> Mathf.dst(g.x * tilesize, g.y * tilesize, player.x, player.y) < state.rules.dropZoneRadius);
}
public void spawnEnemies(){
spawning = true;
for(SpawnGroup group : state.rules.spawns){
int spawned = group.getUnitsSpawned(state.wave - 1);
if(group.type.isFlying){
float spread = margin / 1.5f;
eachFlyerSpawn((spawnX, spawnY) -> {
for(int i = 0; i < spawned; i++){
BaseUnit unit = group.createUnit(waveTeam);
unit.set(spawnX + Mathf.range(spread), spawnY + Mathf.range(spread));
unit.add();
}
});
}else{
float spread = tilesize * 2;
eachGroundSpawn((spawnX, spawnY, doShockwave) -> {
for(int i = 0; i < spawned; i++){
Tmp.v1.rnd(spread);
BaseUnit unit = group.createUnit(waveTeam);
unit.set(spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
Time.run(Math.min(i * 5, 60 * 2), () -> spawnEffect(unit));
}
});
}
}
eachGroundSpawn((spawnX, spawnY, doShockwave) -> {
if(doShockwave){
Time.run(20f, () -> Effects.effect(Fx.spawnShockwave, spawnX, spawnY, state.rules.dropZoneRadius));
Time.run(40f, () -> Damage.damage(waveTeam, spawnX, spawnY, state.rules.dropZoneRadius, 99999999f, true));
}
});
Time.runTask(121f, () -> spawning = false);
}
private void eachGroundSpawn(SpawnConsumer cons){
for(Tile spawn : groundSpawns){
cons.accept(spawn.worldx(), spawn.worldy(), true);
}
if(state.rules.attackMode && state.teams.isActive(waveTeam) && !state.teams.get(defaultTeam).cores.isEmpty()){
Tile firstCore = state.teams.get(defaultTeam).cores.first();
for(Tile core : state.teams.get(waveTeam).cores){
Tmp.v1.set(firstCore).sub(core.worldx(), core.worldy()).limit(coreMargin + core.block().size*tilesize);
cons.accept(core.worldx() + Tmp.v1.x, core.worldy() + Tmp.v1.y, false);
}
}
}
private void eachFlyerSpawn(PositionConsumer cons){
for(FlyerSpawn spawn : flySpawns){
float trns = (world.width() + world.height()) * tilesize;
float spawnX = Mathf.clamp(world.width() * tilesize / 2f + Angles.trnsx(spawn.angle, trns), -margin, world.width() * tilesize + margin);
float spawnY = Mathf.clamp(world.height() * tilesize / 2f + Angles.trnsy(spawn.angle, trns), -margin, world.height() * tilesize + margin);
cons.accept(spawnX, spawnY);
}
if(state.rules.attackMode && state.teams.isActive(waveTeam)){
for(Tile core : state.teams.get(waveTeam).cores){
cons.accept(core.worldx(), core.worldy());
}
}
}
public boolean isSpawning(){
return spawning && !Net.client();
}
private void reset(){
flySpawns.clear();
groundSpawns.clear();
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
if(world.tile(x, y).overlay() == Blocks.spawn){
addSpawns(x, y);
}
}
}
}
private void addSpawns(int x, int y){
groundSpawns.add(world.tile(x, y));
FlyerSpawn fspawn = new FlyerSpawn();
fspawn.angle = Angles.angle(world.width() / 2f, world.height() / 2f, x, y);
flySpawns.add(fspawn);
}
private void spawnEffect(BaseUnit unit){
Effects.effect(Fx.unitSpawn, unit.x, unit.y, 0f, unit);
Time.run(30f, () -> {
unit.add();
Effects.effect(Fx.spawn, unit);
});
}
private interface SpawnConsumer{
void accept(float x, float y, boolean shockwave);
}
private class FlyerSpawn{
float angle;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,711 @@
package io.anuke.mindustry.content;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Time;
import io.anuke.arc.util.Tmp;
import io.anuke.mindustry.entities.Damage;
import io.anuke.mindustry.entities.Effects;
import io.anuke.mindustry.entities.bullet.*;
import io.anuke.mindustry.entities.effect.*;
import io.anuke.mindustry.entities.type.Unit;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.graphics.Pal;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.BuildBlock;
import static io.anuke.mindustry.Vars.world;
public class Bullets implements ContentList{
public static BulletType
//artillery
artilleryDense, arilleryPlastic, artilleryPlasticFrag, artilleryHoming, artlleryIncendiary, artilleryExplosive, artilleryUnit,
//flak
flakScrap, flakLead, flakPlastic, flakExplosive, flakSurge,
//missiles
missileExplosive, missileIncendiary, missileSurge, missileJavelin, missileSwarm, missileRevenant,
//standard
standardCopper, standardDense, standardThorium, standardHoming, standardIncendiary, standardMechSmall,
standardGlaive, standardDenseBig, standardThoriumBig, standardIncendiaryBig,
//electric
lancerLaser, meltdownLaser, lightning, arc, damageLightning,
//liquid
waterShot, cryoShot, slagShot, oilShot,
//environment, misc.
fireball, basicFlame, pyraFlame, driverBolt, healBullet, frag, eruptorShot,
//bombs
bombExplosive, bombIncendiary, bombOil, explode;
@Override
public void load(){
artilleryDense = new ArtilleryBulletType(3f, 0, "shell"){{
hitEffect = Fx.flakExplosion;
knockback = 0.8f;
lifetime = 50f;
bulletWidth = bulletHeight = 11f;
collidesTiles = false;
splashDamageRadius = 25f;
splashDamage = 33f;
}};
artilleryPlasticFrag = new BasicBulletType(2.5f, 7, "bullet"){{
bulletWidth = 10f;
bulletHeight = 12f;
bulletShrink = 1f;
lifetime = 15f;
backColor = Pal.plastaniumBack;
frontColor = Pal.plastaniumFront;
despawnEffect = Fx.none;
}};
arilleryPlastic = new ArtilleryBulletType(3.4f, 0, "shell"){{
hitEffect = Fx.plasticExplosion;
knockback = 1f;
lifetime = 55f;
bulletWidth = bulletHeight = 13f;
collidesTiles = false;
splashDamageRadius = 35f;
splashDamage = 45f;
fragBullet = artilleryPlasticFrag;
fragBullets = 10;
backColor = Pal.plastaniumBack;
frontColor = Pal.plastaniumFront;
}};
artilleryHoming = new ArtilleryBulletType(3f, 0, "shell"){{
hitEffect = Fx.flakExplosion;
knockback = 0.8f;
lifetime = 45f;
bulletWidth = bulletHeight = 11f;
collidesTiles = false;
splashDamageRadius = 25f;
splashDamage = 33f;
homingPower = 2f;
homingRange = 50f;
}};
artlleryIncendiary = new ArtilleryBulletType(3f, 0, "shell"){{
hitEffect = Fx.blastExplosion;
knockback = 0.8f;
lifetime = 60f;
bulletWidth = bulletHeight = 13f;
collidesTiles = false;
splashDamageRadius = 25f;
splashDamage = 30f;
incendAmount = 4;
incendSpread = 11f;
frontColor = Pal.lightishOrange;
backColor = Pal.lightOrange;
trailEffect = Fx.incendTrail;
}};
artilleryExplosive = new ArtilleryBulletType(2f, 0, "shell"){{
hitEffect = Fx.blastExplosion;
knockback = 0.8f;
lifetime = 70f;
bulletWidth = bulletHeight = 14f;
collidesTiles = false;
splashDamageRadius = 45f;
splashDamage = 50f;
backColor = Pal.missileYellowBack;
frontColor = Pal.missileYellow;
}};
artilleryUnit = new ArtilleryBulletType(2f, 0, "shell"){{
hitEffect = Fx.blastExplosion;
knockback = 0.8f;
lifetime = 90f;
bulletWidth = bulletHeight = 14f;
collides = true;
collidesTiles = true;
splashDamageRadius = 20f;
splashDamage = 38f;
backColor = Pal.bulletYellowBack;
frontColor = Pal.bulletYellow;
}};
flakLead = new FlakBulletType(4.2f, 3){{
lifetime = 60f;
ammoMultiplier = 3f;
shootEffect = Fx.shootSmall;
bulletWidth = 6f;
bulletHeight = 8f;
hitEffect = Fx.flakExplosion;
splashDamage = 27f;
splashDamageRadius = 15f;
}};
flakScrap = new FlakBulletType(4f, 3){{
lifetime = 60f;
ammoMultiplier = 3f;
shootEffect = Fx.shootSmall;
reloadMultiplier = 0.5f;
bulletWidth = 6f;
bulletHeight = 8f;
hitEffect = Fx.flakExplosion;
splashDamage = 22f;
splashDamageRadius = 24f;
}};
flakPlastic = new FlakBulletType(4f, 6){{
splashDamageRadius = 50f;
fragBullet = artilleryPlasticFrag;
fragBullets = 6;
hitEffect = Fx.plasticExplosion;
frontColor = Pal.plastaniumFront;
backColor = Pal.plastaniumBack;
shootEffect = Fx.shootBig;
}};
flakExplosive = new FlakBulletType(4f, 5){{
//default bullet type, no changes
shootEffect = Fx.shootBig;
}};
flakSurge = new FlakBulletType(4f, 7){{
splashDamage = 33f;
lightining = 2;
lightningLength = 12;
shootEffect = Fx.shootBig;
}};
missileExplosive = new MissileBulletType(2.7f, 10, "missile"){{
bulletWidth = 8f;
bulletHeight = 8f;
bulletShrink = 0f;
drag = -0.01f;
splashDamageRadius = 30f;
splashDamage = 30f;
lifetime = 150f;
hitEffect = Fx.blastExplosion;
despawnEffect = Fx.blastExplosion;
}};
missileIncendiary = new MissileBulletType(2.9f, 12, "missile"){{
frontColor = Pal.lightishOrange;
backColor = Pal.lightOrange;
bulletWidth = 7f;
bulletHeight = 8f;
bulletShrink = 0f;
drag = -0.01f;
homingPower = 7f;
splashDamageRadius = 10f;
splashDamage = 10f;
lifetime = 160f;
hitEffect = Fx.blastExplosion;
incendSpread = 10f;
incendAmount = 3;
}};
missileSurge = new MissileBulletType(4.4f, 15, "bullet"){{
bulletWidth = 8f;
bulletHeight = 8f;
bulletShrink = 0f;
drag = -0.01f;
splashDamageRadius = 30f;
splashDamage = 22f;
lifetime = 150f;
hitEffect = Fx.blastExplosion;
despawnEffect = Fx.blastExplosion;
lightining = 2;
lightningLength = 14;
}};
missileJavelin = new MissileBulletType(5f, 10.5f, "missile"){{
bulletWidth = 8f;
bulletHeight = 8f;
bulletShrink = 0f;
drag = -0.003f;
keepVelocity = false;
splashDamageRadius = 20f;
splashDamage = 1f;
lifetime = 90f;
trailColor = Color.valueOf("b6c6fd");
hitEffect = Fx.blastExplosion;
despawnEffect = Fx.blastExplosion;
backColor = Pal.bulletYellowBack;
frontColor = Pal.bulletYellow;
weaveScale = 8f;
weaveMag = 2f;
}};
missileSwarm = new MissileBulletType(2.7f, 12, "missile"){{
bulletWidth = 8f;
bulletHeight = 8f;
bulletShrink = 0f;
drag = -0.003f;
homingRange = 60f;
keepVelocity = false;
splashDamageRadius = 25f;
splashDamage = 10f;
lifetime = 120f;
trailColor = Color.GRAY;
backColor = Pal.bulletYellowBack;
frontColor = Pal.bulletYellow;
hitEffect = Fx.blastExplosion;
despawnEffect = Fx.blastExplosion;
weaveScale = 8f;
weaveMag = 2f;
}};
missileRevenant = new MissileBulletType(2.7f, 12, "missile"){{
bulletWidth = 8f;
bulletHeight = 8f;
bulletShrink = 0f;
drag = -0.003f;
homingRange = 60f;
keepVelocity = false;
splashDamageRadius = 25f;
splashDamage = 10f;
lifetime = 50f;
trailColor = Pal.unitBack;
backColor = Pal.unitBack;
frontColor = Pal.unitFront;
hitEffect = Fx.blastExplosion;
despawnEffect = Fx.blastExplosion;
weaveScale = 6f;
weaveMag = 1f;
}};
standardCopper = new BasicBulletType(2.5f, 9, "bullet"){{
bulletWidth = 7f;
bulletHeight = 9f;
shootEffect = Fx.shootSmall;
smokeEffect = Fx.shootSmallSmoke;
ammoMultiplier = 1;
}};
standardDense = new BasicBulletType(3.5f, 18, "bullet"){{
bulletWidth = 9f;
bulletHeight = 12f;
reloadMultiplier = 0.6f;
ammoMultiplier = 2;
}};
standardThorium = new BasicBulletType(4f, 29, "bullet"){{
bulletWidth = 10f;
bulletHeight = 13f;
shootEffect = Fx.shootBig;
smokeEffect = Fx.shootBigSmoke;
ammoMultiplier = 2;
}};
standardHoming = new BasicBulletType(3f, 9, "bullet"){{
bulletWidth = 7f;
bulletHeight = 9f;
homingPower = 5f;
reloadMultiplier = 1.4f;
ammoMultiplier = 3;
}};
standardIncendiary = new BasicBulletType(3.2f, 11, "bullet"){{
bulletWidth = 10f;
bulletHeight = 12f;
frontColor = Pal.lightishOrange;
backColor = Pal.lightOrange;
incendSpread = 3f;
incendAmount = 1;
incendChance = 0.3f;
inaccuracy = 3f;
}};
standardGlaive = new BasicBulletType(4f, 7.5f, "bullet"){{
bulletWidth = 10f;
bulletHeight = 12f;
frontColor = Color.valueOf("feb380");
backColor = Color.valueOf("ea8878");
incendSpread = 3f;
incendAmount = 1;
incendChance = 0.3f;
}};
standardMechSmall = new BasicBulletType(4f, 9, "bullet"){{
bulletWidth = 11f;
bulletHeight = 14f;
lifetime = 40f;
inaccuracy = 5f;
despawnEffect = Fx.hitBulletSmall;
}};
standardDenseBig = new BasicBulletType(7f, 42, "bullet"){{
bulletWidth = 15f;
bulletHeight = 21f;
shootEffect = Fx.shootBig;
}};
standardThoriumBig = new BasicBulletType(8f, 65, "bullet"){{
bulletWidth = 16f;
bulletHeight = 23f;
shootEffect = Fx.shootBig;
}};
standardIncendiaryBig = new BasicBulletType(7f, 38, "bullet"){{
bulletWidth = 16f;
bulletHeight = 21f;
frontColor = Pal.lightishOrange;
backColor = Pal.lightOrange;
incendSpread = 3f;
incendAmount = 2;
incendChance = 0.3f;
shootEffect = Fx.shootBig;
}};
damageLightning = new BulletType(0.0001f, 0f){{
lifetime = Lightning.lifetime;
hitEffect = Fx.hitLancer;
despawnEffect = Fx.none;
status = StatusEffects.shocked;
statusDuration = 10f;
}};
healBullet = new BulletType(5.2f, 13){
float healPercent = 3f;
{
shootEffect = Fx.shootHeal;
smokeEffect = Fx.hitLaser;
hitEffect = Fx.hitLaser;
despawnEffect = Fx.hitLaser;
collidesTeam = true;
}
@Override
public boolean collides(Bullet b, Tile tile){
return tile.getTeam() != b.getTeam() || tile.entity.healthf() < 1f;
}
@Override
public void draw(Bullet b){
Draw.color(Pal.heal);
Lines.stroke(2f);
Lines.lineAngleCenter(b.x, b.y, b.rot(), 7f);
Draw.color(Color.WHITE);
Lines.lineAngleCenter(b.x, b.y, b.rot(), 3f);
Draw.reset();
}
@Override
public void hitTile(Bullet b, Tile tile){
super.hit(b);
tile = tile.link();
if(tile.getTeam() == b.getTeam() && !(tile.block() instanceof BuildBlock)){
Effects.effect(Fx.healBlockFull, Pal.heal, tile.drawx(), tile.drawy(), tile.block().size);
tile.entity.healBy(healPercent / 100f * tile.entity.maxHealth());
}
}
};
fireball = new BulletType(1f, 4){
{
pierce = true;
hitTiles = false;
collides = false;
collidesTiles = false;
drag = 0.03f;
hitEffect = despawnEffect = Fx.none;
}
@Override
public void init(Bullet b){
b.velocity().setLength(0.6f + Mathf.random(2f));
}
@Override
public void draw(Bullet b){
Draw.color(Pal.lightFlame, Pal.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 * Time.delta())){
Tile tile = world.tileWorld(b.x, b.y);
if(tile != null){
Fire.create(tile);
}
}
if(Mathf.chance(0.1 * Time.delta())){
Effects.effect(Fx.fireballsmoke, b.x, b.y);
}
if(Mathf.chance(0.1 * Time.delta())){
Effects.effect(Fx.ballfire, b.x, b.y);
}
}
};
basicFlame = new BulletType(3f, 6f){
{
ammoMultiplier = 3f;
hitSize = 7f;
lifetime = 42f;
pierce = true;
drag = 0.05f;
statusDuration = 60f * 4;
shootEffect = Fx.shootSmallFlame;
hitEffect = Fx.hitFlameSmall;
despawnEffect = Fx.none;
status = StatusEffects.burning;
}
@Override
public void draw(Bullet b){
}
};
pyraFlame = new BulletType(3.3f, 9f){
{
ammoMultiplier = 4f;
hitSize = 7f;
lifetime = 42f;
pierce = true;
drag = 0.05f;
statusDuration = 60f * 6;
shootEffect = Fx.shootPyraFlame;
hitEffect = Fx.hitFlameSmall;
despawnEffect = Fx.none;
status = StatusEffects.burning;
}
@Override
public void draw(Bullet b){
}
};
lancerLaser = new BulletType(0.001f, 140){
Color[] colors = {Pal.lancerLaser.cpy().mul(1f, 1f, 1f, 0.4f), Pal.lancerLaser, Color.WHITE};
float[] tscales = {1f, 0.7f, 0.5f, 0.2f};
float[] lenscales = {1f, 1.1f, 1.13f, 1.14f};
float length = 160f;
{
hitEffect = Fx.hitLancer;
despawnEffect = Fx.none;
hitSize = 4;
lifetime = 16f;
pierce = true;
}
@Override
public float range(){
return length;
}
@Override
public void init(Bullet b){
Damage.collideLine(b, b.getTeam(), hitEffect, b.x, b.y, b.rot(), 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.rot(), 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.rot(), baseLen * lenscales[i]);
}
}
Draw.reset();
}
};
meltdownLaser = new BulletType(0.001f, 70){
Color tmpColor = new Color();
Color[] colors = {Color.valueOf("ec745855"), Color.valueOf("ec7458aa"), Color.valueOf("ff9c5a"), Color.WHITE};
float[] tscales = {1f, 0.7f, 0.5f, 0.2f};
float[] strokes = {2f, 1.5f, 1f, 0.3f};
float[] lenscales = {1f, 1.12f, 1.15f, 1.17f};
float length = 220f;
{
hitEffect = Fx.hitMeltdown;
despawnEffect = Fx.none;
hitSize = 4;
drawSize = 420f;
lifetime = 16f;
pierce = true;
}
@Override
public void update(Bullet b){
if(b.timer.get(1, 5f)){
Damage.collideLine(b, b.getTeam(), hitEffect, b.x, b.y, b.rot(), length, true);
}
Effects.shake(1f, 1f, b.x, b.y);
}
@Override
public void hit(Bullet b, float hitx, float hity){
Effects.effect(hitEffect, colors[2], hitx, hity);
if(Mathf.chance(0.4)){
Fire.create(world.tileWorld(hitx + Mathf.range(5f), hity + Mathf.range(5f)));
}
}
@Override
public void draw(Bullet b){
float baseLen = (length) * b.fout();
Lines.lineAngle(b.x, b.y, b.rot(), baseLen);
for(int s = 0; s < colors.length; s++){
Draw.color(tmpColor.set(colors[s]).mul(1f + Mathf.absin(Time.time(), 1f, 0.1f)));
for(int i = 0; i < tscales.length; i++){
Tmp.v1.trns(b.rot() + 180f, (lenscales[i] - 1f) * 35f);
Lines.stroke((9f + Mathf.absin(Time.time(), 0.8f, 1.5f)) * b.fout() * strokes[s] * tscales[i]);
Lines.lineAngle(b.x + Tmp.v1.x, b.y + Tmp.v1.y, b.rot(), baseLen * lenscales[i], CapStyle.none);
}
}
Draw.reset();
}
};
waterShot = new LiquidBulletType(Liquids.water){{
knockback = 0.7f;
}};
cryoShot = new LiquidBulletType(Liquids.cryofluid){{
}};
slagShot = new LiquidBulletType(Liquids.slag){{
damage = 4;
drag = 0.03f;
}};
eruptorShot = new LiquidBulletType(Liquids.slag){{
damage = 2;
speed = 2.1f;
drag = 0.02f;
}};
oilShot = new LiquidBulletType(Liquids.oil){{
drag = 0.03f;
}};
lightning = new BulletType(0.001f, 12f){
{
lifetime = 1f;
shootEffect = Fx.hitLancer;
smokeEffect = Fx.none;
despawnEffect = Fx.none;
hitEffect = Fx.hitLancer;
keepVelocity = false;
}
@Override
public float range(){
return 70f;
}
@Override
public void draw(Bullet b){
}
@Override
public void init(Bullet b){
Lightning.create(b.getTeam(), Pal.lancerLaser, damage, b.x, b.y, b.rot(), 30);
}
};
arc = new BulletType(0.001f, 25){
{
lifetime = 1;
despawnEffect = Fx.none;
hitEffect = Fx.hitLancer;
}
@Override
public void draw(Bullet b){
}
@Override
public void init(Bullet b){
Lightning.create(b.getTeam(), Pal.lancerLaser, damage, b.x, b.y, b.rot(), 25);
}
};
driverBolt = new MassDriverBolt();
frag = new BasicBulletType(5f, 8, "bullet"){{
bulletWidth = 8f;
bulletHeight = 9f;
bulletShrink = 0.5f;
lifetime = 50f;
drag = 0.04f;
}};
bombExplosive = new BombBulletType(10f, 20f, "shell"){{
bulletWidth = 9f;
bulletHeight = 13f;
hitEffect = Fx.flakExplosion;
shootEffect = Fx.none;
smokeEffect = Fx.none;
}};
bombIncendiary = new BombBulletType(7f, 10f, "shell"){{
bulletWidth = 8f;
bulletHeight = 12f;
hitEffect = Fx.flakExplosion;
backColor = Pal.lightOrange;
frontColor = Pal.lightishOrange;
incendChance = 1f;
incendAmount = 3;
incendSpread = 10f;
}};
bombOil = new BombBulletType(2f, 3f, "shell"){
{
bulletWidth = 8f;
bulletHeight = 12f;
hitEffect = Fx.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);
}
}
};
explode = new BombBulletType(2f, 3f, "clear"){
{
hitEffect = Fx.pulverize;
lifetime = 30f;
speed = 1f;
splashDamageRadius = 50f;
splashDamage = 28f;
}
@Override
public void init(Bullet b){
if(b.getOwner() instanceof Unit){
((Unit)b.getOwner()).kill();
}
b.time(b.lifetime());
}
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,101 @@
package io.anuke.mindustry.content;
import io.anuke.arc.graphics.Color;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.ItemType;
public class Items implements ContentList{
public static Item scrap, copper, lead, graphite, coal, titanium, thorium, silicon, plastanium, phasefabric, surgealloy,
sporePod, sand, blastCompound, pyratite, metaglass;
@Override
public void load(){
copper = new Item("copper", Color.valueOf("d99d73")){{
type = ItemType.material;
hardness = 1;
cost = 0.5f;
alwaysUnlocked = true;
}};
lead = new Item("lead", Color.valueOf("8c7fa9")){{
type = ItemType.material;
hardness = 1;
cost = 0.7f;
}};
metaglass = new Item("metaglass", Color.valueOf("ebeef5")){{
type = ItemType.material;
cost = 1.5f;
}};
graphite = new Item("graphite", Color.valueOf("b2c6d2")){{
type = ItemType.material;
cost = 1f;
}};
sand = new Item("sand", Color.valueOf("f7cba4")){{
}};
coal = new Item("coal", Color.valueOf("272727")){{
explosiveness = 0.4f;
flammability = 1f;
hardness = 2;
}};
titanium = new Item("titanium", Color.valueOf("8da1e3")){{
type = ItemType.material;
hardness = 3;
cost = 1f;
}};
thorium = new Item("thorium", Color.valueOf("f9a3c7")){{
type = ItemType.material;
explosiveness = 0.2f;
hardness = 4;
radioactivity = 1f;
cost = 1.1f;
}};
scrap = new Item("scrap", Color.valueOf("777777")){{
}};
silicon = new Item("silicon", Color.valueOf("53565c")){{
type = ItemType.material;
cost = 0.8f;
}};
plastanium = new Item("plastanium", Color.valueOf("cbd97f")){{
type = ItemType.material;
flammability = 0.1f;
explosiveness = 0.2f;
cost = 1.3f;
}};
phasefabric = new Item("phase-fabric", Color.valueOf("f4ba6e")){{
type = ItemType.material;
cost = 1.3f;
radioactivity = 0.6f;
}};
surgealloy = new Item("surge-alloy", Color.valueOf("f3e979")){{
type = ItemType.material;
}};
sporePod = new Item("spore-pod", Color.valueOf("7457ce")){{
flammability = 1.05f;
}};
blastCompound = new Item("blast-compound", Color.valueOf("ff795e")){{
flammability = 0.4f;
explosiveness = 1.2f;
}};
pyratite = new Item("pyratite", Color.valueOf("ffaa5f")){{
flammability = 1.4f;
explosiveness = 0.4f;
}};
}
}

View File

@@ -0,0 +1,38 @@
package io.anuke.mindustry.content;
import io.anuke.arc.graphics.Color;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.type.Liquid;
public class Liquids implements ContentList{
public static Liquid water, slag, oil, cryofluid;
@Override
public void load(){
water = new Liquid("water", Color.valueOf("596ab8")){{
heatCapacity = 0.4f;
effect = StatusEffects.wet;
}};
slag = new Liquid("slag", Color.valueOf("ffa166")){{
temperature = 1f;
viscosity = 0.8f;
effect = StatusEffects.melting;
}};
oil = new Liquid("oil", Color.valueOf("313131")){{
viscosity = 0.7f;
flammability = 1.2f;
explosiveness = 1.2f;
heatCapacity = 0.7f;
effect = StatusEffects.tarred;
}};
cryofluid = new Liquid("cryofluid", Color.valueOf("6ecdec")){{
heatCapacity = 0.9f;
temperature = 0.25f;
effect = StatusEffects.freezing;
}};
}
}

View File

@@ -0,0 +1,54 @@
package io.anuke.mindustry.content;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.type.Loadout;
public class Loadouts implements ContentList{
public static Loadout
basicShard,
advancedShard,
basicFoundation,
basicNucleus;
@Override
public void load(){
basicShard = new Loadout(
" ### ",
" #1# ",
" ### ",
" ^ ^ ",
" ## ## ",
" C# C# "
);
advancedShard = new Loadout(
" ### ",
" #1# ",
"#######",
"C#^ ^C#",
" ## ## ",
" C# C# "
);
basicFoundation = new Loadout(
" #### ",
" #### ",
" #2## ",
" #### ",
" ^^^^ ",
" ###### ",
" C#C#C# "
);
basicNucleus = new Loadout(
" ##### ",
" ##### ",
" ##3## ",
" ##### ",
" >#####< ",
" ^ ^ ^ ^ ",
"#### ####",
"C#C# C#C#"
);
}
}

View File

@@ -0,0 +1,374 @@
package io.anuke.mindustry.content;
import io.anuke.arc.Core;
import io.anuke.arc.graphics.Blending;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.entities.Effects;
import io.anuke.mindustry.entities.Units;
import io.anuke.mindustry.entities.bullet.BombBulletType;
import io.anuke.mindustry.entities.effect.Lightning;
import io.anuke.mindustry.entities.type.Player;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.graphics.Pal;
import io.anuke.mindustry.graphics.Shaders;
import io.anuke.mindustry.type.Mech;
import io.anuke.mindustry.type.Weapon;
public class Mechs implements ContentList{
public static Mech alpha, delta, tau, omega, dart, javelin, trident, glaive;
public static Mech starter;
@Override
public void load(){
alpha = new Mech("alpha-mech", false){
{
drillPower = 1;
mineSpeed = 1.5f;
mass = 1.2f;
speed = 0.5f;
boostSpeed = 0.95f;
buildPower = 1.2f;
engineColor = Color.valueOf("ffd37f");
health = 250f;
weapon = new Weapon("blaster"){{
length = 1.5f;
reload = 14f;
roundrobin = true;
ejectEffect = Fx.shellEjectSmall;
bullet = Bullets.standardMechSmall;
}};
}
@Override
public void updateAlt(Player player){
player.healBy(Time.delta() * 0.09f);
}
};
delta = new Mech("delta-mech", false){
float cooldown = 120;
{
drillPower = -1;
speed = 0.75f;
boostSpeed = 0.95f;
itemCapacity = 15;
mass = 0.9f;
health = 150f;
buildPower = 0.9f;
weaponOffsetX = -1;
weaponOffsetY = -1;
engineColor = Color.valueOf("d3ddff");
weapon = new Weapon("shockgun"){{
shake = 2f;
length = 1f;
reload = 55f;
shotDelay = 3f;
roundrobin = true;
shots = 2;
inaccuracy = 0f;
ejectEffect = Fx.none;
bullet = Bullets.lightning;
}};
}
@Override
public void onLand(Player player){
if(player.timer.get(Player.timerAbility, cooldown)){
Effects.shake(1f, 1f, player);
Effects.effect(Fx.landShock, player);
for(int i = 0; i < 8; i++){
Time.run(Mathf.random(8f), () -> Lightning.create(player.getTeam(), Pal.lancerLaser, 17f, player.x, player.y, Mathf.random(360f), 14));
}
}
}
};
tau = new Mech("tau-mech", false){
float healRange = 60f;
float healAmount = 10f;
float healReload = 160f;
boolean wasHealed;
{
drillPower = 4;
mineSpeed = 3f;
itemCapacity = 70;
weaponOffsetY = -1;
weaponOffsetX = 1;
mass = 1.75f;
speed = 0.44f;
drag = 0.35f;
boostSpeed = 0.8f;
canHeal = true;
health = 200f;
buildPower = 1.6f;
engineColor = Pal.heal;
weapon = new Weapon("heal-blaster"){{
length = 1.5f;
reload = 24f;
roundrobin = false;
ejectEffect = Fx.none;
recoil = 2f;
bullet = Bullets.healBullet;
}};
}
@Override
public void updateAlt(Player player){
if(player.timer.get(Player.timerAbility, healReload)){
wasHealed = false;
Units.nearby(player.getTeam(), player.x, player.y, healRange, unit -> {
if(unit.health < unit.maxHealth()){
Effects.effect(Fx.heal, unit);
wasHealed = true;
}
unit.healBy(healAmount);
});
if(wasHealed){
Effects.effect(Fx.healWave, player);
}
}
}
};
omega = new Mech("omega-mech", false){
protected TextureRegion armorRegion;
{
drillPower = 2;
mineSpeed = 1.5f;
itemCapacity = 50;
speed = 0.36f;
boostSpeed = 0.6f;
mass = 4f;
shake = 4f;
weaponOffsetX = 1;
weaponOffsetY = 0;
engineColor = Color.valueOf("feb380");
health = 350f;
buildPower = 1.5f;
weapon = new Weapon("swarmer"){{
length = 1.5f;
recoil = 4f;
reload = 38f;
shots = 4;
spacing = 8f;
inaccuracy = 8f;
roundrobin = true;
ejectEffect = Fx.none;
shake = 3f;
bullet = Bullets.missileSwarm;
}};
}
@Override
public float getRotationAlpha(Player player){
return 0.6f - player.shootHeat * 0.3f;
}
@Override
public float spreadX(Player player){
return player.shootHeat * 2f;
}
@Override
public void load(){
super.load();
armorRegion = Core.atlas.find(name + "-armor");
}
@Override
public void updateAlt(Player player){
float scl = 1f - player.shootHeat / 2f;
player.velocity().scl(scl);
}
@Override
public float getExtraArmor(Player player){
return player.shootHeat * 30f;
}
@Override
public void draw(Player player){
if(player.shootHeat <= 0.01f) return;
Shaders.build.progress = player.shootHeat;
Shaders.build.region = armorRegion;
Shaders.build.time = Time.time() / 10f;
Shaders.build.color.set(Pal.accent).a = player.shootHeat;
Draw.shader(Shaders.build);
Draw.rect(armorRegion, player.x, player.y, player.rotation);
Draw.shader();
}
};
dart = new Mech("dart-ship", true){
{
drillPower = 1;
mineSpeed = 0.9f;
speed = 0.5f;
drag = 0.09f;
health = 200f;
weaponOffsetX = -1;
weaponOffsetY = -1;
engineColor = Pal.lightTrail;
cellTrnsY = 1f;
buildPower = 1.1f;
weapon = new Weapon("blaster"){{
length = 1.5f;
reload = 15f;
roundrobin = true;
ejectEffect = Fx.shellEjectSmall;
bullet = Bullets.standardCopper;
}};
}
@Override
public boolean alwaysUnlocked(){
return true;
}
};
javelin = new Mech("javelin-ship", true){
float minV = 3.6f;
float maxV = 6f;
TextureRegion shield;
{
drillPower = -1;
speed = 0.11f;
drag = 0.01f;
mass = 2f;
health = 170f;
engineColor = Color.valueOf("d3ddff");
cellTrnsY = 1f;
weapon = new Weapon("missiles"){{
length = 1.5f;
reload = 70f;
shots = 4;
inaccuracy = 2f;
roundrobin = true;
ejectEffect = Fx.none;
velocityRnd = 0.2f;
spacing = 1f;
bullet = Bullets.missileJavelin;
}};
}
@Override
public void load(){
super.load();
shield = Core.atlas.find(name + "-shield");
}
@Override
public float getRotationAlpha(Player player){
return 0.5f;
}
@Override
public void updateAlt(Player player){
float scl = scld(player);
if(Mathf.chance(Time.delta() * (0.15 * scl))){
Effects.effect(Fx.hitLancer, Pal.lancerLaser, player.x, player.y);
Lightning.create(player.getTeam(), Pal.lancerLaser, 10f,
player.x + player.velocity().x, player.y + player.velocity().y, player.rotation, 14);
}
}
@Override
public void draw(Player player){
float scl = scld(player);
if(scl < 0.01f) return;
Draw.color(Pal.lancerLaser);
Draw.alpha(scl / 2f);
Draw.blend(Blending.additive);
Draw.rect(shield, player.x + Mathf.range(scl / 2f), player.y + Mathf.range(scl / 2f), player.rotation - 90);
Draw.blend();
}
float scld(Player player){
return Mathf.clamp((player.velocity().len() - minV) / (maxV - minV));
}
};
trident = new Mech("trident-ship", true){
{
drillPower = 2;
speed = 0.15f;
drag = 0.034f;
mass = 2.5f;
turnCursor = false;
health = 250f;
itemCapacity = 30;
engineColor = Color.valueOf("84f491");
cellTrnsY = 1f;
buildPower = 2.5f;
weapon = new Weapon("bomber"){{
length = 0f;
width = 2f;
reload = 25f;
shots = 2;
shotDelay = 1f;
shots = 8;
roundrobin = true;
ejectEffect = Fx.none;
velocityRnd = 1f;
inaccuracy = 20f;
ignoreRotation = true;
bullet = new BombBulletType(16f, 25f, "shell"){{
bulletWidth = 10f;
bulletHeight = 14f;
hitEffect = Fx.flakExplosion;
shootEffect = Fx.none;
smokeEffect = Fx.none;
}};
}};
}
@Override
public boolean canShoot(Player player){
return player.velocity().len() > 1.2f;
}
};
glaive = new Mech("glaive-ship", true){
{
drillPower = 4;
mineSpeed = 1.3f;
speed = 0.32f;
drag = 0.06f;
mass = 3f;
health = 240f;
itemCapacity = 60;
engineColor = Color.valueOf("feb380");
cellTrnsY = 1f;
buildPower = 1.2f;
weapon = new Weapon("bomber"){{
length = 1.5f;
reload = 13f;
roundrobin = true;
ejectEffect = Fx.shellEjectSmall;
bullet = Bullets.standardGlaive;
}};
}
};
starter = dart;
}
}

View File

@@ -0,0 +1,87 @@
package io.anuke.mindustry.content;
import io.anuke.arc.math.Mathf;
import io.anuke.mindustry.entities.Effects;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.type.StatusEffect;
public class StatusEffects implements ContentList{
public static StatusEffect none, burning, freezing, wet, melting, tarred, overdrive, shielded, shocked, corroded, boss;
@Override
public void load(){
none = new StatusEffect();
burning = new StatusEffect(){{
damage = 0.04f;
effect = Fx.burning;
opposite(() -> wet, () -> freezing);
trans(() -> tarred, ((unit, time, newTime, result) -> {
unit.damage(1f);
Effects.effect(Fx.burning, unit.x + Mathf.range(unit.getSize() / 2f), unit.y + Mathf.range(unit.getSize() / 2f));
result.set(this, Math.min(time + newTime, 300f));
}));
}};
freezing = new StatusEffect(){{
speedMultiplier = 0.6f;
armorMultiplier = 0.8f;
effect = Fx.freezing;
opposite(() -> melting, () -> burning);
}};
wet = new StatusEffect(){{
speedMultiplier = 0.9f;
effect = Fx.wet;
trans(() -> shocked, ((unit, time, newTime, result) -> unit.damage(15f)));
opposite(() -> burning, () -> shocked);
}};
melting = new StatusEffect(){{
speedMultiplier = 0.8f;
armorMultiplier = 0.8f;
damage = 0.3f;
effect = Fx.melting;
trans(() -> tarred, ((unit, time, newTime, result) -> result.set(this, Math.min(time + newTime / 2f, 140f))));
opposite(() -> wet, () -> freezing);
}};
tarred = new StatusEffect(){{
speedMultiplier = 0.6f;
effect = Fx.oily;
trans(() -> melting, ((unit, time, newTime, result) -> result.set(burning, newTime + time)));
trans(() -> burning, ((unit, time, newTime, result) -> result.set(burning, newTime + time)));
}};
overdrive = new StatusEffect(){{
armorMultiplier = 0.95f;
speedMultiplier = 1.15f;
damageMultiplier = 1.4f;
damage = -0.01f;
effect = Fx.overdriven;
}};
shielded = new StatusEffect(){{
armorMultiplier = 3f;
}};
boss = new StatusEffect(){{
armorMultiplier = 3f;
damageMultiplier = 3f;
speedMultiplier = 1.1f;
}};
shocked = new StatusEffect();
//no effects, just small amounts of damage.
corroded = new StatusEffect(){{
damage = 0.1f;
}};
}
}

View File

@@ -0,0 +1,328 @@
package io.anuke.mindustry.content;
import io.anuke.arc.collection.Array;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.type.ItemStack;
import io.anuke.mindustry.world.Block;
import static io.anuke.mindustry.content.Blocks.*;
public class TechTree implements ContentList{
public static TechNode root;
@Override
public void load(){
root = node(coreShard, () -> {
node(conveyor, () -> {
node(junction, () -> {
node(itemBridge);
node(router, () -> {
node(launchPad, () -> {
node(launchPadLarge, () -> {
});
});
node(distributor);
node(sorter, () -> {
node(overflowGate);
});
node(container, () -> {
node(unloader);
node(vault, () -> {
});
});
node(titaniumConveyor, () -> {
node(phaseConveyor, () -> {
node(massDriver, () -> {
});
});
});
});
});
});
node(duo, () -> {
node(scatter, () -> {
node(hail, () -> {
node(salvo, () -> {
node(swarmer, () -> {
node(cyclone, () -> {
node(spectre, () -> {
});
});
});
node(ripple, () -> {
node(fuse, () -> {
});
});
});
});
});
node(scorch, () -> {
node(arc, () -> {
node(wave, () -> {
});
node(lancer, () -> {
node(meltdown, () -> {
});
node(shockMine, () -> {
});
});
});
});
node(copperWall, () -> {
node(copperWallLarge);
node(titaniumWall, () -> {
node(door, () -> {
node(doorLarge);
});
node(titaniumWallLarge);
node(thoriumWall, () -> {
node(thoriumWallLarge);
node(surgeWall, () -> {
node(surgeWallLarge);
node(phaseWall, () -> {
node(phaseWallLarge);
});
});
});
});
});
});
node(mechanicalDrill, () -> {
node(graphitePress, () -> {
node(pneumaticDrill, () -> {
node(cultivator, () -> {
});
node(laserDrill, () -> {
node(blastDrill, () -> {
});
node(waterExtractor, () -> {
node(oilExtractor, () -> {
});
});
});
});
node(pyratiteMixer, () -> {
node(blastMixer, () -> {
});
});
node(siliconSmelter, () -> {
node(sporePress, () -> {
node(coalCentrifuge, () -> {
});
node(multiPress, () -> {
});
node(plastaniumCompressor, () -> {
node(phaseWeaver, () -> {
});
});
});
node(kiln, () -> {
node(incinerator, () -> {
node(melter, () -> {
node(surgeSmelter, () -> {
});
node(separator, () -> {
node(pulverizer, () -> {
});
});
node(cryofluidMixer, () -> {
});
});
});
});
});
});
node(mechanicalPump, () -> {
node(conduit, () -> {
node(liquidJunction, () -> {
node(liquidRouter, () -> {
node(liquidTank);
node(pulseConduit, () -> {
node(phaseConduit, () -> {
});
});
node(rotaryPump, () -> {
node(thermalPump, () -> {
});
});
});
node(bridgeConduit);
});
});
});
node(combustionGenerator, () -> {
node(powerNode, () -> {
node(powerNodeLarge, () -> {
node(surgeTower, () -> {
});
});
node(battery, () -> {
node(batteryLarge, () -> {
});
});
node(mender, () -> {
node(mendProjector, () -> {
node(forceProjector, () -> {
node(overdriveProjector, () -> {
});
});
node(repairPoint, () -> {
});
});
});
node(turbineGenerator, () -> {
node(thermalGenerator, () -> {
node(rtgGenerator, () -> {
node(differentialGenerator, () -> {
node(thoriumReactor, () -> {
node(impactReactor, () -> {
});
});
});
});
});
});
node(solarPanel, () -> {
node(largeSolarPanel, () -> {
});
});
});
node(draugFactory, () -> {
node(spiritFactory, () -> {
node(phantomFactory);
});
node(daggerFactory, () -> {
node(crawlerFactory, () -> {
node(titanFactory, () -> {
node(fortressFactory, () -> {
});
});
});
node(wraithFactory, () -> {
node(ghoulFactory, () -> {
node(revenantFactory, () -> {
});
});
});
});
});
node(dartPad, () -> {
node(deltaPad, () -> {
node(javelinPad, () -> {
node(tridentPad, () -> {
node(glaivePad);
});
});
node(tauPad, () -> {
node(omegaPad, () -> {
});
});
});
});
});
});
});
}
private TechNode node(Block block, Runnable children){
ItemStack[] requirements = new ItemStack[block.buildRequirements.length];
for(int i = 0; i < requirements.length; i++){
requirements[i] = new ItemStack(block.buildRequirements[i].item, block.buildRequirements[i].amount * 5);
}
return new TechNode(block, requirements, children);
}
private TechNode node(Block block){
return node(block, () -> {});
}
public static class TechNode{
static TechNode context;
public final Block block;
public final ItemStack[] requirements;
public final Array<TechNode> children = new Array<>();
TechNode(Block block, ItemStack[] requirements, Runnable children){
if(context != null){
context.children.add(this);
}
this.block = block;
this.requirements = requirements;
TechNode last = context;
context = this;
children.run();
context = last;
}
}
}

View File

@@ -0,0 +1,347 @@
package io.anuke.mindustry.content;
import io.anuke.arc.collection.ObjectSet;
import io.anuke.mindustry.entities.type.base.*;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.type.UnitType;
import io.anuke.mindustry.type.Weapon;
public class UnitTypes implements ContentList{
public static UnitType
draug, spirit, phantom,
wraith, ghoul, revenant, lich, reaper,
dagger, crawler, titan, fortress, eruptor, chaosArray, eradicator;
@Override
public void load(){
draug = new UnitType("draug", Draug.class, Draug::new){{
isFlying = true;
drag = 0.01f;
speed = 0.3f;
maxVelocity = 1.2f;
range = 50f;
health = 60;
minePower = 0.5f;
engineSize = 1.8f;
engineOffset = 5.7f;
weapon = new Weapon("you have incurred my wrath. prepare to die."){{
bullet = Bullets.lancerLaser;
}};
}};
spirit = new UnitType("spirit", Spirit.class, Spirit::new){{
isFlying = true;
drag = 0.01f;
speed = 0.4f;
maxVelocity = 1.6f;
range = 50f;
health = 60;
engineSize = 1.8f;
engineOffset = 5.7f;
weapon = new Weapon("heal-blaster"){{
length = 1.5f;
reload = 40f;
width = 0.5f;
roundrobin = true;
ejectEffect = Fx.none;
recoil = 2f;
bullet = Bullets.healBullet;
}};
}};
phantom = new UnitType("phantom", Phantom.class, Phantom::new){{
isFlying = true;
drag = 0.01f;
mass = 2f;
speed = 0.45f;
maxVelocity = 1.9f;
range = 70f;
itemCapacity = 70;
health = 220;
buildPower = 0.9f;
minePower = 1.1f;
engineOffset = 6.5f;
toMine = ObjectSet.with(Items.lead, Items.copper, Items.titanium);
weapon = new Weapon("heal-blaster"){{
length = 1.5f;
reload = 20f;
width = 0.5f;
roundrobin = true;
ejectEffect = Fx.none;
recoil = 2f;
bullet = Bullets.healBullet;
}};
}};
dagger = new UnitType("dagger", Dagger.class, Dagger::new){{
maxVelocity = 1.1f;
speed = 0.2f;
drag = 0.4f;
hitsize = 8f;
mass = 1.75f;
health = 130;
weapon = new Weapon("chain-blaster"){{
length = 1.5f;
reload = 28f;
roundrobin = true;
ejectEffect = Fx.shellEjectSmall;
bullet = Bullets.standardCopper;
}};
}};
crawler = new UnitType("crawler", Crawler.class, Crawler::new){{
maxVelocity = 1.2f;
speed = 0.26f;
drag = 0.4f;
hitsize = 8f;
mass = 1.75f;
health = 100;
weapon = new Weapon("bomber"){{
reload = 12f;
ejectEffect = Fx.none;
bullet = Bullets.explode;
}};
}};
titan = new UnitType("titan", Titan.class, Titan::new){{
maxVelocity = 0.8f;
speed = 0.18f;
drag = 0.4f;
mass = 3.5f;
hitsize = 9f;
rotatespeed = 0.1f;
health = 440;
immunities.add(StatusEffects.burning);
weapon = new Weapon("flamethrower"){{
length = 1f;
reload = 14f;
roundrobin = true;
recoil = 1f;
ejectEffect = Fx.none;
bullet = Bullets.basicFlame;
}};
}};
fortress = new UnitType("fortress", Fortress.class, Fortress::new){{
maxVelocity = 0.78f;
speed = 0.15f;
drag = 0.4f;
mass = 5f;
hitsize = 10f;
rotatespeed = 0.06f;
targetAir = false;
health = 750;
weapon = new Weapon("artillery"){{
length = 1f;
reload = 60f;
width = 10f;
roundrobin = true;
recoil = 4f;
shake = 2f;
ejectEffect = Fx.shellEjectMedium;
bullet = Bullets.artilleryUnit;
}};
}};
eruptor = new UnitType("eruptor", Eruptor.class, Eruptor::new){{
maxVelocity = 0.81f;
speed = 0.16f;
drag = 0.4f;
mass = 5f;
hitsize = 9f;
rotatespeed = 0.05f;
targetAir = false;
health = 600;
immunities = ObjectSet.with(StatusEffects.burning, StatusEffects.melting);
weapon = new Weapon("eruption"){{
length = 3f;
reload = 10f;
roundrobin = true;
ejectEffect = Fx.none;
bullet = Bullets.eruptorShot;
recoil = 1f;
width = 7f;
}};
}};
chaosArray = new UnitType("chaos-array", Dagger.class, Dagger::new){{
maxVelocity = 0.68f;
speed = 0.12f;
drag = 0.4f;
mass = 5f;
hitsize = 20f;
rotatespeed = 0.06f;
health = 4000;
weapon = new Weapon("chaos"){{
length = 8f;
reload = 50f;
width = 17f;
roundrobin = true;
recoil = 3f;
shake = 2f;
shots = 4;
spacing = 4f;
shotDelay = 5;
ejectEffect = Fx.shellEjectMedium;
bullet = Bullets.flakSurge;
}};
}};
eradicator = new UnitType("eradicator", Dagger.class, Dagger::new){{
maxVelocity = 0.68f;
speed = 0.12f;
drag = 0.4f;
mass = 5f;
hitsize = 20f;
rotatespeed = 0.06f;
health = 10000;
weapon = new Weapon("eradication"){{
length = 13f;
reload = 30f;
width = 22f;
roundrobin = true;
recoil = 3f;
shake = 2f;
inaccuracy = 3f;
shots = 4;
spacing = 0f;
shotDelay = 3;
ejectEffect = Fx.shellEjectMedium;
bullet = Bullets.standardThoriumBig;
}};
}};
wraith = new UnitType("wraith", Wraith.class, Wraith::new){{
speed = 0.3f;
maxVelocity = 1.9f;
drag = 0.01f;
mass = 1.5f;
isFlying = true;
health = 75;
engineOffset = 5.5f;
range = 140f;
weapon = new Weapon("chain-blaster"){{
length = 1.5f;
reload = 28f;
roundrobin = true;
ejectEffect = Fx.shellEjectSmall;
bullet = Bullets.standardCopper;
}};
}};
ghoul = new UnitType("ghoul", Ghoul.class, Ghoul::new){{
health = 220;
speed = 0.2f;
maxVelocity = 1.4f;
mass = 3f;
drag = 0.01f;
isFlying = true;
targetAir = false;
engineOffset = 7.8f;
range = 140f;
weapon = new Weapon("bomber"){{
length = 0f;
width = 2f;
reload = 12f;
roundrobin = true;
ejectEffect = Fx.none;
velocityRnd = 1f;
inaccuracy = 40f;
ignoreRotation = true;
bullet = Bullets.bombExplosive;
}};
}};
revenant = new UnitType("revenant", Revenant.class, Revenant::new){{
health = 1000;
mass = 5f;
hitsize = 20f;
speed = 0.1f;
maxVelocity = 1f;
drag = 0.01f;
range = 80f;
shootCone = 40f;
isFlying = true;
rotateWeapon = true;
engineOffset = 12f;
engineSize = 3f;
rotatespeed = 0.01f;
attackLength = 90f;
baseRotateSpeed = 0.06f;
weapon = new Weapon("revenant-missiles"){{
length = 3f;
reload = 70f;
width = 10f;
shots = 2;
inaccuracy = 2f;
roundrobin = true;
ejectEffect = Fx.none;
velocityRnd = 0.2f;
spacing = 1f;
bullet = Bullets.missileRevenant;
}};
}};
lich = new UnitType("lich", Revenant.class, Revenant::new){{
health = 7000;
mass = 20f;
hitsize = 40f;
speed = 0.01f;
maxVelocity = 0.6f;
drag = 0.02f;
range = 80f;
shootCone = 20f;
isFlying = true;
rotateWeapon = true;
engineOffset = 21;
engineSize = 5.3f;
rotatespeed = 0.01f;
attackLength = 90f;
baseRotateSpeed = 0.04f;
weapon = new Weapon("lich-missiles"){{
length = 4f;
reload = 180f;
width = 22f;
shots = 22;
shootCone = 100f;
shotDelay = 2;
inaccuracy = 10f;
roundrobin = true;
ejectEffect = Fx.none;
velocityRnd = 0.2f;
spacing = 1f;
bullet = Bullets.missileRevenant;
}};
}};
reaper = new UnitType("reaper", Revenant.class, Revenant::new){{
health = 13000;
mass = 30f;
hitsize = 56f;
speed = 0.01f;
maxVelocity = 0.6f;
drag = 0.02f;
range = 80f;
shootCone = 30f;
isFlying = true;
rotateWeapon = true;
engineOffset = 40;
engineSize = 7.3f;
rotatespeed = 0.01f;
baseRotateSpeed = 0.04f;
weapon = new Weapon("reaper-gun"){{
length = 3f;
reload = 10f;
width = 32f;
shots = 1;
shootCone = 100f;
shake = 1f;
inaccuracy = 3f;
roundrobin = true;
ejectEffect = Fx.none;
bullet = Bullets.standardDenseBig;
}};
}};
}
}

View File

@@ -0,0 +1,177 @@
package io.anuke.mindustry.content;
import io.anuke.arc.collection.Array;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.game.SpawnGroup;
import io.anuke.mindustry.maps.generators.MapGenerator;
import io.anuke.mindustry.maps.generators.MapGenerator.Decoration;
import io.anuke.mindustry.maps.zonegen.DesertWastesGenerator;
import io.anuke.mindustry.type.*;
import io.anuke.mindustry.world.Block;
public class Zones implements ContentList{
public static Zone
groundZero, desertWastes,
craters, frozenForest, ruinousShores, stainedMountains, tarFields,
saltFlats, overgrowth, infestedIslands,
desolateRift, nuclearComplex;
@Override
public void load(){
groundZero = new Zone("groundZero", new MapGenerator("groundZero", 1)){{
baseLaunchCost = ItemStack.with(Items.copper, -100);
startingItems = ItemStack.list(Items.copper, 100);
alwaysUnlocked = true;
conditionWave = 5;
launchPeriod = 5;
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.sand};
}};
desertWastes = new Zone("desertWastes", new DesertWastesGenerator(260, 260)){{
startingItems = ItemStack.list(Items.copper, 200);
conditionWave = 20;
launchPeriod = 10;
loadout = Loadouts.advancedShard;
zoneRequirements = ZoneRequirement.with(groundZero, 20);
resources = new Item[]{Items.copper, Items.lead, Items.coal, Items.sand};
rules = r -> {
r.waves = true;
r.waveTimer = true;
r.launchWaveMultiplier = 3f;
r.waveSpacing = 60 * 50f;
r.spawns = Array.with(
new SpawnGroup(UnitTypes.crawler){{
unitScaling = 3f;
}},
new SpawnGroup(UnitTypes.dagger){{
unitScaling = 4f;
begin = 2;
spacing = 2;
}},
new SpawnGroup(UnitTypes.wraith){{
unitScaling = 3f;
begin = 11;
spacing = 3;
}},
new SpawnGroup(UnitTypes.eruptor){{
unitScaling = 3f;
begin = 22;
unitAmount = 1;
spacing = 4;
}},
new SpawnGroup(UnitTypes.titan){{
unitScaling = 3f;
begin = 37;
unitAmount = 2;
spacing = 4;
}},
new SpawnGroup(UnitTypes.fortress){{
unitScaling = 2f;
effect = StatusEffects.boss;
begin = 41;
spacing = 20;
}}
);
};
}};
saltFlats = new Zone("saltFlats", new MapGenerator("saltFlats")){{
baseLaunchCost = ItemStack.with(Items.copper, -100);
startingItems = ItemStack.list(Items.copper, 100);
alwaysUnlocked = true;
conditionWave = 5;
launchPeriod = 5;
loadout = Loadouts.basicFoundation;
zoneRequirements = ZoneRequirement.with(desertWastes, 60);
blockRequirements = new Block[]{Blocks.daggerFactory, Blocks.draugFactory};
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.sand};
}};
craters = new Zone("craters", new MapGenerator("craters", 1).dist(0).decor(new Decoration(Blocks.snow, Blocks.sporeCluster, 0.004))){{
startingItems = ItemStack.list(Items.copper, 200);
conditionWave = 10;
zoneRequirements = ZoneRequirement.with(groundZero, 10);
blockRequirements = new Block[]{Blocks.router};
resources = new Item[]{Items.copper, Items.lead, Items.coal, Items.sand, Items.scrap};
}};
frozenForest = new Zone("frozenForest", new MapGenerator("frozenForest", 1)
.decor(new Decoration(Blocks.snow, Blocks.sporeCluster, 0.02))){{
loadout = Loadouts.basicFoundation;
baseLaunchCost = ItemStack.with();
startingItems = ItemStack.list(Items.copper, 400);
conditionWave = 10;
zoneRequirements = ZoneRequirement.with(craters, 10);
resources = new Item[]{Items.copper, Items.lead, Items.coal};
}};
overgrowth = new Zone("overgrowth", new MapGenerator("overgrowth")){{
startingItems = ItemStack.list(Items.copper, 3000, Items.lead, 2000, Items.silicon, 1000, Items.metaglass, 500);
conditionWave = 12;
launchPeriod = 4;
loadout = Loadouts.basicNucleus;
zoneRequirements = ZoneRequirement.with(frozenForest, 40);
blockRequirements = new Block[]{Blocks.router};
resources = new Item[]{Items.copper, Items.lead, Items.coal, Items.titanium, Items.sand, Items.thorium, Items.scrap};
}};
ruinousShores = new Zone("ruinousShores", new MapGenerator("ruinousShores", 1).dist(3f, true)){{
loadout = Loadouts.basicFoundation;
baseLaunchCost = ItemStack.with();
startingItems = ItemStack.list(Items.copper, 400);
conditionWave = 20;
launchPeriod = 20;
zoneRequirements = ZoneRequirement.with(desertWastes, 20, craters, 15);
blockRequirements = new Block[]{Blocks.graphitePress, Blocks.combustionGenerator};
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.sand};
}};
stainedMountains = new Zone("stainedMountains", new MapGenerator("stainedMountains", 2)
.dist(0f, false)
.decor(new Decoration(Blocks.shale, Blocks.shaleBoulder, 0.02))){{
loadout = Loadouts.basicFoundation;
startingItems = ItemStack.list(Items.copper, 400, Items.lead, 100);
conditionWave = 10;
launchPeriod = 10;
zoneRequirements = ZoneRequirement.with(frozenForest, 15);
blockRequirements = new Block[]{Blocks.pneumaticDrill};
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.titanium, Items.sand};
}};
tarFields = new Zone("tarFields", new MapGenerator("tarFields")
.dist(0f, false)
.decor(new Decoration(Blocks.shale, Blocks.shaleBoulder, 0.02))){{
loadout = Loadouts.basicFoundation;
startingItems = ItemStack.list(Items.copper, 500, Items.lead, 200);
conditionWave = 15;
launchPeriod = 10;
zoneRequirements = ZoneRequirement.with(ruinousShores, 20);
blockRequirements = new Block[]{Blocks.coalCentrifuge};
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.titanium, Items.sand};
}};
desolateRift = new Zone("desolateRift", new MapGenerator("desolateRift").dist(2f)){{
loadout = Loadouts.basicNucleus;
baseLaunchCost = ItemStack.with();
startingItems = ItemStack.list(Items.copper, 2000, Items.lead, 2000, Items.graphite, 500, Items.titanium, 500, Items.silicon, 500);
conditionWave = 3;
launchPeriod = 2;
zoneRequirements = ZoneRequirement.with(tarFields, 20);
blockRequirements = new Block[]{Blocks.thermalGenerator};
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.titanium, Items.sand, Items.thorium};
}};
nuclearComplex = new Zone("nuclearComplex", new MapGenerator("nuclearProductionComplex", 1)
.decor(new Decoration(Blocks.snow, Blocks.sporeCluster, 0.01))){{
loadout = Loadouts.basicNucleus;
baseLaunchCost = ItemStack.with();
startingItems = ItemStack.list(Items.copper, 2500, Items.lead, 3000, Items.silicon, 800, Items.metaglass, 400);
conditionWave = 30;
launchPeriod = 15;
zoneRequirements = ZoneRequirement.with(stainedMountains, 20);
blockRequirements = new Block[]{Blocks.thermalGenerator};
resources = new Item[]{Items.copper, Items.scrap, Items.lead, Items.coal, Items.titanium, Items.thorium, Items.sand};
}};
}
}

View File

@@ -0,0 +1,245 @@
package io.anuke.mindustry.core;
import io.anuke.arc.collection.*;
import io.anuke.arc.function.Consumer;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.Pixmap;
import io.anuke.arc.util.Log;
import io.anuke.mindustry.content.*;
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.entities.traits.TypeTrait;
import io.anuke.mindustry.entities.type.Player;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.type.*;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.LegacyColorMapper;
import static io.anuke.arc.Core.files;
/**
* Loads all game content.
* Call load() before doing anything with content.
*/
@SuppressWarnings("unchecked")
public class ContentLoader{
private boolean loaded = false;
private boolean verbose = false;
private ObjectMap<String, MappableContent>[] contentNameMap = new ObjectMap[ContentType.values().length];
private Array<Content>[] contentMap = new Array[ContentType.values().length];
private MappableContent[][] temporaryMapper;
private ObjectSet<Consumer<Content>> initialization = new ObjectSet<>();
private ContentList[] content = {
new Fx(),
new Items(),
new StatusEffects(),
new Liquids(),
new Bullets(),
new Mechs(),
new UnitTypes(),
new Blocks(),
new Loadouts(),
new TechTree(),
new Zones(),
//these are not really content classes, but this makes initialization easier
new LegacyColorMapper(),
};
public void setVerbose(){
verbose = true;
}
/** Creates all content types. */
public void load(){
if(loaded){
Log.info("Content already loaded, skipping.");
return;
}
registerTypes();
for(ContentType type : ContentType.values()){
contentMap[type.ordinal()] = new Array<>();
contentNameMap[type.ordinal()] = new ObjectMap<>();
}
for(ContentList list : content){
list.load();
}
int total = 0;
for(ContentType type : ContentType.values()){
for(Content c : contentMap[type.ordinal()]){
if(c instanceof MappableContent){
String name = ((MappableContent)c).name;
if(contentNameMap[type.ordinal()].containsKey(name)){
throw new IllegalArgumentException("Two content objects cannot have the same name! (issue: '" + name + "')");
}
contentNameMap[type.ordinal()].put(name, (MappableContent)c);
}
total++;
}
}
//set up ID mapping
for(Array<Content> arr : contentMap){
for(int i = 0; i < arr.size; i++){
int id = arr.get(i).id;
if(id != i){
throw new IllegalArgumentException("Out-of-order IDs for content '" + arr.get(i) + "' (expected " + i + " but got " + id + ")");
}
}
}
if(verbose){
Log.info("--- CONTENT INFO ---");
for(int k = 0; k < contentMap.length; k++){
Log.info("[{0}]: loaded {1}", ContentType.values()[k].name(), contentMap[k].size);
}
Log.info("Total content loaded: {0}", total);
Log.info("-------------------");
}
loaded = true;
}
public void initialize(Consumer<Content> callable){
initialize(callable, false);
}
/** Initializes all content with the specified function. */
public void initialize(Consumer<Content> callable, boolean override){
if(initialization.contains(callable) && !override) return;
for(ContentType type : ContentType.values()){
for(Content content : contentMap[type.ordinal()]){
callable.accept(content);
}
}
initialization.add(callable);
}
/** Loads block colors. */
public void loadColors(){
Pixmap pixmap = new Pixmap(files.internal("sprites/block_colors.png"));
for(int i = 0; i < pixmap.getWidth(); i++){
if(blocks().size > i){
int color = pixmap.getPixel(i, 0);
if(color == 0) continue;
Block block = block(i);
Color.rgba8888ToColor(block.color, color);
}
}
pixmap.dispose();
}
public void verbose(boolean verbose){
this.verbose = verbose;
}
public void dispose(){
//clear all content, currently not needed
}
public void handleContent(Content content){
contentMap[content.getContentType().ordinal()].add(content);
}
public void setTemporaryMapper(MappableContent[][] temporaryMapper){
this.temporaryMapper = temporaryMapper;
}
public Array<Content>[] getContentMap(){
return contentMap;
}
public <T extends MappableContent> T getByName(ContentType type, String name){
if(contentNameMap[type.ordinal()] == null){
return null;
}
return (T)contentNameMap[type.ordinal()].get(name);
}
public <T extends Content> T getByID(ContentType type, int id){
if(temporaryMapper != null && temporaryMapper[type.ordinal()] != null && temporaryMapper[type.ordinal()].length != 0){
//-1 = invalid content
if(id < 0){
return null;
}
if(temporaryMapper[type.ordinal()].length <= id || temporaryMapper[type.ordinal()][id] == null){
return getByID(type, 0); //default value is always ID 0
}
return (T)temporaryMapper[type.ordinal()][id];
}
if(id >= contentMap[type.ordinal()].size || id < 0){
return null;
}
return (T)contentMap[type.ordinal()].get(id);
}
public <T extends Content> Array<T> getBy(ContentType type){
return (Array<T>)contentMap[type.ordinal()];
}
//utility methods, just makes things a bit shorter
public Array<Block> blocks(){
return getBy(ContentType.block);
}
public Block block(int id){
return (Block)getByID(ContentType.block, id);
}
public Array<Item> items(){
return getBy(ContentType.item);
}
public Item item(int id){
return (Item)getByID(ContentType.item, id);
}
public Array<Liquid> liquids(){
return getBy(ContentType.liquid);
}
public Liquid liquid(int id){
return (Liquid)getByID(ContentType.liquid, id);
}
public Array<BulletType> bullets(){
return getBy(ContentType.bullet);
}
public BulletType bullet(int id){
return (BulletType)getByID(ContentType.bullet, id);
}
public Array<Zone> zones(){
return getBy(ContentType.zone);
}
public Array<UnitType> units(){
return getBy(ContentType.unit);
}
/**
* Registers sync IDs for all types of sync entities.
* Do not register units here!
*/
private void registerTypes(){
TypeTrait.registerType(Player.class, Player::new);
TypeTrait.registerType(Fire.class, Fire::new);
TypeTrait.registerType(Puddle.class, Puddle::new);
}
}

View File

@@ -1,410 +1,329 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Buttons;
import com.badlogic.gdx.graphics.Color;
import io.anuke.arc.*;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.GL20;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.TextureAtlas;
import io.anuke.arc.input.KeyCode;
import io.anuke.arc.scene.ui.Dialog;
import io.anuke.arc.scene.ui.TextField;
import io.anuke.arc.util.*;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.entities.Effects;
import io.anuke.mindustry.entities.type.Player;
import io.anuke.mindustry.game.*;
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.io.Saves;
import io.anuke.mindustry.gen.Call;
import io.anuke.mindustry.input.*;
import io.anuke.mindustry.maps.Map;
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.ucore.core.*;
import io.anuke.ucore.core.Inputs.DeviceType;
import io.anuke.ucore.entities.Entities;
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 io.anuke.mindustry.type.*;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import io.anuke.mindustry.world.Tile;
import java.io.IOException;
import java.nio.IntBuffer;
import static io.anuke.arc.Core.scene;
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.*/
public class Control extends Module{
private UpgradeInventory upgrades = new UpgradeInventory();
private Tutorial tutorial = new Tutorial();
private boolean hiscore = false;
* Should <i>not</i> handle any logic-critical state.
* This class is not created in the headless server.
*/
public class Control implements ApplicationListener{
public final Saves saves;
private boolean wasPaused = false;
private Interval timer = new Interval(2);
private boolean hiscore = false;
private boolean wasPaused = false;
private InputHandler input;
private Saves saves;
public Control(){
IntBuffer buf = BufferUtils.newIntBuffer(1);
Core.gl.glGetIntegerv(GL20.GL_MAX_TEXTURE_SIZE, buf);
int maxSize = buf.get(0);
private float respawntime;
private InputHandler input;
saves = new Saves();
data = new GlobalData();
private InputProxy proxy;
private float controlx, controly;
private boolean controlling;
private Throwable error;
Core.input.setCatch(KeyCode.BACK, true);
public Control(){
saves = new Saves();
Effects.setShakeFalloff(10000f);
Inputs.useControllers(!gwt);
content.initialize(Content::init);
Core.atlas = new TextureAtlas(maxSize < 2048 ? "sprites/sprites_fallback.atlas" : "sprites/sprites.atlas");
Draw.scl = 1f / Core.atlas.find("scale_marker").getWidth();
content.initialize(Content::load, true);
Gdx.input.setCatchBackKey(true);
data.load();
if(mobile){
input = new AndroidInput();
}else{
input = new DesktopInput();
}
Core.settings.setAppName(appName);
Core.settings.defaults(
"ip", "localhost",
"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", "",
"lastBuild", 0
);
proxy = new InputProxy(Gdx.input){
@Override
public int getY() {
return controlling ? (int)controly : input.getY();
createPlayer();
saves.load();
Events.on(StateChangeEvent.class, event -> {
if((event.from == State.playing && event.to == State.menu) || (event.from == State.menu && event.to != State.menu)){
Time.runTask(5f, Platform.instance::updateRPC);
}
});
@Override
public int getX() {
return controlling ? (int)controlx : input.getX();
}
Events.on(PlayEvent.class, event -> {
player.setTeam(defaultTeam);
player.setDead(true);
player.add();
@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+"",
"name", mobile || gwt ? "player" : UCore.getProperty("user.name"),
"servers", "",
"color", Color.rgba8888(playerColors[8]),
"lastVersion", "3.2",
"lastBuild", 0
);
KeyBinds.load();
for(Map map : world.maps().list()){
Settings.defaults("hiscore" + map.name, 0);
}
player = new Player();
player.name = Settings.getString("name");
player.isAndroid = mobile;
player.color.set(Settings.getInt("color"));
player.isLocal = true;
saves.load();
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(WorldLoadEvent.class, event -> {
Core.app.post(() -> Core.app.post(() -> {
if(Net.active() && player.getClosestCore() != null){
//set to closest core since that's where the player will probably respawn; prevents camera jumps
Core.camera.position.set(player.getClosestCore());
}else{
//locally, set to player position since respawning occurs immediately
Core.camera.position.set(player);
}
}));
});
Entities.initPhysics();
Entities.collisions().setCollider(tilesize, world::solid);
Events.on(ResetEvent.class, event -> {
player.reset();
Platform.instance.updateRPC();
hiscore = false;
checkOldUser();
}
saves.resetSave();
});
@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(WaveEvent.class, event -> {
if(world.getMap().getHightScore() < state.wave){
hiscore = true;
world.getMap().setHighScore(state.wave);
}
});
if(Inputs.keyRelease("select")){
Inputs.getProcessor().touchUp(Gdx.input.getX(), Gdx.input.getY(), 0, Buttons.LEFT);
Events.on(GameOverEvent.class, event -> {
state.stats.wavesLasted = state.wave;
Effects.shake(5, 6, Core.camera.position.x, Core.camera.position.y);
//the restart dialog can show info for any number of scenarios
Call.onGameOver(event.winner);
if(state.rules.zone != null){
//remove zone save on game over
if(saves.getZoneSlot() != null){
saves.getZoneSlot().delete();
}
}
});
float xa = Inputs.getAxis("cursor_x");
float ya = Inputs.getAxis("cursor_y");
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;
Gdx.input.setCursorCatched(true);
Inputs.getProcessor().touchDragged(Gdx.input.getX(), Gdx.input.getY(), 0);
//autohost for pvp maps
Events.on(WorldLoadEvent.class, event -> {
if(state.rules.pvp && !Net.active()){
try{
Net.host(port);
player.isAdmin = true;
}catch(IOException e){
ui.showError(Core.bundle.format("server.error", Strings.parseException(e, true)));
Core.app.post(() -> state.set(State.menu));
}
}
});
controlx = Mathf.clamp(controlx, 0, Gdx.graphics.getWidth());
controly = Mathf.clamp(controly, 0, Gdx.graphics.getHeight());
Events.on(UnlockEvent.class, e -> ui.hudfrag.showUnlock(e.content));
if(Gdx.input.getDeltaX() > 1 || Gdx.input.getDeltaY() > 1) {
controlling = false;
Gdx.input.setCursorCatched(false);
}
Events.on(BlockBuildEndEvent.class, e -> {
if(e.team == player.getTeam()){
if(e.breaking){
state.stats.buildingsDeconstructed++;
}else{
state.stats.buildingsBuilt++;
}
}
});
Events.on(BlockDestroyEvent.class, e -> {
if(e.tile.getTeam() == player.getTeam()){
state.stats.buildingsDestroyed++;
}
});
Events.on(UnitDestroyEvent.class, e -> {
if(e.unit.getTeam() != player.getTeam()){
state.stats.enemyUnitsDestroyed++;
}
});
Events.on(ZoneRequireCompleteEvent.class, e -> {
ui.hudfrag.showToast(Core.bundle.format("zone.requirement.complete", state.wave, e.zone.localizedName));
});
Events.on(ZoneConfigureCompleteEvent.class, e -> {
ui.hudfrag.showToast(Core.bundle.format("zone.config.complete", e.zone.configureWave));
});
}
void createPlayer(){
player = new Player();
player.name = Core.settings.getString("name");
player.color.set(Core.settings.getInt("color-0"));
player.isLocal = true;
player.isMobile = mobile;
if(mobile){
input = new MobileInput();
}else{
controlling = false;
Gdx.input.setCursorCatched(false);
input = new DesktopInput();
}
if(!controlling){
controlx = Gdx.input.getX();
controly = Gdx.input.getY();
if(!state.is(State.menu)){
player.add();
}
Core.input.addProcessor(input);
}
public InputHandler input(){
return input;
}
public void playMap(Map map, Rules rules){
ui.loadAnd(() -> {
logic.reset();
world.loadMap(map);
state.rules = rules;
logic.play();
});
}
public void playZone(Zone zone){
ui.loadAnd(() -> {
logic.reset();
world.loadGenerator(zone.generator);
zone.rules.accept(state.rules);
state.rules.zone = zone;
for(Tile core : state.teams.get(defaultTeam).cores){
for(ItemStack stack : zone.getStartingItems()){
core.entity.items.add(stack.item, stack.amount);
}
}
state.set(State.playing);
control.saves.zoneSave();
logic.play();
});
}
public boolean isHighScore(){
return hiscore;
}
@Override
public void dispose(){
content.dispose();
Net.dispose();
ui.editor.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(){
Platform.instance.updateRPC();
if(!Core.settings.getBool("4.0-warning-2", false)){
Time.run(5f, () -> {
FloatingDialog dialog = new FloatingDialog("VERY IMPORTANT");
dialog.buttons.addButton("$ok", () -> {
dialog.hide();
Core.settings.put("4.0-warning-2", true);
Core.settings.save();
}).size(100f, 60f);
dialog.cont.add("Reminder: The alpha version you are about to play is very unstable, and is [accent]not representative of the final v4 release.[]\n\n " +
"\nThere is currently[scarlet] no sound implemented[]; this is intentional.\n" +
"All current art and UI is unfinished, and will be changed before release. " +
"\n\n[accent]Saves may be corrupted without warning between updates.").wrap().width(400f);
dialog.show();
});
}
}
@Override
public void update(){
saves.update();
if(state.inventory.isUpdated() && (Timers.get("updateItems", 8) || state.is(State.paused))){
ui.hudfrag.updateItems();
state.inventory.setUpdated(false);
}
input.updateController();
if(!state.is(State.menu)){
input.update();
//autosave global data if it's modified
data.checkSave();
if(Inputs.keyTap("pause") && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
if(!state.is(State.menu)){
input.update();
if(world.isZone()){
for(Tile tile : state.teams.get(player.getTeam()).cores){
for(Item item : content.items()){
if(tile.entity.items.has(item)){
data.unlockContent(item);
}
}
}
}
//auto-update rpc every 5 seconds
if(timer.get(0, 60 * 5)){
Platform.instance.updateRPC();
}
if(Core.input.keyTap(Binding.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();
state.set(State.playing);
}else if (!ui.restart.isShown()){
if(ui.chatfrag.chatOpen()) {
ui.chatfrag.hide();
}else{
ui.paused.show();
state.set(State.paused);
}
}
}
if(Core.input.keyTap(Binding.menu) && !ui.restart.isShown()){
if(ui.chatfrag.chatOpen()){
ui.chatfrag.hide();
}else if(!ui.paused.isShown() && !scene.hasDialog()){
ui.paused.show();
state.set(State.paused);
}
}
if(!state.is(State.paused) || Net.active()){
Entities.update(effectGroup);
if(!mobile && Core.input.keyTap(Binding.screenshot) && !(scene.getKeyboardFocus() instanceof TextField) && !ui.chatfrag.chatOpen()){
renderer.takeMapScreenshot();
}
if(respawntime > 0){
}else{
if(!state.isPaused()){
Time.update();
}
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(!scene.hasDialog() && !(scene.root.getChildren().peek() instanceof Dialog) && Core.input.keyTap(KeyCode.BACK)){
Platform.instance.hide();
}
}
}
}

View File

@@ -1,40 +1,63 @@
package io.anuke.mindustry.core;
import io.anuke.mindustry.game.Difficulty;
import io.anuke.arc.Events;
import io.anuke.mindustry.entities.type.BaseUnit;
import io.anuke.mindustry.entities.type.base.BaseDrone;
import io.anuke.mindustry.game.EventType.StateChangeEvent;
import io.anuke.mindustry.game.GameMode;
import io.anuke.mindustry.game.Inventory;
import io.anuke.ucore.core.Events;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.net.Net;
import static io.anuke.mindustry.Vars.unitGroups;
import static io.anuke.mindustry.Vars.waveTeam;
public class GameState{
private State state = State.menu;
/** Current wave number, can be anything in non-wave modes. */
public int wave = 1;
/** Wave countdown in ticks. */
public float wavetime;
/** Whether the game is in game over state. */
public boolean gameOver = false, launched = false;
/** The current game rules. */
public Rules rules = new Rules();
/** Statistics for this save/game. Displayed after game over. */
public Stats stats = new Stats();
/** Team data. Gets reset every new game. */
public Teams teams = new Teams();
/** Number of enemies in the game; only used clientside in servers. */
public int enemies;
/** Current game state. */
private State state = State.menu;
public final Inventory inventory = new Inventory();
public int enemies(){
return Net.client() ? enemies : unitGroups[waveTeam.ordinal()].count(b -> !(b instanceof BaseDrone));
}
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 BaseUnit boss(){
return unitGroups[waveTeam.ordinal()].find(BaseUnit::isBoss);
}
public State getState(){
return state;
}
public enum State{
paused, playing, menu
}
public void set(State astate){
Events.fire(new StateChangeEvent(state, astate));
state = astate;
}
public boolean isEditor(){
return rules.editor;
}
public boolean isPaused(){
return (is(State.paused) && !Net.active()) || (gameOver && !Net.active());
}
public boolean is(State astate){
return state == astate;
}
public State getState(){
return state;
}
public enum State{
paused, playing, menu
}
}

View File

@@ -1,109 +1,167 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.utils.Array;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.arc.ApplicationListener;
import io.anuke.arc.Events;
import io.anuke.arc.collection.ObjectSet.ObjectSetIterator;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.mindustry.game.EnemySpawn;
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.entities.*;
import io.anuke.mindustry.entities.type.Player;
import io.anuke.mindustry.entities.type.TileEntity;
import io.anuke.mindustry.game.EventType.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.game.Teams.TeamData;
import io.anuke.mindustry.gen.BrokenBlock;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.net.NetEvents;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.world.Block;
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.modules.Module;
import io.anuke.ucore.util.Mathf;
import io.anuke.mindustry.world.blocks.BuildBlock;
import io.anuke.mindustry.world.blocks.BuildBlock.BuildEntity;
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 implements ApplicationListener{
@Override
public void init(){
Entities.initPhysics();
Entities.collisions().setCollider(tilesize, world::solid);
public Logic(){
Events.on(WaveEvent.class, event -> {
if(world.isZone()){
world.getZone().updateWave(state.wave);
}
for (Player p : playerGroup.all()) {
p.respawns = state.rules.respawns;
}
});
Events.on(BlockDestroyEvent.class, event -> {
//blocks that get broken are appended to the team's broken block queue
Tile tile = event.tile;
Block block = tile.block();
if(block instanceof BuildBlock){
BuildEntity entity = tile.entity();
//update block to reflect the fact that something was being constructed
if(entity.cblock != null && entity.cblock.synthetic()){
block = entity.cblock;
}else{
//otherwise this was a deconstruction that was interrupted, don't want to rebuild that
return;
}
}
TeamData data = state.teams.get(tile.getTeam());
data.brokenBlocks.addFirst(BrokenBlock.get(tile.x, tile.y, tile.rotation(), block.id));
});
}
/** Handles the event of content being used by either the player or some block. */
public void handleContent(UnlockableContent content){
if(!headless){
data.unlockContent(content);
}
}
public void play(){
state.wavetime = wavespace * state.difficulty.timeScaling * 2;
state.set(State.playing);
state.wavetime = state.rules.waveSpacing * 2; //grace period of 2x wave time before game starts
Events.fire(new PlayEvent());
if(state.mode.infiniteResources){
state.inventory.fill();
//add starting items
if(!world.isZone()){
for(Team team : Team.all){
if(state.teams.isActive(team)){
for(Tile core : state.teams.get(team).cores){
core.entity.items.add(Items.copper, 200);
}
}
}
}
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.wavetime = state.rules.waveSpacing;
state.gameOver = state.launched = false;
state.teams = new Teams();
state.rules = new Rules();
state.stats = new Stats();
Timers.clear();
Time.clear();
Entities.clear();
TileEntity.sleepingEntities = 0;
Events.fire(ResetEvent.class);
Events.fire(new ResetEvent());
}
public void runWave(){
world.spawner.spawnEnemies();
state.wave++;
state.wavetime = world.isZone() && world.getZone().isBossWave(state.wave) ? state.rules.waveSpacing * state.rules.bossWaveMultiplier :
world.isZone() && world.getZone().isLaunchWave(state.wave) ? state.rules.waveSpacing * state.rules.launchWaveMultiplier : state.rules.waveSpacing;
if(state.lastUpdated < state.wave + 1){
world.pathfinder().resetPaths();
state.lastUpdated = state.wave + 1;
}
Events.fire(new WaveEvent());
}
for(EnemySpawn spawn : spawns){
Array<SpawnPoint> spawns = world.getSpawns();
private void checkGameOver(){
if(!state.rules.attackMode && state.teams.get(defaultTeam).cores.size == 0 && !state.gameOver){
state.gameOver = true;
Events.fire(new GameOverEvent(waveTeam));
}else if(state.rules.attackMode){
Team alive = null;
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 ++;
});
for(Team team : Team.all){
if(state.teams.get(team).cores.size > 0){
if(alive != null){
return;
}
alive = team;
}
}
if(alive != null && !state.gameOver){
state.gameOver = true;
Events.fire(new GameOverEvent(alive));
}
}
}
@Remote(called = Loc.both)
public static void launchZone(){
if(!headless){
ui.hudfrag.showLaunch();
}
state.wave ++;
state.wavetime = wavespace * state.difficulty.timeScaling;
state.extrawavetime = maxwavespace * state.difficulty.maxTimeScaling;
for(Tile tile : new ObjectSetIterator<>(state.teams.get(defaultTeam).cores)){
Effects.effect(Fx.launch, tile);
}
Events.fire(WaveEvent.class);
Time.runTask(30f, () -> {
for(Tile tile : new ObjectSetIterator<>(state.teams.get(defaultTeam).cores)){
for(Item item : content.items()){
data.addItem(item, tile.entity.items.get(item));
}
world.removeBlock(tile);
}
state.launched = true;
});
}
@Remote(called = Loc.both)
public static void onGameOver(Team winner){
state.stats.wavesLasted = state.wave;
ui.restart.show(winner);
netClient.setQuiet();
}
@Override
@@ -111,50 +169,64 @@ public class Logic extends Module {
if(!state.is(State.menu)){
if(control != null) control.triggerInputUpdate();
if(!state.isPaused()){
Time.update();
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(!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();
if(state.rules.waves && state.rules.waveTimer && !state.gameOver){
if(!state.rules.waitForWaveToEnd || unitGroups[waveTeam.ordinal()].size() == 0){
state.wavetime = Math.max(state.wavetime - Time.delta(), 0);
}
}
if(!Net.client() && (state.wavetime <= 0 || state.extrawavetime <= 0)){
if(!Net.client() && state.wavetime <= 0 && state.rules.waves){
runWave();
}
Entities.update(Entities.defaultGroup());
Entities.update(bulletGroup);
Entities.update(enemyGroup);
Entities.update(tileGroup);
Entities.update(shieldGroup);
if(!headless){
Entities.update(effectGroup);
Entities.update(groundEffectGroup);
}
if(!state.isEditor()){
for(EntityGroup group : unitGroups){
Entities.update(group);
}
Entities.update(puddleGroup);
Entities.update(shieldGroup);
Entities.update(bulletGroup);
Entities.update(tileGroup);
Entities.update(fireGroup);
}else{
for(EntityGroup<?> group : unitGroups){
group.updateEvents();
collisions.updatePhysics(group);
}
}
Entities.update(playerGroup);
Entities.collideGroups(bulletGroup, enemyGroup);
Entities.collideGroups(bulletGroup, playerGroup);
//effect group only contains item transfers in the headless version, update it!
if(headless){
Entities.update(effectGroup);
}
if(!state.isEditor()){
for(EntityGroup group : unitGroups){
if(group.isEmpty()) continue;
collisions.collideGroups(bulletGroup, group);
}
collisions.collideGroups(bulletGroup, playerGroup);
}
world.pathfinder.update();
}
if(!Net.client() && !world.isInvalidMap() && !state.isEditor()){
checkGameOver();
}
}
}

View File

@@ -1,82 +1,89 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.IntSet;
import com.badlogic.gdx.utils.TimeUtils;
import io.anuke.annotations.Annotations.*;
import io.anuke.arc.ApplicationListener;
import io.anuke.arc.Core;
import io.anuke.arc.collection.IntSet;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.math.RandomXS128;
import io.anuke.arc.util.*;
import io.anuke.arc.util.io.ReusableByteInStream;
import io.anuke.arc.util.serialization.Base64Coder;
import io.anuke.mindustry.Vars;
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.net.Net;
import io.anuke.mindustry.entities.Entities;
import io.anuke.mindustry.entities.EntityGroup;
import io.anuke.mindustry.entities.traits.BuilderTrait.BuildRequest;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.entities.traits.TypeTrait;
import io.anuke.mindustry.entities.type.Player;
import io.anuke.mindustry.entities.type.Unit;
import io.anuke.mindustry.game.Version;
import io.anuke.mindustry.gen.Call;
import io.anuke.mindustry.gen.RemoteReadClient;
import io.anuke.mindustry.net.Administration.TraceInfo;
import io.anuke.mindustry.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.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.modules.Module;
import io.anuke.ucore.util.Log;
import io.anuke.ucore.util.Timer;
import io.anuke.mindustry.world.modules.ItemModule;
import java.nio.ByteBuffer;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.zip.InflaterInputStream;
import static io.anuke.mindustry.Vars.*;
public class NetClient extends Module {
private final static float dataTimeout = 60*18; //18 seconds timeout
public class NetClient implements ApplicationListener{
private final static float dataTimeout = 60 * 18;
private final static float playerSyncTime = 2;
private final static int maxRequests = 50;
public final static float viewScale = 2f;
private Timer timer = new Timer(5);
private Interval timer = new Interval(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;
/** 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 ReusableByteInStream byteStream = new ReusableByteInStream();
private DataInputStream dataStream = new DataInputStream(byteStream);
public NetClient(){
Net.handleClient(Connect.class, packet -> {
Log.info("Connecting to server: {0}", packet.addressTCP);
player.isAdmin = false;
Net.setClientLoaded(false);
recieved.clear();
recent.clear();
timeoutTime = 0f;
connecting = true;
kicked = false;
reset();
ui.chatfrag.clearMessages();
ui.loadfrag.hide();
ui.loadfrag.show("$text.connecting.data");
ui.loadfrag.show("$connecting.data");
Entities.clear();
ui.loadfrag.setButton(() -> {
ui.loadfrag.hide();
connecting = false;
quiet = true;
Net.disconnect();
});
ConnectPacket c = new ConnectPacket();
c.name = player.name;
c.android = mobile;
c.mobile = mobile;
c.versionType = Version.type;
c.color = Color.rgba8888(player.color);
c.usid = getUsid(packet.addressTCP);
c.uuid = Platform.instance.getUUID();
if(c.uuid == null){
ui.showError("$text.invalidid");
ui.showError("$invalidid");
ui.loadfrag.hide();
disconnectQuietly();
return;
@@ -86,238 +93,207 @@ public class NetClient extends Module {
});
Net.handleClient(Disconnect.class, packet -> {
if (kicked) return;
Timers.runTask(3f, ui.loadfrag::hide);
state.set(State.menu);
ui.showError("$text.disconnect");
connecting = false;
logic.reset();
Platform.instance.updateRPC();
if(quiet) return;
Time.runTask(3f, ui.loadfrag::hide);
ui.showError("$disconnect");
});
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());
NetworkIO.loadWorld(new InflaterInputStream(data.stream));
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;
//called on all clients
@Remote(called = Loc.server, targets = Loc.server, variants = Variant.both)
public static void sendMessage(String message, String sender, Player playersender){
if(Vars.ui != null){
Vars.ui.chatfrag.addMessage(message, sender);
}
ByteBuffer data = ByteBuffer.wrap(packet.data);
long time = data.getLong();
if(playersender != null){
playersender.lastText = message;
playersender.textFadeTime = 1f;
}
}
byte groupid = data.get();
//equivalent to above method but there's no sender and no console log
@Remote(called = Loc.server, targets = Loc.server)
public static void sendMessage(String message){
if(Vars.ui != null){
Vars.ui.chatfrag.addMessage(message, null);
}
}
EntityGroup<?> group = Entities.getGroup(groupid);
//called when a server recieves a chat message from a player
@Remote(called = Loc.server, targets = Loc.client)
public static void sendChatMessage(Player player, String message){
if(message.length() > maxTextLength){
throw new ValidateException(player, "Player has sent a message above the text limit.");
}
while (data.position() < data.capacity()) {
int id = data.getInt();
//server console logging
Log.info("&y{0}: &lb{1}", player.name, message);
SyncEntity entity = (SyncEntity) group.getByID(id);
//invoke event for all clients but also locally
//this is required so other clients get the correct name even if they don't know who's sending it yet
Call.sendMessage(message, colorizeName(player.id, player.name), player);
}
if(entity instanceof Player) players ++;
if(entity instanceof Enemy) enemies ++;
private static String colorizeName(int id, String name){
Player player = playerGroup.getByID(id);
if(name == null || player == null) return null;
return "[#" + player.color.toString().toUpperCase() + "]" + name;
}
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 ++;
@Remote(variants = Variant.one)
public static void onTraceInfo(Player player, TraceInfo info){
if(player != null){
ui.traces.show(player, info);
}
}
@Remote(variants = Variant.one, priority = PacketPriority.high)
public static void onKick(KickReason reason){
netClient.disconnectQuietly();
state.set(State.menu);
if(!reason.quiet){
if(reason.extraText() != null){
ui.showText(reason.toString(), reason.extraText());
}else{
ui.showText("$disconnect", reason.toString());
}
}
ui.loadfrag.hide();
}
@Remote(variants = Variant.both)
public static void onInfoMessage(String message){
ui.showText("", message);
}
@Remote(variants = Variant.both)
public static void onWorldDataBegin(){
Entities.clear();
netClient.removed.clear();
logic.reset();
ui.chatfrag.clearMessages();
Net.setClientLoaded(false);
ui.loadfrag.show("$connecting.data");
ui.loadfrag.setButton(() -> {
ui.loadfrag.hide();
netClient.connecting = false;
netClient.quiet = true;
Net.disconnect();
});
}
@Remote(variants = Variant.one)
public static void onPositionSet(float x, float y){
player.x = x;
player.y = y;
}
@Remote
public static void onPlayerDisconnect(int playerid){
playerGroup.removeByID(playerid);
}
@Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
public static void onEntitySnapshot(byte groupID, short amount, short dataLen, byte[] data){
try{
netClient.byteStream.setBytes(Net.decompressSnapshot(data, dataLen));
DataInputStream input = netClient.dataStream;
EntityGroup group = Entities.getGroup(groupID);
//go through each entity
for(int j = 0; j < amount; j++){
int id = input.readInt();
byte typeID = input.readByte();
SyncTrait entity = group == null ? null : (SyncTrait)group.getByID(id);
boolean add = false, created = false;
if(entity == null && id == player.id){
entity = player;
add = true;
}
//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;
}
data.position(data.position() + SyncEntity.getWriteSize((Class<? extends SyncEntity>) group.getType()));
} else {
entity.read(data, time);
created = true;
}
//read the entity
entity.read(input);
if(created && entity.getInterpolator() != null && entity.getInterpolator().target != null){
//set initial starting position
entity.setNet(entity.getInterpolator().target.x, entity.getInterpolator().target.y);
if(entity instanceof Unit && entity.getInterpolator().targets.length > 0){
((Unit)entity).rotation = entity.getInterpolator().targets[0];
}
}
if(add){
entity.add();
netClient.addRemovedEntity(entity.getID());
}
}
}catch(IOException e){
throw new RuntimeException(e);
}
}
@Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
public static void onStateSnapshot(float waveTime, int wave, int enemies, short coreDataLen, byte[] coreData){
try{
state.wavetime = waveTime;
state.wave = wave;
state.enemies = enemies;
netClient.byteStream.setBytes(Net.decompressSnapshot(coreData, coreDataLen));
DataInputStream input = netClient.dataStream;
byte cores = input.readByte();
for(int i = 0; i < cores; i++){
int pos = input.readInt();
Tile tile = world.tile(pos);
if(tile != null && tile.entity != null){
tile.entity.items.read(input);
}else{
new ItemModule().read(input);
}
}
if(debugNet){
clientDebug.setSyncDebug(players, enemies);
}
});
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(IOException e){
throw new RuntimeException(e);
}
}
@Override
@@ -329,12 +305,12 @@ public class NetClient extends Module {
}else if(!connecting){
Net.disconnect();
}else{ //...must be connecting
timeoutTime += Timers.delta();
timeoutTime += Time.delta();
if(timeoutTime > dataTimeout){
Log.err("Failed to load data!");
ui.loadfrag.hide();
kicked = true;
ui.showError("$text.disconnect.data");
quiet = true;
ui.showError("$disconnect.data");
Net.disconnect();
timeoutTime = 0f;
}
@@ -351,8 +327,20 @@ public class NetClient extends Module {
ui.loadfrag.hide();
ui.join.hide();
Net.setClientLoaded(true);
Timers.runTask(1f, () -> Net.send(new ConnectConfirmPacket(), SendMode.tcp));
Timers.runTask(40f, Platform.instance::updateRPC);
Core.app.post(Call::connectConfirm);
Time.runTask(40f, Platform.instance::updateRPC);
}
private void reset(){
Net.setClientLoaded(false);
removed.clear();
timeoutTime = 0f;
connecting = true;
quiet = false;
lastSent = 0;
Entities.clear();
ui.chatfrag.clearMessages();
}
public void beginConnecting(){
@@ -360,31 +348,60 @@ public class NetClient extends Module {
}
public void disconnectQuietly(){
kicked = true;
quiet = true;
Net.disconnect();
}
public void clearRecieved(){
recieved.clear();
/** When set, any disconnects will be ignored and no dialogs will be shown. */
public void setQuiet(){
quiet = true;
}
public void addRemovedEntity(int id){
removed.add(id);
}
public boolean isEntityUsed(int id){
return removed.contains(id);
}
void sync(){
requests = 0;
if(timer.get(0, playerSyncTime)){
BuildRequest[] requests;
//limit to 10 to prevent buffer overflows
int usedRequests = Math.min(player.buildQueue().size, 10);
byte[] bytes = new byte[player.getWriteSize() + 8];
ByteBuffer buffer = ByteBuffer.wrap(bytes);
buffer.putLong(TimeUtils.millis());
player.write(buffer);
requests = new BuildRequest[usedRequests];
for(int i = 0; i < usedRequests; i++){
requests[i] = player.buildQueue().get(i);
}
PositionPacket packet = new PositionPacket();
packet.data = bytes;
Net.send(packet, SendMode.udp);
Call.onClientShapshot(lastSent++, player.x, player.y,
player.pointerX, player.pointerY, player.rotation, player.baseRotation,
player.velocity().x, player.velocity().y,
player.getMineTile(),
player.isBoosting, player.isShooting, ui.chatfrag.chatOpen(),
requests,
Core.camera.position.x, Core.camera.position.y,
Core.camera.width * viewScale, Core.camera.height * viewScale);
}
if(timer.get(1, 60)){
Net.updatePing();
}
}
}
String getUsid(String ip){
if(Core.settings.getString("usid-" + ip, null) != null){
return Core.settings.getString("usid-" + ip, null);
}else{
byte[] bytes = new byte[8];
new RandomXS128().nextBytes(bytes);
String result = new String(Base64Coder.encode(bytes));
Core.settings.put("usid-" + ip, result);
Core.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,52 +1,71 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.utils.*;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.arc.ApplicationListener;
import io.anuke.arc.Events;
import io.anuke.arc.collection.IntMap;
import io.anuke.arc.collection.ObjectSet;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.Colors;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Rectangle;
import io.anuke.arc.math.geom.Vector2;
import io.anuke.arc.util.*;
import io.anuke.arc.util.io.*;
import io.anuke.mindustry.content.Blocks;
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.io.Version;
import io.anuke.mindustry.entities.Entities;
import io.anuke.mindustry.entities.EntityGroup;
import io.anuke.mindustry.entities.traits.BuilderTrait.BuildRequest;
import io.anuke.mindustry.entities.traits.Entity;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.entities.type.Player;
import io.anuke.mindustry.game.EventType.WorldLoadEvent;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.game.Version;
import io.anuke.mindustry.gen.Call;
import io.anuke.mindustry.gen.RemoteReadServer;
import io.anuke.mindustry.net.*;
import io.anuke.mindustry.net.Administration.PlayerInfo;
import io.anuke.mindustry.net.Net.SendMode;
import io.anuke.mindustry.net.Administration.TraceInfo;
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.modules.Module;
import io.anuke.ucore.util.Log;
import io.anuke.ucore.util.Timer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.zip.DeflaterOutputStream;
import static io.anuke.mindustry.Vars.*;
public class NetServer extends Module{
private final static float serverSyncTime = 4, itemSyncTime = 10, kickDuration = 30 * 1000;
private final static int timerEntitySync = 0;
private final static int timerStateSync = 1;
public class NetServer implements ApplicationListener{
public final static int maxSnapshotSize = 430;
private final static float serverSyncTime = 15, kickDuration = 30 * 1000;
private final static Vector2 vector = new Vector2();
private final static Rectangle viewport = new Rectangle();
/** If a player 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);
private ByteBuffer writeBuffer = ByteBuffer.allocate(127);
private ByteBufferOutput outputBuffer = new ByteBufferOutput(writeBuffer);
/** Stream for writing player sync data to. */
private ReusableByteOutStream syncStream = new ReusableByteOutStream();
/** Data stream for writing player sync data to. */
private DataOutputStream dataStream = new DataOutputStream(syncStream);
public NetServer(){
Events.on(GameOverEvent.class, () -> {
weapons.clear();
admins.getEditLogs().clear();
});
Events.on(WorldLoadEvent.class, event -> {
if(!headless){
connections.clear();
}
});
Net.handleServer(Connect.class, (id, connect) -> {
if(admins.isIPBanned(connect.addressTCP)){
@@ -54,28 +73,72 @@ public class NetServer extends Module{
}
});
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;
NetConnection connection = Net.getConnection(id);
if(connection == null ||
admins.isIPBanned(connection.address)) return;
if(connection.hasBegunConnecting){
kick(id, KickReason.idInUse);
return;
}
connection.hasBegunConnecting = true;
TraceInfo trace = admins.getTrace(Net.getConnection(id).address);
PlayerInfo info = admins.getInfo(uuid);
trace.uuid = uuid;
trace.android = packet.android;
connection.mobile = packet.mobile;
if(admins.isIDBanned(uuid)){
kick(id, KickReason.banned);
return;
}
if(TimeUtils.millis() - info.lastKicked < kickDuration){
if(Time.millis() - info.lastKicked < kickDuration){
kick(id, KickReason.recentKick);
return;
}
Log.info("Recieved connect packet for player '{0}' / UUID {1} / IP {2}", packet.name, uuid, trace.ip);
if(packet.versionType == null || ((packet.version == -1 || !packet.versionType.equals(Version.type)) && Version.build != -1 && !admins.allowsCustomClients())){
kick(id, KickReason.customClient);
return;
}
boolean preventDuplicates = headless && netServer.admins.getStrict();
if(preventDuplicates){
for(Player player : playerGroup.all()){
if(player.name.trim().equalsIgnoreCase(packet.name.trim())){
kick(id, KickReason.nameInUse);
return;
}
if(player.uuid.equals(packet.uuid) || player.usid.equals(packet.usid)){
kick(id, KickReason.idInUse);
return;
}
}
}
packet.name = fixName(packet.name);
if(packet.name.trim().length() <= 0){
kick(id, KickReason.nameEmpty);
return;
}
Log.debug("Recieved connect packet for player '{0}' / UUID {1} / IP {2}", packet.name, uuid, connection.address);
String ip = Net.getConnection(id).address;
@@ -87,294 +150,250 @@ public class NetServer extends Module{
}
if(packet.version == -1){
trace.modclient = true;
connection.modclient = true;
}
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.dead = true;
player.setNet(player.x, player.y);
player.color.set(packet.color);
player.color.a = 1f;
try{
writeBuffer.position(0);
player.write(outputBuffer);
}catch(Throwable t){
t.printStackTrace();
kick(id, KickReason.nameEmpty);
return;
}
//playing in pvp mode automatically assigns players to teams
if(state.rules.pvp){
player.setTeam(assignTeam(playerGroup.all()));
Log.info("Auto-assigned player {0} to team {1}.", player.name, player.getTeam());
}
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());
}
sendWorldData(player, id);
Platform.instance.updateRPC();
});
Net.handleServer(MapAckPacket.class, (id, packet) -> {
Net.handleServer(InvokePacket.class, (id, packet) -> {
Player player = connections.get(id);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
NetworkIO.writeWorld(player, weapons.get(admins.getTrace(Net.getConnection(id).address).uuid, new ByteArray()), stream);
WorldData data = new WorldData();
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();
Player player = connections.get(id);
player.read(buffer, time);
});
Net.handleServer(ShootPacket.class, (id, packet) -> {
TraceInfo info = admins.getTrace(Net.getConnection(id).address);
Weapon weapon = (Weapon)Upgrade.getByID(packet.weaponid);
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);
}
}else{
info.fastShots.put(weapon.id, 0);
}
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);
if(player == null) return;
RemoteReadServer.readPacket(packet.writeBuffer, packet.type, player);
});
}
public Team assignTeam(Iterable<Player> players){
//find team with minimum amount of players and auto-assign player to that.
return Structs.findMin(Team.all, team -> {
if(state.teams.isActive(team) && !state.teams.get(team).cores.isEmpty()){
int count = 0;
for(Player other : players){
if(other.getTeam() == team){
count++;
}
}
return count;
}
return Integer.MAX_VALUE;
});
}
public void sendWorldData(Player player, int clientID){
ByteArrayOutputStream stream = new ByteArrayOutputStream();
DeflaterOutputStream def = new FastDeflaterOutputStream(stream);
NetworkIO.writeWorld(player, def);
WorldStream data = new WorldStream();
data.stream = new ByteArrayInputStream(stream.toByteArray());
Net.sendStream(clientID, data);
Log.debug("Packed {0} compressed bytes of world data.", stream.size());
}
public static void onDisconnect(Player player){
//singleplayer multiplayer wierdness
if(player.con == null){
player.remove();
return;
}
if(player.con.hasConnected){
Call.sendMessage("[accent]" + player.name + "[accent] has disconnected.");
Call.onPlayerDisconnect(player.id);
}
player.remove();
netServer.connections.remove(player.con.id);
Log.info("&lm[{1}] &lc{0} has disconnected.", player.name, player.uuid);
}
private static float compound(float speed, float drag){
float total = 0f;
for(int i = 0; i < 50; i++){
total *= (1f - drag);
total += speed;
}
return total;
}
@Remote(targets = Loc.client, unreliable = true)
public static void onClientShapshot(
Player player,
int snapshotID,
float x, float y,
float pointerX, float pointerY,
float rotation, float baseRotation,
float xVelocity, float yVelocity,
Tile mining,
boolean boosting, boolean shooting, boolean chatting,
BuildRequest[] requests,
float viewX, float viewY, float viewWidth, float viewHeight
){
NetConnection connection = player.con;
if(connection == null || snapshotID < connection.lastRecievedClientSnapshot) return;
boolean verifyPosition = !player.isDead() && netServer.admins.getStrict() && headless;
if(connection.lastRecievedClientTime == 0) connection.lastRecievedClientTime = Time.millis() - 16;
connection.viewX = viewX;
connection.viewY = viewY;
connection.viewWidth = viewWidth;
connection.viewHeight = viewHeight;
long elapsed = Time.timeSinceMillis(connection.lastRecievedClientTime);
float maxSpeed = boosting && !player.mech.flying ? player.mech.boostSpeed : player.mech.speed;
float maxMove = elapsed / 1000f * 60f * Math.min(compound(maxSpeed, player.mech.drag) * 1.25f, player.mech.maxSpeed * 1.1f);
player.pointerX = pointerX;
player.pointerY = pointerY;
player.setMineTile(mining);
player.isTyping = chatting;
player.isBoosting = boosting;
player.isShooting = shooting;
player.buildQueue().clear();
for(BuildRequest req : requests){
Tile tile = world.tile(req.x, req.y);
if(tile == null) continue;
//auto-skip done requests
if(req.breaking && tile.block() == Blocks.air){
continue;
}else if(!req.breaking && tile.block() == req.block && (!req.block.rotate || tile.rotation() == req.rotation)){
continue;
}
player.buildQueue().addLast(req);
}
vector.set(x - player.getInterpolator().target.x, 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);
if(!player.mech.flying && player.boostHeat < 0.01f){
player.move(vector.x, vector.y);
}else{
player.x += vector.x;
player.y += vector.y;
}
float newx = player.x, newy = player.y;
if(!verifyPosition){
player.x = prevx;
player.y = prevy;
newx = x;
newy = y;
}else if(Mathf.dst(x, y, newx, newy) > correctDist){
Call.onPositionSet(player.con.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, rotation, baseRotation);
player.velocity().set(xVelocity, yVelocity); //only for visual calculation purposes, doesn't actually update the player
connection.lastRecievedClientSnapshot = snapshotID;
connection.lastRecievedClientTime = Time.millis();
}
@Remote(targets = Loc.client, called = Loc.server)
public static void onAdminRequest(Player player, Player other, AdminAction action){
if(!player.isAdmin){
Log.warn("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 && !player.isLocal) && other != player)){
Log.warn("{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){
TraceInfo info = new TraceInfo(other.con.address, other.uuid, other.con.modclient, other.con.mobile);
if(player.con != null){
Call.onTraceInfo(player.con.id, other, info);
}else{
NetClient.onTraceInfo(other, info);
}
Log.info("&lc{0} has requested trace info of {1}.", player.name, other.name);
}
}
@Remote(targets = Loc.client)
public static void connectConfirm(Player player){
if(player.con == null || player.con.hasConnected) return;
player.add();
player.con.hasConnected = true;
Call.sendMessage("[accent]" + player.name + "[accent] has connected.");
Log.info("&lm[{1}] &y{0} has connected. ", player.name, player.uuid);
}
public boolean isWaitingForPlayers(){
if(state.rules.pvp){
int used = 0;
for(Team t : Team.all){
if(playerGroup.count(p -> p.getTeam() == t) > 0){
used++;
}
}
return used < 2;
}
return false;
}
public void update(){
if(!headless && !closing && Net.server() && state.is(State.menu)){
closing = true;
reset();
ui.loadfrag.show("$text.server.closing");
Timers.runTask(5f, () -> {
ui.loadfrag.show("$server.closing");
Time.runTask(5f, () -> {
Net.closeServer();
ui.loadfrag.hide();
closing = false;
@@ -386,9 +405,10 @@ public class NetServer extends Module{
}
}
public void reset(){
weapons.clear();
admins.clearTraces();
public void kickAll(KickReason reason){
for(NetConnection con : Net.getConnections()){
kick(con.id, reason);
}
}
public void kick(int connection, KickReason reason){
@@ -397,107 +417,159 @@ public class NetServer extends Module{
Log.err("Cannot kick unknown player!");
return;
}else{
Log.info("Kicking connection #{0} / IP: {1}. Reason: {2}", connection, con.address, reason);
Log.info("Kicking connection #{0} / IP: {1}. Reason: {2}", connection, con.address, reason.name());
}
if((reason == KickReason.kick || reason == KickReason.banned) && admins.getTrace(con.address).uuid != null){
PlayerInfo info = admins.getInfo(admins.getTrace(con.address).uuid);
info.timesKicked ++;
info.lastKicked = TimeUtils.millis();
Player player = connections.get(con.id);
if(player != null && (reason == KickReason.kick || reason == KickReason.banned) && player.uuid != null){
PlayerInfo info = admins.getInfo(player.uuid);
info.timesKicked++;
info.lastKicked = Time.millis();
}
KickPacket p = new KickPacket();
p.reason = reason;
Call.onKick(connection, reason);
con.send(p, SendMode.tcp);
Timers.runTask(2f, con::close);
Time.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);
public void writeSnapshot(Player player) throws IOException{
syncStream.reset();
ObjectSet<Tile> cores = state.teams.get(player.getTeam()).cores;
dataStream.writeByte(cores.size);
for(Tile tile : cores){
dataStream.writeInt(tile.pos());
tile.entity.items.write(dataStream);
}
dataStream.close();
byte[] stateBytes = syncStream.toByteArray();
//write basic state data.
Call.onStateSnapshot(player.con.id, state.wavetime, state.wave, state.enemies(), (short)stateBytes.length, Net.compressSnapshot(stateBytes));
viewport.setSize(player.con.viewWidth, player.con.viewHeight).setCenter(player.con.viewX, player.con.viewY);
//check for syncable groups
for(EntityGroup<?> group : Entities.getAllGroups()){
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.");
}
syncStream.reset();
int sent = 0;
for(Entity entity : group.all()){
SyncTrait sync = (SyncTrait)entity;
if(!sync.isSyncing()) continue;
//write all entities now
dataStream.writeInt(entity.getID()); //write id
dataStream.writeByte(sync.getTypeID()); //write type ID
sync.write(dataStream); //write entity
sent++;
if(syncStream.size() > maxSnapshotSize){
dataStream.close();
byte[] syncBytes = syncStream.toByteArray();
Call.onEntitySnapshot(player.con.id, (byte)group.getID(), (short)sent, (short)syncBytes.length, Net.compressSnapshot(syncBytes));
sent = 0;
syncStream.reset();
}
}
if(sent > 0){
dataStream.close();
byte[] syncBytes = syncStream.toByteArray();
Call.onEntitySnapshot(player.con.id, (byte)group.getID(), (short)sent, (short)syncBytes.length, Net.compressSnapshot(syncBytes));
}
}
}
String fixName(String name){
name = name.trim();
if(name.equals("[") || name.equals("]")){
return "";
}
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);
name = prev + result;
}
}
StringBuilder result = new StringBuilder();
int curChar = 0;
while(curChar < name.length() && result.toString().getBytes().length < maxNameLength){
result.append(name.charAt(curChar++));
}
return result.toString();
}
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(){
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;
try{
//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;
//iterate through each player
for(int i = 0; i < playerGroup.size(); i++){
Player player = playerGroup.all().get(i);
if(player.isLocal) continue;
//current buffer you're writing to
ByteBuffer current = null;
//number of entities written to this packet/buffer
int written = 0;
NetConnection connection = player.con;
//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;
}
if(connection == null || !connection.isConnected() || !connections.containsKey(connection.id)){
//player disconnected, call d/c event
onDisconnect(player);
return;
}
//make sure to send incomplete packets too
if(current != null){
SyncPacket packet = new SyncPacket();
packet.data = current.array();
Net.send(packet, SendMode.udp);
}
if(!player.timer.get(Player.timerSync, serverSyncTime) || !connection.hasConnected) continue;
writeSnapshot(player);
}
}
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);
}catch(IOException e){
e.printStackTrace();
}
}
}

View File

@@ -1,89 +1,91 @@
package io.anuke.mindustry.core;
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;
import io.anuke.arc.Core;
import io.anuke.arc.Input.TextInput;
import io.anuke.arc.files.FileHandle;
import io.anuke.arc.function.Consumer;
import io.anuke.arc.function.Predicate;
import io.anuke.arc.math.RandomXS128;
import io.anuke.arc.scene.ui.TextField;
import io.anuke.arc.util.serialization.Base64Coder;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
import static io.anuke.mindustry.Vars.mobile;
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){}
/** Add a text input dialog that should show up after the field is tapped. */
public void addDialog(TextField field){
addDialog(field, 16);
}
/**Show a file chooser. Desktop only.
*
/** See addDialog(). */
public void addDialog(TextField field, int maxLength){
if(!mobile) return; //this is mobile only, desktop doesn't need dialogs
field.tapped(() -> {
TextInput input = new TextInput();
input.text = field.getText();
input.maxLength = maxLength;
input.accepted = text -> {
field.clearText();
field.appendText(text);
field.change();
Core.input.setOnscreenKeyboardVisible(false);
};
Core.input.getTextInput(input);
});
}
/** Update discord RPC. */
public void updateRPC(){
}
/** Whether donating is supported. */
public boolean canDonate(){
return false;
}
/** Must be a base64 string 8 bytes in length. */
public String getUUID(){
String uuid = Core.settings.getString("uuid", "");
if(uuid.isEmpty()){
byte[] result = new byte[8];
new RandomXS128().nextBytes(result);
uuid = new String(Base64Coder.encode(result));
Core.settings.put("uuid", uuid);
Core.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){
}
/**
* Show a file chooser.
* @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, Predicate<String> filetype){
}
/** Hide the app. Android only. */
public void hide(){
}
/** Forces the app into landscape mode. Currently Android only. */
public void beginForceLandscape(){
}
/** Stops forcing the app into landscape orientation. Currently Android only. */
public void endForceLandscape(){
}
}

View File

@@ -1,593 +1,373 @@
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 io.anuke.arc.ApplicationListener;
import io.anuke.arc.Core;
import io.anuke.arc.files.FileHandle;
import io.anuke.arc.function.Consumer;
import io.anuke.arc.function.Predicate;
import io.anuke.arc.graphics.*;
import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.graphics.glutils.FrameBuffer;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Rectangle;
import io.anuke.arc.math.geom.Vector2;
import io.anuke.arc.util.*;
import io.anuke.arc.util.pooling.Pools;
import io.anuke.mindustry.content.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.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.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.mindustry.entities.*;
import io.anuke.mindustry.entities.effect.GroundEffectEntity;
import io.anuke.mindustry.entities.effect.GroundEffectEntity.GroundEffect;
import io.anuke.mindustry.entities.impl.EffectEntity;
import io.anuke.mindustry.entities.traits.*;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.graphics.*;
import io.anuke.mindustry.world.blocks.defense.ForceProjector.ShieldEntity;
import static io.anuke.arc.Core.*;
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 class Renderer implements ApplicationListener{
public final BlockRenderer blocks = new BlockRenderer();
public final MinimapRenderer minimap = new MinimapRenderer();
public final OverlayRenderer overlays = new OverlayRenderer();
public final Pixelator pixelator = new Pixelator();
public Renderer() {
Lines.setCircleVertices(14);
public FrameBuffer shieldBuffer = new FrameBuffer(2, 2);
private Color clearColor;
private float targetscale = io.anuke.arc.scene.ui.layout.Unit.dp.scl(4);
private float camerascale = targetscale;
private Rectangle rect = new Rectangle(), rect2 = new Rectangle();
private float shakeIntensity, shaketime;
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);
}
}
});
public Renderer(){
batch = new SpriteBatch(4096);
camera = new Camera();
Lines.setCircleVertices(20);
Shaders.init();
Cursors.cursorScaling = 3;
Cursors.outlineColor = Color.valueOf("444444");
Cursors.arrow = Cursors.loadCursor("cursor");
Cursors.hand = Cursors.loadCursor("hand");
Cursors.ibeam = Cursors.loadCursor("ibar");
Effects.setScreenShakeProvider((intensity, duration) -> {
shakeIntensity = Math.max(intensity, shakeIntensity);
shaketime = Math.max(shaketime, duration);
});
clearColor = Hue.lightness(0.4f);
clearColor.a = 1f;
Effects.setEffectProvider((effect, color, x, y, rotation, data) -> {
if(effect == Fx.none) return;
if(Core.settings.getBool("effects")){
Rectangle view = camera.bounds(rect);
Rectangle pos = rect2.setSize(effect.size).setCenter(x, y);
background.setWrap(TextureWrap.Repeat, TextureWrap.Repeat);
}
if(view.overlaps(pos)){
@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);
}
if(!(effect instanceof GroundEffect)){
EffectEntity entity = Pools.obtain(EffectEntity.class, EffectEntity::new);
entity.effect = effect;
entity.color.set(color);
entity.rotation = rotation;
entity.data = data;
entity.id++;
entity.set(x, y);
if(data instanceof Entity){
entity.setParent((Entity)data);
}
effectGroup.add(entity);
}else{
GroundEffectEntity entity = Pools.obtain(GroundEffectEntity.class, GroundEffectEntity::new);
entity.effect = effect;
entity.color.set(color);
entity.rotation = rotation;
entity.id++;
entity.data = data;
entity.set(x, y);
if(data instanceof Entity){
entity.setParent((Entity)data);
}
groundEffectGroup.add(entity);
}
}
}
});
public void setPixelate(boolean pixelate){
this.pixelate = pixelate;
}
@Override
public void update(){
if(Core.cameraScale != targetscale){
float targetzoom = (float) Core.cameraScale / targetscale;
camera.zoom = Mathf.lerpDelta(camera.zoom, targetzoom, 0.2f);
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);
}
if(state.is(State.menu)){
clearScreen();
}else{
boolean smoothcam = Settings.getBool("smoothcam");
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);
clearColor = new Color(0f, 0f, 0f, 1f);
}
void drawEnemyMarkers(){
Graphics.surface(indicatorSurface);
Draw.color(Color.RED);
@Override
public void update(){
//TODO hack, find source of this bug
Color.WHITE.set(1f, 1f, 1f, 1f);
for(Enemy enemy : enemyGroup.all()) {
camerascale = Mathf.lerpDelta(camerascale, targetscale, 0.1f);
camera.width = graphics.getWidth() / camerascale;
camera.height = graphics.getHeight() / camerascale;
if (rect.setSize(camera.viewportWidth, camera.viewportHeight).setCenter(camera.position.x, camera.position.y)
.overlaps(enemy.hitbox.getRect(enemy.x, enemy.y))) {
continue;
}
if(state.is(State.menu)){
graphics.clear(Color.BLACK);
}else{
Vector2 position = Tmp.v3.set(player);
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(player.isDead()){
TileEntity core = player.getClosestCore();
if(core != null && player.spawner == null){
camera.position.lerpDelta(core.x, core.y, 0.08f);
}else{
camera.position.lerpDelta(position, 0.08f);
}
}else if(!mobile){
camera.position.lerpDelta(position, 0.08f);
}
Draw.color();
Draw.alpha(0.4f);
Graphics.flushSurface();
Draw.color();
}
updateShake(0.75f);
if(pixelator.enabled()){
pixelator.drawPixelate();
}else{
draw();
}
}
}
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);
@Override
public void dispose(){
minimap.dispose();
shieldBuffer.dispose();
blocks.dispose();
}
time += Timers.delta() / shieldHitDuration;
shieldHits.set(i * 3 + 2, time);
void updateShake(float scale){
if(shaketime > 0){
float intensity = shakeIntensity * (settings.getInt("screenshake", 4) / 4f) * scale;
camera.position.add(Mathf.range(intensity), Mathf.range(intensity));
shakeIntensity -= 0.25f * Time.delta();
shaketime -= Time.delta();
shakeIntensity = Mathf.clamp(shakeIntensity, 0f, 100f);
}else{
shakeIntensity = 0f;
}
}
if(time >= 1f){
shieldHits.removeRange(i * 3, i * 3 + 2);
i--;
}
}
public void draw(){
camera.update();
Texture texture = shieldSurface.texture();
Shaders.shield.color.set(Color.SKY);
if(Float.isNaN(camera.position.x) || Float.isNaN(camera.position.y)){
camera.position.x = player.x;
camera.position.y = player.y;
}
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;
}
graphics.clear(clearColor);
Graphics.end();
Graphics.shader(Shaders.shield.isFallback ? Shaders.outline : Shaders.shield);
Graphics.setScreen();
if(!graphics.isHidden() && (Core.settings.getBool("animatedwater") || Core.settings.getBool("animatedshields")) && (shieldBuffer.getWidth() != graphics.getWidth() || shieldBuffer.getHeight() != graphics.getHeight())){
shieldBuffer.resize(graphics.getWidth(), graphics.getHeight());
}
Core.batch.draw(texture, 0, Gdx.graphics.getHeight(), Gdx.graphics.getWidth(), -Gdx.graphics.getHeight());
Draw.proj(camera.projection());
Graphics.shader();
Graphics.end();
Graphics.beginCam();
Draw.color();
shieldDraws.clear();
}
blocks.floor.drawFloor();
public BlockRenderer getBlocks() {
return blocks;
}
drawAndInterpolate(groundEffectGroup, e -> e instanceof BelowLiquidTrait);
drawAndInterpolate(puddleGroup);
drawAndInterpolate(groundEffectGroup, e -> !(e instanceof BelowLiquidTrait));
public void addShieldHit(float x, float y){
shieldHits.addAll(x, y, 0f);
}
blocks.processBlocks();
public void addShield(Callable call){
shieldDraws.add(call);
}
blocks.drawShadows();
Draw.color();
void drawOverlay(){
blocks.floor.beginDraw();
blocks.floor.drawLayer(CacheLayer.walls);
blocks.floor.endDraw();
//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();
blocks.drawBlocks(Layer.block);
blocks.drawFog();
Lines.stroke(1f);
Draw.color(Color.YELLOW);
Lines.square(x * tilesize, y * tilesize, tilesize / 2f + Mathf.sin(Timers.time(), 4f, 1f));
Draw.shader(Shaders.blockbuild, true);
blocks.drawBlocks(Layer.placement);
Draw.shader();
Draw.color(Color.ORANGE);
Lines.stroke(2f);
if(rot != -1){
Lines.lineAngle(x * tilesize, y * tilesize, rot * 90, 6);
}
Draw.reset();
}
blocks.drawBlocks(Layer.overlay);
//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);
}
drawGroundShadows();
InputHandler input = control.input();
drawAllTeams(false);
//draw placement box
if((input.recipe != null && state.inventory.hasItems(input.recipe.requirements) && (!ui.hasMouse() || mobile)
&& control.input().drawPlace())){
blocks.skipLayer(Layer.turret);
blocks.drawBlocks(Layer.laser);
input.placeMode.draw(control.input().getBlockX(), control.input().getBlockY(),
control.input().getBlockEndX(), control.input().getBlockEndY());
drawFlyerShadows();
Lines.stroke(1f);
Draw.color(Color.SCARLET);
for(SpawnPoint spawn : world.getSpawns()){
Lines.dashCircle(spawn.start.worldx(), spawn.start.worldy(), enemyspawnspace);
}
drawAllTeams(true);
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)){
drawAndInterpolate(bulletGroup);
drawAndInterpolate(effectGroup);
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());
}
overlays.drawBottom();
drawAndInterpolate(playerGroup, p -> true, Player::drawBuildRequests);
if(ui.toolfrag.confirming){
ToolFragment t = ui.toolfrag;
PlaceMode.areaDelete.draw(t.px, t.py, t.px2, t.py2);
}
Draw.reset();
if(Entities.countInBounds(shieldGroup) > 0){
if(settings.getBool("animatedshields")){
Draw.flush();
shieldBuffer.begin();
graphics.clear(Color.CLEAR);
Entities.draw(shieldGroup);
Entities.draw(shieldGroup, shield -> true, shield -> ((ShieldEntity)shield).drawOver());
Draw.flush();
shieldBuffer.end();
Draw.shader(Shaders.shield);
Draw.color(Pal.accent);
Draw.rect(Draw.wrap(shieldBuffer.getTexture()), camera.position.x, camera.position.y, camera.width, -camera.height);
Draw.color();
Draw.shader();
}else{
Entities.draw(shieldGroup, shield -> true, shield -> ((ShieldEntity)shield).drawSimple());
}
}
//draw selected block bars and info
if(input.recipe == null && !ui.hasMouse()){
Tile tile = world.tileWorld(Graphics.mouseWorld().x, Graphics.mouseWorld().y);
overlays.drawTop();
if(tile != null && tile.block() != Blocks.air){
Tile target = tile;
if(tile.isLinked())
target = tile.getLinked();
drawAndInterpolate(playerGroup, p -> !p.isDead() && !p.isLocal, Player::drawName);
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();
Draw.color();
Draw.flush();
}
private void drawGroundShadows(){
Draw.color(0, 0, 0, 0.4f);
float rad = 1.6f;
Consumer<Unit> draw = u -> {
float size = Math.max(u.getIconRegion().getWidth(), u.getIconRegion().getHeight()) * Draw.scl;
Draw.rect("circle-shadow", u.x, u.y, size * rad, size * rad);
};
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();
}
for(EntityGroup<? extends BaseUnit> group : unitGroups){
if(!group.isEmpty()){
drawAndInterpolate(group, unit -> !unit.isDead(), draw::accept);
}
}
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();
}
if(!playerGroup.isEmpty()){
drawAndInterpolate(playerGroup, unit -> !unit.isDead(), draw::accept);
}
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);
Draw.color();
}
float value = bar.value.get(target);
private void drawFlyerShadows(){
float trnsX = -12, trnsY = -13;
Draw.color(0, 0, 0, 0.22f);
if(MathUtils.isEqual(value, -1f)) continue;
for(EntityGroup<? extends BaseUnit> group : unitGroups){
if(!group.isEmpty()){
drawAndInterpolate(group, unit -> unit.isFlying() && !unit.isDead(), baseUnit -> baseUnit.drawShadow(trnsX, trnsY));
}
}
drawBar(bar.color, target.drawx(), target.drawy() + offset, value);
if(!playerGroup.isEmpty()){
drawAndInterpolate(playerGroup, unit -> unit.isFlying() && !unit.isDead(), player -> player.drawShadow(trnsX, trnsY));
}
if (bar.top)
top++;
else
bot++;
}
}
Draw.color();
}
target.block().drawSelect(target);
}
}
if((!debug || showUI) && Settings.getBool("healthbars")){
private void drawAllTeams(boolean flying){
for(Team team : Team.all){
EntityGroup<BaseUnit> group = unitGroups[team.ordinal()];
//draw entity health bars
for(Enemy entity : enemyGroup.all()){
drawHealth(entity);
}
if(group.count(p -> p.isFlying() == flying) +
playerGroup.count(p -> p.isFlying() == flying && p.getTeam() == team) == 0 && flying) continue;
for(Player player : playerGroup.all()){
if(!player.isDead() && !player.isAndroid) drawHealth(player);
}
}
}
drawAndInterpolate(unitGroups[team.ordinal()], u -> u.isFlying() == flying && !u.isDead(), Unit::drawUnder);
drawAndInterpolate(playerGroup, p -> p.isFlying() == flying && p.getTeam() == team && !p.isDead(), Unit::drawUnder);
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);
}
}
drawAndInterpolate(unitGroups[team.ordinal()], u -> u.isFlying() == flying && !u.isDead(), Unit::drawAll);
drawAndInterpolate(playerGroup, p -> p.isFlying() == flying && p.getTeam() == team, Unit::drawAll);
blocks.drawTeamBlocks(Layer.turret, team);
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);
drawAndInterpolate(unitGroups[team.ordinal()], u -> u.isFlying() == flying && !u.isDead(), Unit::drawOver);
drawAndInterpolate(playerGroup, p -> p.isFlying() == flying && p.getTeam() == team, Unit::drawOver);
}
}
if(finion > 0) finion = Mathf.clamp(finion + 0.2f, 0.24f, 1f);
public <T extends DrawTrait> void drawAndInterpolate(EntityGroup<T> group){
drawAndInterpolate(group, t -> true, DrawTrait::draw);
}
float len = 3;
public <T extends DrawTrait> void drawAndInterpolate(EntityGroup<T> group, Predicate<T> toDraw){
drawAndInterpolate(group, toDraw, DrawTrait::draw);
}
float w = (int) (len * 2 * finion) + 0.5f;
public <T extends DrawTrait> void drawAndInterpolate(EntityGroup<T> group, Predicate<T> toDraw, Consumer<T> drawer){
Entities.draw(group, toDraw, drawer);
}
x -= 0.5f;
y += 0.5f;
public void scaleCamera(float amount){
targetscale += amount;
clampScale();
}
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();
}
public void clampScale(){
float s = io.anuke.arc.scene.ui.layout.Unit.dp.scl(1f);
targetscale = Mathf.clamp(targetscale, s * 1.5f, Math.round(s * 6));
}
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);
}
}
}
public float getScale(){
return targetscale;
}
public void scaleCamera(int amount){
setCameraScale(targetscale + amount);
}
public void setScale(float scl){
targetscale = scl;
clampScale();
}
public void clampScale(){
targetscale = Mathf.clamp(targetscale, Math.round(Unit.dp.scl(2)), Math.round(Unit.dp.scl((5))));
}
public void takeMapScreenshot(){
drawGroundShadows();
int w = world.width() * tilesize, h = world.height() * tilesize;
int memory = w * h * 4 / 1024 / 1024;
if(memory >= 65){
ui.showInfo("$screenshot.invalid");
return;
}
boolean hadShields = Core.settings.getBool("animatedshields");
boolean hadWater = Core.settings.getBool("animatedwater");
Core.settings.put("animatedwater", false);
Core.settings.put("animatedshields", false);
FrameBuffer buffer = new FrameBuffer(w, h);
float vpW = camera.width, vpH = camera.height, px = camera.position.x, py = camera.position.y;
disableUI = true;
camera.width = w;
camera.height = h;
camera.position.x = w / 2f + tilesize / 2f;
camera.position.y = h / 2f + tilesize / 2f;
Draw.flush();
buffer.begin();
draw();
Draw.flush();
buffer.end();
disableUI = false;
camera.width = vpW;
camera.height = vpH;
camera.position.set(px, py);
buffer.begin();
byte[] lines = ScreenUtils.getFrameBufferPixels(0, 0, w, h, true);
for(int i = 0; i < lines.length; i += 4){
lines[i + 3] = (byte)255;
}
buffer.end();
Pixmap fullPixmap = new Pixmap(w, h, Pixmap.Format.RGBA8888);
BufferUtils.copy(lines, 0, fullPixmap.getPixels(), lines.length);
FileHandle file = screenshotDirectory.child("screenshot-" + Time.millis() + ".png");
PixmapIO.writePNG(file, fullPixmap);
fullPixmap.dispose();
ui.showInfoFade(Core.bundle.format("screenshot", file.toString()));
buffer.dispose();
Core.settings.put("animatedwater", hadWater);
Core.settings.put("animatedshields", hadShields);
}
}

View File

@@ -1,145 +0,0 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.utils.Array;
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<>();
private final ThreadProvider impl;
private float delta = 1f;
private long frame = 0;
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);
});
}
public void run(Runnable r){
if(enabled) {
synchronized (toRun) {
toRun.add(r);
}
}else{
r.run();
}
}
public int getFPS(){
return (int)(60/delta);
}
public long getFrameID(){
return frame;
}
public float getFramesSinceUpdate(){
return framesSinceUpdate;
}
public void handleRender(){
if(!enabled) return;
framesSinceUpdate += Timers.delta();
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();
synchronized (toRun) {
for(Runnable r : toRun){
r.run();
}
toRun.clear();
}
logic.update();
long elapsed = TimeUtils.timeSinceMillis(time);
long target = (long) (1000 / 60f);
delta = Math.max(elapsed, target) / 1000f * 60f;
if (elapsed < target) {
impl.sleep(target - elapsed);
}
synchronized(updateLock) {
while(!rendered) {
impl.wait(updateLock);
}
rendered = false;
}
frame ++;
framesSinceUpdate = 0;
}
} catch (InterruptedException ex) {
Log.info("Stopping logic thread.");
} catch (Throwable ex) {
control.setError(ex);
}
}
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

@@ -231,7 +231,6 @@ public class UI implements ApplicationListener{
showTextInput(title, text, textLength < 0 ? 12 : textLength, def, (field, c) -> true, confirmed);
}
public void showInfoFade(String info){
Table table = new Table();
table.setFillParent(true);

View File

@@ -1,359 +1,523 @@
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 io.anuke.mindustry.io.Maps;
import io.anuke.annotations.Annotations.Nullable;
import io.anuke.arc.*;
import io.anuke.arc.collection.IntArray;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Geometry;
import io.anuke.arc.math.geom.Point2;
import io.anuke.arc.util.*;
import io.anuke.mindustry.ai.*;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.Entities;
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.MapIO;
import io.anuke.mindustry.maps.*;
import io.anuke.mindustry.maps.generators.Generator;
import io.anuke.mindustry.type.Zone;
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.ucore.modules.Module;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Tmp;
import io.anuke.mindustry.world.blocks.BlockPart;
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<>();
public class World implements ApplicationListener{
public final Maps maps = new Maps();
public final BlockIndexer indexer = new BlockIndexer();
public final WaveSpawner spawner = new WaveSpawner();
public final Pathfinder pathfinder = new Pathfinder();
public final Context context = new Context();
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;
public Array<SpawnPoint> getSpawns(){
return spawns;
}
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 init(){
maps.loadLegacyMaps();
}
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();
}
@Override
public void dispose(){
maps.dispose();
}
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 boolean isInvalidMap(){
return invalidMap;
}
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 boolean solid(int x, int y){
Tile tile = tile(x, y);
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;
}
return tile == null || tile.solid();
}
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 passable(int x, int y){
Tile tile = tile(x, y);
return (TileEntity) closest;
}
return tile != null && tile.passable();
}
/**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 wallSolid(int x, int y){
Tile tile = tile(x, y);
return tile == null || tile.block().solid;
}
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
public boolean isAccessible(int x, int y){
return !wallSolid(x, y - 1) || !wallSolid(x, y + 1) || !wallSolid(x - 1, y) || !wallSolid(x + 1, y);
}
int err = dx - dy;
int e2;
while(true){
public Map getMap(){
return currentMap;
}
if(!passable(x0, y0)){
return Tmp.g1.set(x0, y0);
}
if(x0 == x1 && y0 == y1) break;
public void setMap(Map map){
this.currentMap = map;
}
e2 = 2 * err;
if(e2 > -dy){
err = err - dy;
x0 = x0 + sx;
}
public int width(){
return tiles == null ? 0 : tiles.length;
}
if(e2 < dx){
err = err + dx;
y0 = y0 + sy;
}
}
return null;
}
public int height(){
return tiles == null ? 0 : tiles[0].length;
}
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 int unitWidth(){
return width()*tilesize;
}
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
public int unitHeight(){
return height()*tilesize;
}
int err = dx - dy;
int e2;
while(true){
public @Nullable Tile tile(int pos){
return tiles == null ? null : tile(Pos.x(pos), Pos.y(pos));
}
if(cons.accept(x0, y0)) break;
if(x0 == x1 && y0 == y1) break;
public @Nullable Tile tile(int x, int y){
if(tiles == null){
return null;
}
if(!Structs.inBounds(x, y, tiles)) return null;
return tiles[x][y];
}
e2 = 2 * err;
if(e2 > -dy){
err = err - dy;
x0 = x0 + sx;
}
public @Nullable Tile ltile(int x, int y){
Tile tile = tile(x, y);
if(tile == null) return null;
return tile.block().linked(tile);
}
if(e2 < dx){
err = err + dx;
y0 = y0 + sy;
}
}
}
public Tile rawTile(int x, int y){
return tiles[x][y];
}
public interface Raycaster{
boolean accept(int x, int y);
}
public @Nullable Tile tileWorld(float x, float y){
return tile(Math.round(x / tilesize), Math.round(y / tilesize));
}
public @Nullable Tile ltileWorld(float x, float y){
return ltile(Math.round(x / tilesize), Math.round(y / tilesize));
}
public int toTile(float coord){
return Math.round(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 signal the beginning of loading the map with a custom set of tiles. */
public void beginMapLoad(Tile[][] tiles){
this.tiles = tiles;
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(){
prepareTiles(tiles);
for(int x = 0; x < tiles.length; x++){
for(int y = 0; y < tiles[0].length; y++){
Tile tile = tiles[x][y];
tile.updateOcclusion();
if(tile.entity != null){
tile.entity.updateProximity();
}
}
}
addDarkness(tiles);
Entities.getAllGroups().each(group -> group.resize(-finalWorldBounds, -finalWorldBounds, tiles.length * tilesize + finalWorldBounds * 2, tiles[0].length * tilesize + finalWorldBounds * 2));
generating = false;
Events.fire(new WorldLoadEvent());
}
public boolean isGenerating(){
return generating;
}
public boolean isZone(){
return getZone() != null;
}
public Zone getZone(){
return state.rules.zone;
}
public void loadGenerator(Generator generator){
beginMapLoad();
createTiles(generator.width, generator.height);
generator.generate(tiles);
endMapLoad();
}
public void loadMap(Map map){
try{
MapIO.loadMap(map);
}catch(Exception e){
Log.err(e);
if(!headless){
ui.showError("$map.invalid");
Core.app.post(() -> state.set(State.menu));
invalidMap = true;
}
generating = false;
return;
}
this.currentMap = map;
invalidMap = false;
if(!headless){
if(state.teams.get(defaultTeam).cores.size == 0){
ui.showError("$map.nospawn");
invalidMap = true;
}else if(state.rules.pvp){ //pvp maps need two cores to be valid
invalidMap = true;
for(Team team : Team.all){
if(state.teams.get(team).cores.size != 0 && team != defaultTeam){
invalidMap = false;
}
}
if(invalidMap){
ui.showError("$map.nospawn.pvp");
}
}else if(state.rules.attackMode){ //pvp maps need two cores to be valid
invalidMap = state.teams.get(waveTeam).cores.isEmpty();
if(invalidMap){
ui.showError("$map.nospawn.attack");
}
}
}else{
invalidMap = true;
for(Team team : Team.all){
if(state.teams.get(team).cores.size != 0){
invalidMap = false;
}
}
if(invalidMap){
throw new MapException(map, "Map has no cores!");
}
}
if(invalidMap) Core.app.post(() -> state.set(State.menu));
}
public void notifyChanged(Tile tile){
if(!generating){
Core.app.post(() -> Events.fire(new TileChangeEvent(tile)));
}
}
public void removeBlock(Tile tile){
tile.link().getLinkedTiles(other -> other.setBlock(Blocks.air));
}
public void setBlock(Tile tile, Block block, Team team){
setBlock(tile, block, team, 0);
}
public void setBlock(Tile tile, Block block, Team team, int rotation){
tile.setBlock(block, team, rotation);
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.setBlock(BlockPart.get(dx + offsetx, dy + offsety), team);
}
}
}
}
}
}
/**
* Raycast, but with world coordinates.
*/
public Point2 raycastWorld(float x, float y, float x2, float y2){
return raycast(Math.round(x / tilesize), Math.round(y / tilesize),
Math.round(x2 / tilesize), Math.round(y2 / tilesize));
}
/**
* Input is in block coordinates, not world coordinates.
* @return null if no collisions found, block position otherwise.
*/
public Point2 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 raycastEachWorld(float x0, float y0, float x1, float y1, Raycaster cons){
raycastEach(toTile(x0), toTile(y0), toTile(x1), toTile(y1), cons);
}
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 void addDarkness(Tile[][] tiles){
byte[][] dark = new byte[tiles.length][tiles[0].length];
byte[][] writeBuffer = new byte[tiles.length][tiles[0].length];
byte darkIterations = 4;
for(int x = 0; x < tiles.length; x++){
for(int y = 0; y < tiles[0].length; y++){
Tile tile = tiles[x][y];
if(tile.block().solid && !tile.block().synthetic() && tile.block().fillsTile){
dark[x][y] = darkIterations;
}
}
}
for(int i = 0; i < darkIterations; i++){
for(int x = 0; x < tiles.length; x++){
for(int y = 0; y < tiles[0].length; y++){
boolean min = false;
for(Point2 point : Geometry.d4){
int newX = x + point.x, newY = y + point.y;
if(Structs.inBounds(newX, newY, tiles) && dark[newX][newY] < dark[x][y]){
min = true;
break;
}
}
writeBuffer[x][y] = (byte)Math.max(0, dark[x][y] - Mathf.num(min));
}
}
for(int x = 0; x < tiles.length; x++){
System.arraycopy(writeBuffer[x], 0, dark[x], 0, tiles[0].length);
}
}
for(int x = 0; x < tiles.length; x++){
for(int y = 0; y < tiles[0].length; y++){
Tile tile = tiles[x][y];
if(tile.block().solid && !tile.block().synthetic()){
tiles[x][y].rotation(dark[x][y]);
}
}
}
}
/**
* 'Prepares' a tile array by:<br>
* - setting up multiblocks<br>
* - updating occlusion<br>
* Usually used before placing structures on a tile array.
*/
public void prepareTiles(Tile[][] tiles){
//find multiblocks
IntArray multiblocks = new IntArray();
for(int x = 0; x < tiles.length; x++){
for(int y = 0; y < tiles[0].length; y++){
Tile tile = tiles[x][y];
if(tile.block().isMultiblock()){
multiblocks.add(tile.pos());
}
}
}
//place multiblocks now
for(int i = 0; i < multiblocks.size; i++){
int pos = multiblocks.get(i);
int x = Pos.x(pos);
int y = Pos.y(pos);
Block result = tiles[x][y].block();
Team team = tiles[x][y].getTeam();
int offsetx = -(result.size - 1) / 2;
int offsety = -(result.size - 1) / 2;
for(int dx = 0; dx < result.size; dx++){
for(int dy = 0; dy < result.size; dy++){
int worldx = dx + offsetx + x;
int worldy = dy + offsety + y;
if(!(worldx == x && worldy == y)){
Tile toplace = world.tile(worldx, worldy);
if(toplace != null){
toplace.setBlock(BlockPart.get(dx + offsetx, dy + offsety), team);
}
}
}
}
}
}
public interface Raycaster{
boolean accept(int x, int y);
}
class Context implements WorldContext{
@Override
public Tile tile(int x, int y){
return tiles[x][y];
}
@Override
public void resize(int width, int height){
createTiles(width, height);
}
@Override
public Tile create(int x, int y, int floorID, int overlayID, int wallID){
return (tiles[x][y] = new Tile(x, y, floorID, overlayID, wallID));
}
@Override
public boolean isGenerating(){
return World.this.isGenerating();
}
@Override
public void begin(){
beginMapLoad();
}
@Override
public void end(){
endMapLoad();
}
}
}

View File

@@ -0,0 +1,95 @@
package io.anuke.mindustry.editor;
import io.anuke.annotations.Annotations.Struct;
import io.anuke.arc.collection.LongArray;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.gen.TileOp;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.Floor;
import static io.anuke.mindustry.Vars.content;
public class DrawOperation{
private MapEditor editor;
private LongArray array = new LongArray();
public DrawOperation(MapEditor editor) {
this.editor = editor;
}
public boolean isEmpty(){
return array.isEmpty();
}
public void addOperation(long op){
array.add(op);
}
public void undo(){
for(int i = array.size - 1; i >= 0; i--){
updateTile(i);
}
}
public void redo(){
for(int i = 0; i < array.size; i++){
updateTile(i);
}
}
private void updateTile(int i) {
long l = array.get(i);
array.set(i, TileOp.get(TileOp.x(l), TileOp.y(l), TileOp.type(l), getTile(editor.tile(TileOp.x(l), TileOp.y(l)), TileOp.type(l))));
setTile(editor.tile(TileOp.x(l), TileOp.y(l)), TileOp.type(l), TileOp.value(l));
}
short getTile(Tile tile, byte type){
if(type == OpType.floor.ordinal()){
return tile.floorID();
}else if(type == OpType.block.ordinal()){
return tile.blockID();
}else if(type == OpType.rotation.ordinal()){
return tile.rotation();
}else if(type == OpType.team.ordinal()){
return tile.getTeamID();
}else if(type == OpType.overlay.ordinal()){
return tile.overlayID();
}
throw new IllegalArgumentException("Invalid type.");
}
void setTile(Tile tile, byte type, short to){
editor.load(() -> {
if(type == OpType.floor.ordinal()){
tile.setFloor((Floor)content.block(to));
}else if(type == OpType.block.ordinal()){
Block block = content.block(to);
tile.setBlock(block, tile.getTeam(), tile.rotation());
}else if(type == OpType.rotation.ordinal()){
tile.rotation(to);
}else if(type == OpType.team.ordinal()){
tile.setTeam(Team.all[to]);
}else if(type == OpType.overlay.ordinal()){
tile.setOverlayID(to);
}
});
editor.renderer().updatePoint(tile.x, tile.y);
}
@Struct
class TileOpStruct{
short x;
short y;
byte type;
short value;
}
public enum OpType{
floor,
block,
rotation,
team,
overlay
}
}

View File

@@ -0,0 +1,147 @@
package io.anuke.mindustry.editor;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.editor.DrawOperation.OpType;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.gen.TileOp;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.*;
import io.anuke.mindustry.world.modules.*;
import static io.anuke.mindustry.Vars.state;
import static io.anuke.mindustry.Vars.ui;
//TODO somehow remove or replace this class with a more flexible solution
public class EditorTile extends Tile{
public EditorTile(int x, int y, int floor, int overlay, int wall){
super(x, y, floor, overlay, wall);
}
@Override
public void setFloor(Floor type){
if(state.is(State.playing)){
super.setFloor(type);
return;
}
if(type instanceof OverlayFloor){
//don't place on liquids
if(!floor.isLiquid){
setOverlayID(type.id);
}
return;
}
if(floor == type && overlayID() == 0) return;
if(overlayID() != 0) op(OpType.overlay, overlayID());
if(floor != type) op(OpType.floor, floor.id);
super.setFloor(type);
}
@Override
public void setBlock(Block type){
if(state.is(State.playing)){
super.setBlock(type);
return;
}
if(block == type) return;
op(OpType.block, block.id);
if(rotation != 0) op(OpType.rotation, rotation);
if(team != 0) op(OpType.team, team);
super.setBlock(type);
}
@Override
public void setBlock(Block type, Team team, int rotation){
if(state.is(State.playing)){
super.setBlock(type, team, rotation);
return;
}
setBlock(type);
setTeam(team);
rotation(rotation);
}
@Override
public void setTeam(Team team){
if(state.is(State.playing)){
super.setTeam(team);
return;
}
if(getTeamID() == team.ordinal()) return;
op(OpType.team, getTeamID());
super.setTeam(team);
}
@Override
public void rotation(int rotation){
if(state.is(State.playing)){
super.rotation(rotation);
return;
}
if(rotation == rotation()) return;
op(OpType.rotation, rotation());
super.rotation(rotation);
}
@Override
public void setOverlayID(short overlay){
if(state.is(State.playing)){
super.setOverlayID(overlay);
return;
}
if(overlayID() == overlay) return;
op(OpType.overlay, this.overlay);
super.setOverlayID(overlay);
}
@Override
protected void preChanged(){
if(state.is(State.playing)){
super.preChanged();
return;
}
super.setTeam(Team.none);
}
@Override
protected void changed(){
if(state.is(State.playing)){
super.changed();
return;
}
entity = null;
if(block == null){
block = Blocks.air;
}
if(floor == null){
floor = (Floor)Blocks.air;
}
Block block = block();
if(block.hasEntity()){
entity = block.newEntity().init(this, false);
entity.cons = new ConsumeModule(entity);
if(block.hasItems) entity.items = new ItemModule();
if(block.hasLiquids) entity.liquids = new LiquidModule();
if(block.hasPower) entity.power = new PowerModule();
}
}
private void op(OpType type, short value){
ui.editor.editor.addTileOp(TileOp.get(x, y, (byte)type.ordinal(), value));
}
}

View File

@@ -0,0 +1,241 @@
package io.anuke.mindustry.editor;
import io.anuke.arc.collection.IntArray;
import io.anuke.arc.function.*;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Bresenham2;
import io.anuke.arc.util.Structs;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.world.*;
public enum EditorTool{
zoom,
pick{
public void touched(MapEditor editor, int x, int y){
if(!Structs.inBounds(x, y, editor.width(), editor.height())) return;
Tile tile = editor.tile(x, y).link();
editor.drawBlock = tile.block() == Blocks.air ? tile.overlay() == Blocks.air ? tile.floor() : tile.overlay() : tile.block();
}
},
line("replace", "orthogonal"){
@Override
public void touchedLine(MapEditor editor, int x1, int y1, int x2, int y2){
//straight
if(mode == 1){
if(Math.abs(x2 - x1) > Math.abs(y2 - y1)){
y2 = y1;
}else{
x2 = x1;
}
}
Bresenham2.line(x1, y1, x2, y2, (x, y) -> {
if(mode == 0){
//replace
editor.drawBlocksReplace(x, y);
}else{
//normal
editor.drawBlocks(x, y);
}
});
}
},
pencil("replace", "square", "drawteams"){
{
edit = true;
draggable = true;
}
@Override
public void touched(MapEditor editor, int x, int y){
if(mode == -1){
//normal mode
editor.drawBlocks(x, y);
}else if(mode == 0){
//replace mode
editor.drawBlocksReplace(x, y);
}else if(mode == 1){
//square mode
editor.drawBlocks(x, y, true, tile -> true);
}else if(mode == 2){
//draw teams
editor.drawCircle(x, y, tile -> tile.link().setTeam(editor.drawTeam));
}
}
},
eraser("eraseores"){
{
edit = true;
draggable = true;
}
@Override
public void touched(MapEditor editor, int x, int y){
editor.drawCircle(x, y, tile -> {
if(mode == -1){
//erase block
Vars.world.removeBlock(tile);
}else if(mode == 0){
//erase ore
tile.clearOverlay();
}
});
}
},
fill("replaceall", "fillteams"){
{
edit = true;
}
IntArray stack = new IntArray();
@Override
public void touched(MapEditor editor, int x, int y){
if(!Structs.inBounds(x, y, editor.width(), editor.height())) return;
Tile tile = editor.tile(x, y);
if(editor.drawBlock.isMultiblock()){
//don't fill multiblocks, thanks
pencil.touched(editor, x, y);
return;
}
//mode 0 or 1, fill everything with the floor/tile or replace it
if(mode == 0 || mode == -1){
Predicate<Tile> tester;
Consumer<Tile> setter;
if(editor.drawBlock.isOverlay()){
Block dest = tile.overlay();
if(dest == editor.drawBlock) return;
tester = t -> t.overlay() == dest;
setter = t -> t.setOverlay(editor.drawBlock);
}else if(editor.drawBlock.isFloor()){
Block dest = tile.floor();
if(dest == editor.drawBlock) return;
tester = t -> t.floor() == dest;
setter = t -> t.setFloorUnder(editor.drawBlock.asFloor());
}else{
Block dest = tile.block();
if(dest == editor.drawBlock) return;
tester = t -> t.block() == dest;
setter = t -> t.setBlock(editor.drawBlock);
}
//replace only when the mode is 0 using the specified functions
fill(editor, x, y, mode == 0, tester, setter);
}else if(mode == 1){ //mode 1 is team fill
//only fill synthetic blocks, it's meaningless otherwise
if(tile.link().synthetic()){
Team dest = tile.getTeam();
if(dest == editor.drawTeam) return;
fill(editor, x, y, false, t -> t.getTeamID() == dest.ordinal() && t.link().synthetic(), t -> t.setTeam(editor.drawTeam));
}
}
}
void fill(MapEditor editor, int x, int y, boolean replace, Predicate<Tile> tester, Consumer<Tile> filler){
int width = editor.width(), height = editor.height();
if(replace){
//just do it on everything
for(int cx = 0; cx < width; cx++){
for(int cy = 0; cy < height; cy++){
Tile tile = editor.tile(cx, cy);
if(tester.test(tile)){
filler.accept(tile);
}
}
}
}else{
//perform flood fill
int x1;
stack.clear();
stack.add(Pos.get(x, y));
while(stack.size > 0){
int popped = stack.pop();
x = Pos.x(popped);
y = Pos.y(popped);
x1 = x;
while(x1 >= 0 && tester.test(editor.tile(x1, y))) x1--;
x1++;
boolean spanAbove = false, spanBelow = false;
while(x1 < width && tester.test(editor.tile(x1, y))){
filler.accept(editor.tile(x1, y));
if(!spanAbove && y > 0 && tester.test(editor.tile(x1, y - 1))){
stack.add(Pos.get(x1, y - 1));
spanAbove = true;
}else if(spanAbove && !tester.test(editor.tile(x1, y - 1))){
spanAbove = false;
}
if(!spanBelow && y < height - 1 && tester.test(editor.tile(x1, y + 1))){
stack.add(Pos.get(x1, y + 1));
spanBelow = true;
}else if(spanBelow && y < height - 1 && !tester.test(editor.tile(x1, y + 1))){
spanBelow = false;
}
x1++;
}
}
}
}
},
spray("replace"){
final double chance = 0.012;
{
edit = true;
draggable = true;
}
@Override
public void touched(MapEditor editor, int x, int y){
//floor spray
if(editor.drawBlock.isFloor()){
editor.drawCircle(x, y, tile -> {
if(Mathf.chance(chance)){
tile.setFloor(editor.drawBlock.asFloor());
}
});
}else if(mode == 0){ //replace-only mode, doesn't affect air
editor.drawBlocks(x, y, tile -> Mathf.chance(chance) && tile.block() != Blocks.air);
}else{
editor.drawBlocks(x, y, tile -> Mathf.chance(chance));
}
}
};
/** All the internal alternate placement modes of this tool. */
public final String[] altModes;
/** The current alternate placement mode. -1 is the standard mode, no changes.*/
public int mode = -1;
/** Whether this tool causes canvas changes when touched.*/
public boolean edit;
/** Whether this tool should be dragged across the canvas when the mouse moves.*/
public boolean draggable;
EditorTool(){
this(new String[]{});
}
EditorTool(String... altModes){
this.altModes = altModes;
}
public void touched(MapEditor editor, int x, int y){}
public void touchedLine(MapEditor editor, int x1, int y1, int x2, int y2){}
}

View File

@@ -0,0 +1,405 @@
package io.anuke.mindustry.editor;
import io.anuke.arc.collection.StringMap;
import io.anuke.arc.files.FileHandle;
import io.anuke.arc.function.Consumer;
import io.anuke.arc.function.Predicate;
import io.anuke.arc.graphics.Pixmap;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Structs;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.gen.TileOp;
import io.anuke.mindustry.io.LegacyMapIO;
import io.anuke.mindustry.io.MapIO;
import io.anuke.mindustry.maps.Map;
import io.anuke.mindustry.world.*;
import io.anuke.mindustry.world.blocks.BlockPart;
import io.anuke.mindustry.world.blocks.Floor;
import static io.anuke.mindustry.Vars.world;
public class MapEditor{
public static final int[] brushSizes = {1, 2, 3, 4, 5, 9, 15, 20};
private final Context context = new Context();
private StringMap tags = new StringMap();
private MapRenderer renderer = new MapRenderer(this);
private OperationStack stack = new OperationStack();
private DrawOperation currentOp;
private boolean loading;
public int brushSize = 1;
public int rotation;
public Block drawBlock = Blocks.stone;
public Team drawTeam = Team.blue;
public StringMap getTags(){
return tags;
}
public void beginEdit(int width, int height){
reset();
loading = true;
createTiles(width, height);
renderer.resize(width(), height());
loading = false;
}
public void beginEdit(Map map){
reset();
loading = true;
tags.putAll(map.tags);
MapIO.loadMap(map, context);
checkLinkedTiles();
renderer.resize(width(), height());
loading = false;
}
public void beginEdit(Pixmap pixmap){
reset();
createTiles(pixmap.getWidth(), pixmap.getHeight());
load(() -> LegacyMapIO.readPixmap(pixmap, tiles()));
renderer.resize(width(), height());
}
//adds missing blockparts
public void checkLinkedTiles(){
Tile[][] tiles = world.getTiles();
//clear block parts first
for(int x = 0; x < width(); x++){
for(int y = 0; y < height(); y++){
if(tiles[x][y].block() instanceof BlockPart){
tiles[x][y].setBlock(Blocks.air);
}
}
}
//set up missing blockparts
for(int x = 0; x < width(); x++){
for(int y = 0; y < height(); y++){
if(tiles[x][y].block().isMultiblock()){
world.setBlock(tiles[x][y], tiles[x][y].block(), tiles[x][y].getTeam());
}
}
}
}
public void load(Runnable r){
loading = true;
r.run();
loading = false;
}
/** Creates a 2-D array of EditorTiles with stone as the floor block. */
private void createTiles(int width, int height){
Tile[][] tiles = world.createTiles(width, height);
for(int x = 0; x < width; x++){
for(int y = 0; y < height; y++){
tiles[x][y] = new EditorTile(x, y, Blocks.stone.id, (short)0, (short)0);
}
}
}
public Map createMap(FileHandle file){
return new Map(file, width(), height(), new StringMap(tags), true);
}
private void reset(){
clearOp();
brushSize = 1;
drawBlock = Blocks.stone;
tags = new StringMap();
}
public Tile[][] tiles(){
return world.getTiles();
}
public Tile tile(int x, int y){
return world.rawTile(x, y);
}
public int width(){
return world.width();
}
public int height(){
return world.height();
}
public void drawBlocksReplace(int x, int y){
drawBlocks(x, y, tile -> tile.block() != Blocks.air || drawBlock.isFloor());
}
public void drawBlocks(int x, int y){
drawBlocks(x, y, false, tile -> true);
}
public void drawBlocks(int x, int y, Predicate<Tile> tester){
drawBlocks(x, y, false, tester);
}
public void drawBlocks(int x, int y, boolean square, Predicate<Tile> tester){
if(drawBlock.isMultiblock()){
x = Mathf.clamp(x, (drawBlock.size - 1) / 2, width() - drawBlock.size / 2 - 1);
y = Mathf.clamp(y, (drawBlock.size - 1) / 2, height() - drawBlock.size / 2 - 1);
int offsetx = -(drawBlock.size - 1) / 2;
int offsety = -(drawBlock.size - 1) / 2;
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(Structs.inBounds(worldx, worldy, width(), height())){
Tile tile = tile(worldx, worldy);
Block block = tile.block();
//bail out if there's anything blocking the way
if(block.isMultiblock() || block instanceof BlockPart){
return;
}
renderer.updatePoint(worldx, worldy);
}
}
}
world.setBlock(tile(x, y), drawBlock, drawTeam);
}else{
boolean isFloor = drawBlock.isFloor() && drawBlock != Blocks.air;
Consumer<Tile> drawer = tile -> {
if(!tester.test(tile)) return;
//remove linked tiles blocking the way
if(!isFloor && (tile.isLinked() || tile.block().isMultiblock())){
world.removeBlock(tile.link());
}
if(isFloor){
tile.setFloor(drawBlock.asFloor());
}else{
tile.setBlock(drawBlock);
if(drawBlock.synthetic()){
tile.setTeam(drawTeam);
}
if(drawBlock.rotate){
tile.rotation((byte)rotation);
}
}
};
if(square){
drawSquare(x, y, drawer);
}else{
drawCircle(x, y, drawer);
}
}
}
public void drawCircle(int x, int y, Consumer<Tile> drawer){
for(int rx = -brushSize; rx <= brushSize; rx++){
for(int ry = -brushSize; ry <= brushSize; ry++){
if(Mathf.dst2(rx, ry) <= (brushSize - 0.5f) * (brushSize - 0.5f)){
int wx = x + rx, wy = y + ry;
if(wx < 0 || wy < 0 || wx >= width() || wy >= height()){
continue;
}
drawer.accept(tile(wx, wy));
}
}
}
}
public void drawSquare(int x, int y, Consumer<Tile> drawer){
for(int rx = -brushSize; rx <= brushSize; rx++){
for(int ry = -brushSize; ry <= brushSize; ry++){
int wx = x + rx, wy = y + ry;
if(wx < 0 || wy < 0 || wx >= width() || wy >= height()){
continue;
}
drawer.accept(tile(wx, wy));
}
}
}
public void draw_DEPRECATED(int x, int y, boolean paint, Block drawBlock, double chance){
boolean isfloor = drawBlock instanceof Floor && drawBlock != Blocks.air;
Tile[][] tiles = world.getTiles();
if(drawBlock.isMultiblock()){
x = Mathf.clamp(x, (drawBlock.size - 1) / 2, width() - drawBlock.size / 2 - 1);
y = Mathf.clamp(y, (drawBlock.size - 1) / 2, height() - drawBlock.size / 2 - 1);
int offsetx = -(drawBlock.size - 1) / 2;
int offsety = -(drawBlock.size - 1) / 2;
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(Structs.inBounds(worldx, worldy, width(), height())){
Tile tile = tiles[worldx][worldy];
Block block = tile.block();
//bail out if there's anything blocking the way
if(block.isMultiblock() || block instanceof BlockPart){
return;
}
renderer.updatePoint(worldx, worldy);
}
}
}
world.setBlock(tiles[x][y], drawBlock, drawTeam);
}else{
for(int rx = -brushSize; rx <= brushSize; rx++){
for(int ry = -brushSize; ry <= brushSize; ry++){
if(Mathf.dst(rx, ry) <= brushSize - 0.5f && (chance >= 0.999 || Mathf.chance(chance))){
int wx = x + rx, wy = y + ry;
if(wx < 0 || wy < 0 || wx >= width() || wy >= height() || (paint && !isfloor && tiles[wx][wy].block() == Blocks.air)){
continue;
}
Tile tile = tiles[wx][wy];
if(!isfloor && (tile.isLinked() || tile.block().isMultiblock())){
world.removeBlock(tile.link());
}
if(isfloor){
tile.setFloor((Floor)drawBlock);
}else{
tile.setBlock(drawBlock);
if(drawBlock.synthetic()){
tile.setTeam(drawTeam);
}
if(drawBlock.rotate){
tile.rotation((byte)rotation);
}
}
}
}
}
}
}
public MapRenderer renderer(){
return renderer;
}
public void resize(int width, int height){
clearOp();
Tile[][] previous = world.getTiles();
int offsetX = -(width - width()) / 2, offsetY = -(height - height()) / 2;
loading = true;
Tile[][] tiles = world.createTiles(width, height);
for(int x = 0; x < width; x++){
for(int y = 0; y < height; y++){
int px = offsetX + x, py = offsetY + y;
if(Structs.inBounds(px, py, previous.length, previous[0].length)){
tiles[x][y] = previous[px][py];
tiles[x][y].x = (short)x;
tiles[x][y].y = (short)y;
}else{
tiles[x][y] = new EditorTile(x, y, Blocks.stone.id, (short)0, (short)0);
}
}
}
renderer.resize(width, height);
loading = false;
}
public void clearOp(){
stack.clear();
}
public void undo(){
if(stack.canUndo()){
stack.undo();
}
}
public void redo(){
if(stack.canRedo()){
stack.redo();
}
}
public boolean canUndo(){
return stack.canUndo();
}
public boolean canRedo(){
return stack.canRedo();
}
public void flushOp(){
if(currentOp == null || currentOp.isEmpty()) return;
stack.add(currentOp);
currentOp = null;
}
public void addTileOp(long data){
if(loading) return;
if(currentOp == null) currentOp = new DrawOperation(this);
currentOp.addOperation(data);
renderer.updatePoint(TileOp.x(data), TileOp.y(data));
}
class Context implements WorldContext{
@Override
public Tile tile(int x, int y){
return world.tile(x, y);
}
@Override
public void resize(int width, int height){
world.createTiles(width, height);
}
@Override
public Tile create(int x, int y, int floorID, int overlayID, int wallID){
return (tiles()[x][y] = new EditorTile(x, y, floorID, overlayID, wallID));
}
@Override
public boolean isGenerating(){
return world.isGenerating();
}
@Override
public void begin(){
world.beginMapLoad();
}
@Override
public void end(){
world.endMapLoad();
}
}
}

View File

@@ -0,0 +1,658 @@
package io.anuke.mindustry.editor;
import io.anuke.arc.Core;
import io.anuke.arc.collection.Array;
import io.anuke.arc.collection.StringMap;
import io.anuke.arc.files.FileHandle;
import io.anuke.arc.function.Consumer;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.Pixmap;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.arc.input.KeyCode;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Vector2;
import io.anuke.arc.scene.actions.Actions;
import io.anuke.arc.scene.event.Touchable;
import io.anuke.arc.scene.style.TextureRegionDrawable;
import io.anuke.arc.scene.ui.*;
import io.anuke.arc.scene.ui.TextButton.TextButtonStyle;
import io.anuke.arc.scene.ui.layout.Table;
import io.anuke.arc.scene.ui.layout.Unit;
import io.anuke.arc.util.*;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.core.Platform;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.graphics.Pal;
import io.anuke.mindustry.io.JsonIO;
import io.anuke.mindustry.io.MapIO;
import io.anuke.mindustry.maps.Map;
import io.anuke.mindustry.ui.dialogs.FileChooser;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Block.Icon;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.OverlayFloor;
import io.anuke.mindustry.world.blocks.storage.CoreBlock;
import static io.anuke.mindustry.Vars.*;
public class MapEditorDialog extends Dialog implements Disposable{
public final MapEditor editor;
private MapView view;
private MapInfoDialog infoDialog;
private MapLoadDialog loadDialog;
private MapResizeDialog resizeDialog;
private MapGenerateDialog generateDialog;
private ScrollPane pane;
private FloatingDialog menu;
private Rules lastSavedRules;
private boolean saved = false;
private boolean shownWithMap = false;
private Array<Block> blocksOut = new Array<>();
public MapEditorDialog(){
super("", "dialog");
background("dark");
editor = new MapEditor();
view = new MapView(editor);
infoDialog = new MapInfoDialog(editor);
generateDialog = new MapGenerateDialog(editor);
menu = new FloatingDialog("$menu");
menu.addCloseButton();
float isize = iconsize;
float swidth = 180f;
menu.cont.table(t -> {
t.defaults().size(swidth, 60f).padBottom(5).padRight(5).padLeft(5);
t.addImageTextButton("$editor.savemap", "icon-floppy-16", isize, this::save);
t.addImageTextButton("$editor.mapinfo", "icon-pencil", isize, () -> {
infoDialog.show();
menu.hide();
});
t.row();
t.addImageTextButton("$editor.generate", "icon-editor", isize, () -> {
generateDialog.show();
menu.hide();
});
t.addImageTextButton("$editor.resize", "icon-resize", isize, () -> {
resizeDialog.show();
menu.hide();
});
t.row();
t.addImageTextButton("$editor.import", "icon-load-map", isize, () ->
createDialog("$editor.import",
"$editor.importmap", "$editor.importmap.description", "icon-load-map", (Runnable)loadDialog::show,
"$editor.importfile", "$editor.importfile.description", "icon-file", (Runnable)() ->
Platform.instance.showFileChooser("$editor.loadmap", "Map Files", file -> ui.loadAnd(() -> {
world.maps.tryCatchMapError(() -> {
if(MapIO.isImage(file)){
ui.showInfo("$editor.errorimage");
}else if(file.extension().equalsIgnoreCase(oldMapExtension)){
editor.beginEdit(world.maps.makeLegacyMap(file));
}else{
editor.beginEdit(MapIO.createMap(file, true));
}
});
}), true, FileChooser.anyMapFiles),
"$editor.importimage", "$editor.importimage.description", "icon-file-image", (Runnable)() ->
Platform.instance.showFileChooser("$loadimage", "Image Files", file ->
ui.loadAnd(() -> {
try{
Pixmap pixmap = new Pixmap(file);
editor.beginEdit(pixmap);
pixmap.dispose();
}catch(Exception e){
ui.showError(Core.bundle.format("editor.errorload", Strings.parseException(e, true)));
Log.err(e);
}
}), true, FileChooser.pngFiles))
);
t.addImageTextButton("$editor.export", "icon-save-map", isize, () ->
Platform.instance.showFileChooser("$editor.savemap", "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, editor.createMap(result));
}catch(Exception e){
ui.showError(Core.bundle.format("editor.errorsave", Strings.parseException(e, true)));
Log.err(e);
}
});
}, false, FileChooser.mapFiles));
});
menu.cont.row();
menu.cont.addImageTextButton("$editor.ingame", "icon-arrow", isize, this::playtest).padTop(-5).size(swidth * 2f + 10, 60f);
menu.cont.row();
menu.cont.addImageTextButton("$quit", "icon-back", isize, () -> {
tryExit();
menu.hide();
}).size(swidth * 2f + 10, 60f);
resizeDialog = new MapResizeDialog(editor, (x, y) -> {
if(!(editor.width() == x && editor.height() == y)){
ui.loadAnd(() -> {
editor.resize(x, y);
});
}
});
loadDialog = new MapLoadDialog(map -> ui.loadAnd(() -> {
try{
editor.beginEdit(map);
}catch(Exception e){
ui.showError(Core.bundle.format("editor.errorload", Strings.parseException(e, true)));
Log.err(e);
}
}));
setFillParent(true);
clearChildren();
margin(0);
shown(this::build);
update(() -> {
if(Core.scene.getKeyboardFocus() instanceof Dialog && Core.scene.getKeyboardFocus() != this){
return;
}
Vector2 v = pane.stageToLocalCoordinates(Core.input.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;
if(!Core.settings.getBool("landscape")) Platform.instance.beginForceLandscape();
editor.clearOp();
Core.scene.setScrollFocus(view);
if(!shownWithMap){
//clear units, rules and other unnecessary stuff
logic.reset();
state.rules = new Rules();
editor.beginEdit(200, 200);
}
shownWithMap = false;
Time.runTask(10f, Platform.instance::updateRPC);
});
hidden(() -> {
editor.clearOp();
Platform.instance.updateRPC();
if(!Core.settings.getBool("landscape")) Platform.instance.endForceLandscape();
});
}
@Override
protected void drawBackground(float x, float y){
drawDefaultBackground(x, y);
}
public void resumeEditing(){
state.set(State.menu);
shownWithMap = true;
show();
state.rules = (lastSavedRules == null ? new Rules() : lastSavedRules);
lastSavedRules = null;
editor.renderer().updateAll();
}
private void playtest(){
menu.hide();
ui.loadAnd(() -> {
lastSavedRules = state.rules;
hide();
//only reset the player; logic.reset() will clear entities, which we do not want
state.teams = new Teams();
player.reset();
state.rules = Gamemode.editor.apply(lastSavedRules.copy());
world.setMap(new Map(StringMap.of(
"name", "Editor Playtesting",
"width", editor.width(),
"height", editor.height()
)));
world.endMapLoad();
//add entities so they update. is this really needed?
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
Tile tile = world.rawTile(x, y);
if(tile.entity != null){
tile.entity.add();
}
}
}
player.set(world.width() * tilesize/2f, world.height() * tilesize/2f);
player.setDead(false);
logic.play();
});
}
private void save(){
String name = editor.getTags().get("name", "").trim();
editor.getTags().put("rules", JsonIO.write(state.rules));
player.dead = true;
if(name.isEmpty()){
infoDialog.show();
Core.app.post(() -> ui.showError("$editor.save.noname"));
}else{
Map map = world.maps.all().find(m -> m.name().equals(name));
if(map != null && !map.custom){
handleSaveBuiltin(map);
}else{
world.maps.saveMap(editor.getTags());
ui.showInfoFade("$editor.saved");
}
}
menu.hide();
saved = true;
}
/** Called when a built-in map save is attempted.*/
protected void handleSaveBuiltin(Map map){
ui.showError("$editor.save.overwrite");
}
/**
* Argument format:
* 0) button name
* 1) description
* 2) icon name
* 3) listener
*/
private void createDialog(String title, Object... arguments){
FloatingDialog dialog = new FloatingDialog(title);
float h = 90f;
dialog.cont.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];
Runnable listenable = (Runnable)arguments[i + 3];
TextButton button = dialog.cont.addButton(name, () -> {
listenable.run();
dialog.hide();
menu.hide();
}).left().margin(0).get();
button.clearChildren();
button.addImage(iconname).size(iconsize).padLeft(10);
button.table(t -> {
t.add(name).growX().wrap();
t.row();
t.add(description).color(Color.GRAY).growX().wrap();
}).growX().pad(10f).padLeft(5);
button.row();
dialog.cont.row();
}
dialog.addCloseButton();
dialog.show();
}
@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(FileHandle file){
ui.loadAnd(() -> {
try{
shownWithMap = true;
editor.beginEdit(MapIO.createMap(file, true));
show();
}catch(Exception e){
Log.err(e);
ui.showError(Core.bundle.format("editor.errorload", Strings.parseException(e, true)));
}
});
}
public MapView getView(){
return view;
}
public void resetSaved(){
saved = false;
}
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(Core.graphics.getHeight(), Core.graphics.getWidth()) / amount / Unit.dp.scl(1f)) :
Math.min(Core.graphics.getDisplayMode().height / amount, baseSize);
clearChildren();
table(cont -> {
cont.left();
cont.table(mid -> {
mid.top();
Table tools = new Table().top();
ButtonGroup<ImageButton> group = new ButtonGroup<>();
Consumer<EditorTool> addTool = tool -> {
Table[] lastTable = {null};
ImageButton button = new ImageButton("icon-" + tool.name() + "-small", "clear-toggle");
button.clicked(() -> {
view.setTool(tool);
if(lastTable[0] != null){
lastTable[0].remove();
}
});
button.resizeImage(iconsizesmall);
button.update(() -> button.setChecked(view.getTool() == tool));
group.add(button);
if(tool.altModes.length > 0){
button.clicked(l -> {
if(!mobile){
//desktop: rightclick
l.setButton(KeyCode.MOUSE_RIGHT);
}
}, e -> {
//need to double tap
if(mobile && e.getTapCount() < 2){
return;
}
if(lastTable[0] != null){
lastTable[0].remove();
}
Table table = new Table("dialogDim");
table.defaults().size(300f, 70f);
for(int i = 0; i < tool.altModes.length; i++){
int mode = i;
String name = tool.altModes[i];
table.addButton(b -> {
b.left();
b.marginLeft(6);
b.setStyle(Core.scene.skin.get("clear-toggle", TextButtonStyle.class));
b.add(Core.bundle.get("toolmode." + name)).left();
b.row();
b.add(Core.bundle.get("toolmode." + name + ".description")).color(Color.LIGHT_GRAY).left();
}, () -> {
tool.mode = (tool.mode == mode ? -1 : mode);
table.remove();
}).update(b -> b.setChecked(tool.mode == mode));
table.row();
}
table.update(() -> {
Vector2 v = button.localToStageCoordinates(Tmp.v1.setZero());
table.setPosition(v.x, v.y, Align.topLeft);
if(!isShown()){
table.remove();
lastTable[0] = null;
}
});
table.pack();
table.act(Core.graphics.getDeltaTime());
addChild(table);
lastTable[0] = table;
});
}
Label mode = new Label("");
mode.setColor(Pal.remove);
mode.update(() -> mode.setText(tool.mode == -1 ? "" : "M" + (tool.mode + 1) + " "));
mode.setAlignment(Align.bottomRight, Align.bottomRight);
mode.touchable(Touchable.disabled);
tools.stack(button, mode);
};
tools.defaults().size(size, size);
tools.addImageButton("icon-menu-large-small", "clear", iconsizesmall, menu::show);
ImageButton grid = tools.addImageButton("icon-grid-small", "clear-toggle", iconsizesmall, () -> view.setGrid(!view.isGrid())).get();
addTool.accept(EditorTool.zoom);
tools.row();
ImageButton undo = tools.addImageButton("icon-undo-small", "clear", iconsizesmall, editor::undo).get();
ImageButton redo = tools.addImageButton("icon-redo-small", "clear", iconsizesmall, editor::redo).get();
addTool.accept(EditorTool.pick);
tools.row();
undo.setDisabled(() -> !editor.canUndo());
redo.setDisabled(() -> !editor.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.spray);
ImageButton rotate = tools.addImageButton("icon-arrow-16-small", "clear", iconsizesmall, () -> editor.rotation = (editor.rotation + 1) % 4).get();
rotate.getImage().update(() -> {
rotate.getImage().setRotation(editor.rotation * 90);
rotate.getImage().setOrigin(Align.center);
});
tools.row();
tools.table("underline", t -> t.add("$editor.teams"))
.colspan(3).height(40).width(size * 3f + 3f).padBottom(3);
tools.row();
ButtonGroup<ImageButton> teamgroup = new ButtonGroup<>();
int i = 0;
for(Team team : Team.all){
ImageButton button = new ImageButton("white", "clear-toggle-partial");
button.margin(4f);
button.getImageCell().grow();
button.getStyle().imageUpColor = team.color;
button.clicked(() -> editor.drawTeam = team);
button.update(() -> button.setChecked(editor.drawTeam == team));
teamgroup.add(button);
tools.add(button);
if(i++ % 3 == 2) tools.row();
}
mid.add(tools).top().padBottom(-6);
mid.row();
mid.table("underline", t -> {
Slider slider = new Slider(0, MapEditor.brushSizes.length - 1, 1, false);
slider.moved(f -> editor.brushSize = MapEditor.brushSizes[(int)(float)f]);
t.top();
t.add("$editor.brush");
t.row();
t.add(slider).width(size * 3f - 20).padTop(4f);
}).padTop(5).growX().top();
}).margin(0).left().growY();
cont.table(t -> t.add(view).grow()).grow();
cont.table(this::addBlockSelection).right().growY();
}).grow();
}
private void doInput(){
if(Core.input.ctrl()){
//alt mode select
//TODO these keycode are unusable, tweak later
for(int i = 0; i < view.getTool().altModes.length + 1; i++){
if(Core.input.keyTap(KeyCode.valueOf("NUM_" + (i + 1)))){
view.getTool().mode = i - 1;
}
}
}else{
//tool select
for(int i = 0; i < EditorTool.values().length; i++){
if(Core.input.keyTap(KeyCode.valueOf("NUM_" + (i + 1)))){
view.setTool(EditorTool.values()[i]);
break;
}
}
}
if(Core.input.keyTap(KeyCode.ESCAPE)){
if(!menu.isShown()){
menu.show();
}
}
if(Core.input.keyTap(KeyCode.R)){
editor.rotation = Mathf.mod(editor.rotation + 1, 4);
}
if(Core.input.keyTap(KeyCode.E)){
editor.rotation = Mathf.mod(editor.rotation - 1, 4);
}
//ctrl keys (undo, redo, save)
if(Core.input.ctrl()){
if(Core.input.keyTap(KeyCode.Z)){
editor.undo();
}
if(Core.input.keyTap(KeyCode.Y)){
editor.redo();
}
if(Core.input.keyTap(KeyCode.S)){
save();
}
if(Core.input.keyTap(KeyCode.G)){
view.setGrid(!view.isGrid());
}
}
}
private void tryExit(){
if(!saved){
ui.showConfirm("$confirm", "$editor.unsaved", this::hide);
}else{
hide();
}
}
private void addBlockSelection(Table table){
Table content = new Table();
pane = new ScrollPane(content);
pane.setFadeScrollBars(false);
pane.setOverscroll(true, false);
ButtonGroup<ImageButton> group = new ButtonGroup<>();
int i = 0;
blocksOut.clear();
blocksOut.addAll(Vars.content.blocks());
blocksOut.sort((b1, b2) -> {
int core = -Boolean.compare(b1 instanceof CoreBlock, b2 instanceof CoreBlock);
if(core != 0) return core;
int synth = Boolean.compare(b1.synthetic(), b2.synthetic());
if(synth != 0) return synth;
int ore = Boolean.compare(b1 instanceof OverlayFloor, b2 instanceof OverlayFloor);
if(ore != 0) return ore;
return Integer.compare(b1.id, b2.id);
});
for(Block block : blocksOut){
TextureRegion region = block.icon(Icon.medium);
if(!Core.atlas.isFound(region)) continue;
ImageButton button = new ImageButton("white", "clear-toggle");
button.getStyle().imageUp = new TextureRegionDrawable(region);
button.clicked(() -> editor.drawBlock = block);
button.resizeImage(8 * 4f);
button.update(() -> button.setChecked(editor.drawBlock == block));
group.add(button);
content.add(button).size(50f);
if(++i % 4 == 0){
content.row();
}
}
group.getButtons().get(2).setChecked(true);
table.table("underline", extra -> extra.labelWrap(() -> editor.drawBlock.localizedName).width(200f).center()).growX();
table.row();
table.add(pane).growY().fillX();
}
}

View File

@@ -0,0 +1,368 @@
package io.anuke.mindustry.editor;
import io.anuke.arc.Core;
import io.anuke.arc.collection.Array;
import io.anuke.arc.function.Supplier;
import io.anuke.arc.graphics.Pixmap;
import io.anuke.arc.graphics.Pixmap.Format;
import io.anuke.arc.graphics.Texture;
import io.anuke.arc.scene.ui.Image;
import io.anuke.arc.scene.ui.layout.Stack;
import io.anuke.arc.scene.ui.layout.Table;
import io.anuke.arc.util.Scaling;
import io.anuke.arc.util.async.AsyncExecutor;
import io.anuke.arc.util.async.AsyncResult;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.editor.generation.*;
import io.anuke.mindustry.editor.generation.GenerateFilter.GenerateInput;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.graphics.Pal;
import io.anuke.mindustry.io.MapIO;
import io.anuke.mindustry.ui.BorderImage;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.Floor;
import static io.anuke.mindustry.Vars.*;
@SuppressWarnings("unchecked")
public class MapGenerateDialog extends FloatingDialog{
private final Supplier<GenerateFilter>[] filterTypes = new Supplier[]{
NoiseFilter::new, ScatterFilter::new, TerrainFilter::new, DistortFilter::new,
RiverNoiseFilter::new, OreFilter::new, MedianFilter::new, BlendFilter::new
};
private final MapEditor editor;
private Pixmap pixmap;
private Texture texture;
private GenerateInput input = new GenerateInput();
private Array<GenerateFilter> filters = new Array<>();
private int scaling = mobile ? 3 : 1;
private Table filterTable;
private AsyncExecutor executor = new AsyncExecutor(1);
private AsyncResult<Void> result;
private boolean generating;
private GenTile returnTile = new GenTile();
private GenTile[][] buffer1, buffer2;
public MapGenerateDialog(MapEditor editor){
super("$editor.generate");
this.editor = editor;
shown(this::setup);
addCloseButton();
buttons.addButton("$editor.apply", () -> {
ui.loadAnd(() -> {
apply();
hide();
});
}).size(160f, 64f);
buttons.addButton("$editor.randomize", () -> {
for(GenerateFilter filter : filters){
filter.randomize();
}
update();
}).size(160f, 64f);
buttons.addImageTextButton("$add", "icon-add", iconsize, this::showAdd).height(64f).width(140f);
}
void setup(){
if(pixmap != null){
pixmap.dispose();
texture.dispose();
pixmap = null;
texture = null;
}
pixmap = new Pixmap(editor.width() / scaling, editor.height() / scaling, Format.RGBA8888);
texture = new Texture(pixmap);
cont.clear();
cont.table("flat", t -> {
t.margin(8f);
t.stack(new BorderImage(texture){{
setScaling(Scaling.fit);
}}, new Stack(){{
add(new Image("loadDim"));
add(new Image("icon-refresh"){{
setScaling(Scaling.none);
}});
visible(() -> generating && !updateEditorOnChange);
}}).size(mobile ? 300f : 400f).padRight(6);
t.pane(p -> filterTable = p).width(300f).get().setScrollingDisabled(true, false);
}).grow();
buffer1 = create();
buffer2 = create();
update();
rebuildFilters();
}
GenTile[][] create(){
GenTile[][] out = new GenTile[editor.width() / scaling][editor.height() / scaling];
for(int x = 0; x < out.length; x++){
for(int y = 0; y < out[0].length; y++){
out[x][y] = new GenTile();
}
}
return out;
}
void rebuildFilters(){
filterTable.clearChildren();
filterTable.top();
for(GenerateFilter filter : filters){
filterTable.table(t -> {
t.add(filter.name()).padTop(5).color(Pal.accent).growX().left();
t.row();
t.table(b -> {
b.left();
b.defaults().size(50f);
b.addImageButton("icon-refresh-small", iconsizesmall, () -> {
filter.randomize();
update();
});
b.addImageButton("icon-arrow-up-small", iconsizesmall, () -> {
int idx = filters.indexOf(filter);
filters.swap(idx, Math.max(0, idx - 1));
rebuildFilters();
update();
});
b.addImageButton("icon-arrow-down-small", iconsizesmall, () -> {
int idx = filters.indexOf(filter);
filters.swap(idx, Math.min(filters.size - 1, idx + 1));
rebuildFilters();
update();
});
b.addImageButton("icon-trash-small", iconsizesmall, () -> {
filters.remove(filter);
rebuildFilters();
update();
});
}).growX();
}).fillX();
filterTable.row();
filterTable.table("underline", f -> {
f.left();
for(FilterOption option : filter.options){
option.changed = this::update;
f.table(t -> {
t.left();
option.build(t);
}).growX().left();
f.row();
}
}).pad(3).padTop(0).width(280f);
filterTable.row();
}
if(filters.isEmpty()){
filterTable.add("$filters.empty").wrap().width(200f);
}
}
void showAdd(){
FloatingDialog selection = new FloatingDialog("$add");
selection.setFillParent(false);
selection.cont.defaults().size(210f, 60f);
int i = 0;
for(Supplier<GenerateFilter> gen : filterTypes){
GenerateFilter filter = gen.get();
selection.cont.addButton(filter.name(), () -> {
filters.add(filter);
rebuildFilters();
update();
selection.hide();
});
if(++i % 2 == 0) selection.cont.row();
}
selection.cont.addButton("Default Ores", () -> {
int index = 0;
for(Block block : new Block[]{Blocks.oreCopper, Blocks.oreCoal, Blocks.oreLead, Blocks.oreTitanium, Blocks.oreThorium}){
OreFilter filter = new OreFilter();
filter.threshold += index ++ * 0.02f;
filter.ore = block;
filters.add(filter);
}
rebuildFilters();
update();
selection.hide();
});
selection.addCloseButton();
selection.show();
}
GenTile dset(Tile tile){
returnTile.set(tile);
return returnTile;
}
void apply(){
if(result != null){
result.get();
}
buffer1 = null;
buffer2 = null;
generating = false;
if(pixmap != null){
pixmap.dispose();
texture.dispose();
pixmap = null;
texture = null;
}
//writeback buffer
GenTile[][] writeTiles = new GenTile[editor.width()][editor.height()];
for(int x = 0; x < editor.width(); x++){
for(int y = 0; y < editor.height(); y++){
writeTiles[x][y] = new GenTile();
}
}
for(GenerateFilter filter : filters){
input.setFilter(filter, editor.width(), editor.height(), 1, (x, y) -> dset(editor.tile(x, y)));
//write to buffer
for(int x = 0; x < editor.width(); x++){
for(int y = 0; y < editor.height(); y++){
Tile tile = editor.tile(x, y);
input.begin(editor, x, y, tile.floor(), tile.block(), tile.overlay());
filter.apply(input);
writeTiles[x][y].set(input.floor, input.block, input.ore, tile.getTeam(), tile.rotation());
}
}
editor.load(() -> {
//read from buffer back into tiles
for(int x = 0; x < editor.width(); x++){
for(int y = 0; y < editor.height(); y++){
Tile tile = editor.tile(x, y);
GenTile write = writeTiles[x][y];
tile.rotation(write.rotation);
tile.setFloor((Floor)content.block(write.floor));
tile.setBlock(content.block(write.block));
tile.setTeam(Team.all[write.team]);
tile.setOverlay(content.block(write.ore));
}
}
});
}
//reset undo stack as generation... messes things up
editor.load(editor::checkLinkedTiles);
editor.renderer().updateAll();
editor.clearOp();
}
void update(){
if(generating){
return;
}
Array<GenerateFilter> copy = new Array<>(filters);
result = executor.submit(() -> {
try{
generating = true;
if(!filters.isEmpty()){
//write to buffer1 for reading
for(int px = 0; px < pixmap.getWidth(); px++){
for(int py = 0; py < pixmap.getHeight(); py++){
buffer1[px][py].set(editor.tile(px * scaling, py * scaling));
}
}
}
for(GenerateFilter filter : copy){
input.setFilter(filter, pixmap.getWidth(), pixmap.getHeight(), scaling, (x, y) -> buffer1[x][y]);
//read from buffer1 and write to buffer2
for(int px = 0; px < pixmap.getWidth(); px++){
for(int py = 0; py < pixmap.getHeight(); py++){
int x = px * scaling, y = py * scaling;
GenTile tile = buffer1[px][py];
input.begin(editor, x, y, content.block(tile.floor), content.block(tile.block), content.block(tile.ore));
filter.apply(input);
buffer2[px][py].set(input.floor, input.block, input.ore, Team.all[tile.team], tile.rotation);
}
}
for(int px = 0; px < pixmap.getWidth(); px++){
for(int py = 0; py < pixmap.getHeight(); py++){
buffer1[px][py].set(buffer2[px][py]);
}
}
}
for(int px = 0; px < pixmap.getWidth(); px++){
for(int py = 0; py < pixmap.getHeight(); py++){
int color;
//get result from buffer1 if there's filters left, otherwise get from editor directly
if(filters.isEmpty()){
Tile tile = editor.tile(px * scaling, py * scaling);
color = MapIO.colorFor(tile.floor(), tile.block(), tile.overlay(), Team.none);
}else{
GenTile tile = buffer1[px][py];
color = MapIO.colorFor(content.block(tile.floor), content.block(tile.block), content.block(tile.ore), Team.none);
}
pixmap.drawPixel(px, pixmap.getHeight() - 1 - py, color);
}
}
Core.app.post(() -> {
if(pixmap == null || texture == null){
return;
}
texture.draw(pixmap, 0, 0);
generating = false;
});
}catch(Exception e){
generating = false;
e.printStackTrace();
}
return null;
});
}
public static class GenTile{
public byte team, rotation;
public short block, floor, ore;
void set(Block floor, Block wall, Block ore, Team team, int rotation){
this.floor = floor.id;
this.block = wall.id;
this.ore = ore.id;
this.team = (byte)team.ordinal();
this.rotation = (byte)rotation;
}
void set(GenTile other){
this.floor = other.floor;
this.block = other.block;
this.ore = other.ore;
this.team = other.team;
this.rotation = other.rotation;
}
void set(Tile other){
set(other.floor(), other.block(), other.overlay(), other.getTeam(), other.rotation());
}
}
}

View File

@@ -0,0 +1,77 @@
package io.anuke.mindustry.editor;
import io.anuke.arc.Core;
import io.anuke.arc.collection.ObjectMap;
import io.anuke.arc.scene.ui.TextArea;
import io.anuke.arc.scene.ui.TextField;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.core.Platform;
import io.anuke.mindustry.game.Rules;
import io.anuke.mindustry.ui.dialogs.CustomRulesDialog;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
public class MapInfoDialog extends FloatingDialog{
private final MapEditor editor;
private final WaveInfoDialog waveInfo;
private final CustomRulesDialog ruleInfo = new CustomRulesDialog();
public MapInfoDialog(MapEditor editor){
super("$editor.mapinfo");
this.editor = editor;
this.waveInfo = new WaveInfoDialog(editor);
addCloseButton();
shown(this::setup);
}
private void setup(){
cont.clear();
ObjectMap<String, String> tags = editor.getTags();
cont.pane(t -> {
t.add("$editor.name").padRight(8).left();
t.defaults().padTop(15);
TextField name = t.addField(tags.get("name", ""), text -> {
tags.put("name", text);
}).size(400, 55f).get();
name.setMessageText("$unknown");
t.row();
t.add("$editor.description").padRight(8).left();
TextArea description = t.addArea(tags.get("description", ""), "textarea", text -> {
tags.put("description", text);
}).size(400f, 140f).get();
t.row();
t.add("$editor.author").padRight(8).left();
TextField author = t.addField(tags.get("author", Core.settings.getString("mapAuthor", "")), text -> {
tags.put("author", text);
Core.settings.put("mapAuthor", text);
Core.settings.save();
}).size(400, 55f).get();
author.setMessageText("$unknown");
t.row();
t.add("$editor.rules").padRight(8).left();
t.addButton("$edit", () -> ruleInfo.show(Vars.state.rules, () -> Vars.state.rules = new Rules())).left().width(200f);
t.row();
t.add("$editor.waves").padRight(8).left();
t.addButton("$edit", waveInfo::show).left().width(200f);
name.change();
description.change();
author.change();
Platform.instance.addDialog(name, 50);
Platform.instance.addDialog(author, 50);
Platform.instance.addDialog(description, 1000);
t.margin(16f);
});
}
}

View File

@@ -0,0 +1,77 @@
package io.anuke.mindustry.editor;
import io.anuke.arc.function.Consumer;
import io.anuke.arc.scene.ui.*;
import io.anuke.arc.scene.ui.layout.Table;
import io.anuke.arc.util.Scaling;
import io.anuke.mindustry.maps.Map;
import io.anuke.mindustry.ui.BorderImage;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import static io.anuke.mindustry.Vars.world;
public class MapLoadDialog extends FloatingDialog{
private Map selected = null;
public MapLoadDialog(Consumer<Map> loader){
super("$editor.loadmap");
shown(this::rebuild);
rebuild();
TextButton button = new TextButton("$load");
button.setDisabled(() -> selected == null);
button.clicked(() -> {
if(selected != null){
loader.accept(selected);
hide();
}
});
buttons.defaults().size(200f, 50f);
buttons.addButton("$cancel", this::hide);
buttons.add(button);
}
public void rebuild(){
cont.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.name(), "toggle");
button.add(new BorderImage(map.texture, 2f).setScaling(Scaling.fit)).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){
table.add("$maps.none").center();
}else{
cont.add("$editor.loadmap");
}
cont.row();
cont.add(pane);
}
}

View File

@@ -0,0 +1,166 @@
package io.anuke.mindustry.editor;
import io.anuke.arc.Core;
import io.anuke.arc.collection.IntSet;
import io.anuke.arc.collection.IntSet.IntSetIterator;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.Texture;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Disposable;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.graphics.IndexedRenderer;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.BlockPart;
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 Texture texture;
public MapRenderer(MapEditor editor){
this.editor = editor;
texture = Core.atlas.find("clear-editor").getTexture();
}
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){
Draw.flush();
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];
if(mesh == null){
continue;
}
mesh.getTransformMatrix().setToTranslation(tx, ty).scale(tw / (width * tilesize), th / (height * tilesize));
mesh.setProjectionMatrix(Draw.proj());
mesh.render(texture);
}
}
}
public void updatePoint(int x, int y){
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];
Tile tile = editor.tiles()[wx][wy];
Team team = tile.getTeam();
Block floor = tile.floor();
Block wall = tile.block();
TextureRegion region;
int idxWall = (wx % chunkSize) + (wy % chunkSize) * chunkSize;
int idxDecal = (wx % chunkSize) + (wy % chunkSize) * chunkSize + chunkSize * chunkSize;
if(wall != Blocks.air && (wall.synthetic() || wall instanceof BlockPart)){
region = !Core.atlas.isFound(wall.editorIcon()) ? Core.atlas.find("clear-editor") : wall.editorIcon();
if(wall.rotate){
mesh.draw(idxWall, region,
wx * tilesize + wall.offset(), wy * tilesize + wall.offset(),
region.getWidth() * Draw.scl, region.getHeight() * Draw.scl, tile.rotation() * 90 - 90);
}else{
mesh.draw(idxWall, region,
wx * tilesize + wall.offset() + (tilesize - region.getWidth() * Draw.scl) / 2f,
wy * tilesize + wall.offset() + (tilesize - region.getHeight() * Draw.scl) / 2f,
region.getWidth() * Draw.scl, region.getHeight() * Draw.scl);
}
}else{
region = floor.editorVariantRegions()[Mathf.randomSeed(idxWall, 0, floor.editorVariantRegions().length - 1)];
mesh.draw(idxWall, region, wx * tilesize, wy * tilesize, 8, 8);
}
float offsetX = -(wall.size / 3) * tilesize, offsetY = -(wall.size / 3) * tilesize;
if(wall.update || wall.destructible){
mesh.setColor(team.color);
region = Core.atlas.find("block-border-editor");
}else if(!wall.synthetic() && wall != Blocks.air){
region = !Core.atlas.isFound(wall.editorIcon()) ? Core.atlas.find("clear-editor") : wall.editorIcon();
offsetX = tilesize / 2f - region.getWidth() / 2f * Draw.scl;
offsetY = tilesize / 2f - region.getHeight() / 2f * Draw.scl;
}else if(wall == Blocks.air && tile.overlay() != null){
region = tile.overlay().editorVariantRegions()[Mathf.randomSeed(idxWall, 0, tile.overlay().editorVariantRegions().length - 1)];
}else{
region = Core.atlas.find("clear-editor");
}
mesh.draw(idxDecal, region,
wx * tilesize + offsetX, wy * tilesize + offsetY,
region.getWidth() * Draw.scl, region.getHeight() * Draw.scl);
mesh.setColor(Color.WHITE);
}
@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,57 @@
package io.anuke.mindustry.editor;
import io.anuke.arc.function.IntPositionConsumer;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.scene.ui.layout.Table;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
public class MapResizeDialog extends FloatingDialog{
private static final int minSize = 50, maxSize = 500, increment = 50;
int width, height;
public MapResizeDialog(MapEditor editor, IntPositionConsumer cons){
super("$editor.resizemap");
shown(() -> {
cont.clear();
width = editor.width();
height = editor.height();
Table table = new Table();
for(boolean w : Mathf.booleans){
table.add(w ? "$width" : "$height").padRight(8f);
table.defaults().height(60f).padTop(8);
table.addButton("<", () -> {
if(w)
width = move(width, -1);
else
height = move(height, -1);
}).size(60f);
table.table("button", t -> t.label(() -> (w ? width : height) + "")).width(200);
table.addButton(">", () -> {
if(w)
width = move(width, 1);
else
height = move(height, 1);
}).size(60f);
table.row();
}
cont.row();
cont.add(table);
});
buttons.defaults().size(200f, 50f);
buttons.addButton("$cancel", this::hide);
buttons.addButton("$editor.resize", () -> {
cons.accept(width, height);
hide();
});
}
static int move(int value, int direction){
return Mathf.clamp((value / increment + direction) * increment, minSize, maxSize);
}
}

View File

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

View File

@@ -0,0 +1,344 @@
package io.anuke.mindustry.editor;
import io.anuke.arc.Core;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.input.GestureDetector;
import io.anuke.arc.input.GestureDetector.GestureListener;
import io.anuke.arc.input.KeyCode;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.*;
import io.anuke.arc.scene.Element;
import io.anuke.arc.scene.event.*;
import io.anuke.arc.scene.ui.TextField;
import io.anuke.arc.scene.ui.layout.Unit;
import io.anuke.arc.util.Tmp;
import io.anuke.mindustry.graphics.Pal;
import io.anuke.mindustry.input.Binding;
import io.anuke.mindustry.ui.GridImage;
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 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) -> Mathf.dst(x, y, index, index) <= index - 0.5f);
}
Core.input.getInputProcessors().insert(0, new GestureDetector(20, 0.5f, 2, 0.15f, this));
touchable(Touchable.enabled);
Point2 firstTouch = new Point2();
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, KeyCode button){
if(pointer != 0){
return false;
}
if(!mobile && button != KeyCode.MOUSE_LEFT && button != KeyCode.MOUSE_MIDDLE){
return true;
}
if(button == KeyCode.MOUSE_MIDDLE){
lastTool = tool;
tool = EditorTool.zoom;
}
mousex = x;
mousey = y;
Point2 p = project(x, y);
lastx = p.x;
lasty = p.y;
startx = p.x;
starty = p.y;
tool.touched(editor, p.x, p.y);
firstTouch.set(p);
if(tool.edit){
ui.editor.resetSaved();
}
drawing = true;
return true;
}
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){
if(!mobile && button != KeyCode.MOUSE_LEFT && button != KeyCode.MOUSE_MIDDLE){
return;
}
drawing = false;
Point2 p = project(x, y);
if(tool == EditorTool.line){
ui.editor.resetSaved();
tool.touchedLine(editor, startx, starty, p.x, p.y);
}
editor.flushOp();
if(button == KeyCode.MOUSE_MIDDLE && lastTool != null){
tool = lastTool;
lastTool = null;
}
}
@Override
public void touchDragged(InputEvent event, float x, float y, int pointer){
mousex = x;
mousey = y;
Point2 p = project(x, y);
if(drawing && tool.draggable && !(p.x == lastx && p.y == lasty)){
ui.editor.resetSaved();
Bresenham2.line(lastx, lasty, p.x, p.y, (cx, cy) -> tool.touched(editor, cx, cy));
}
if(tool == EditorTool.line && tool.mode == 1){
if(Math.abs(p.x - firstTouch.x) > Math.abs(p.y - firstTouch.y)){
lastx = p.x;
lasty = firstTouch.y;
}else{
lastx = firstTouch.x;
lasty = p.y;
}
}else{
lastx = p.x;
lasty = p.y;
}
}
});
}
public EditorTool getTool(){
return tool;
}
public void setTool(EditorTool tool){
this.tool = tool;
}
public boolean isGrid(){
return grid;
}
public void setGrid(boolean grid){
this.grid = grid;
}
@Override
public void act(float delta){
super.act(delta);
if(Core.scene.getKeyboardFocus() == null || !(Core.scene.getKeyboardFocus() instanceof TextField) && !Core.input.keyDown(KeyCode.CONTROL_LEFT)){
float ax = Core.input.axis(Binding.move_x);
float ay = Core.input.axis(Binding.move_y);
offsetx -= ax * 15f / zoom;
offsety -= ay * 15f / zoom;
}
if(Core.input.keyTap(KeyCode.SHIFT_LEFT)){
lastTool = tool;
tool = EditorTool.pick;
}
if(Core.input.keyRelease(KeyCode.SHIFT_LEFT) && lastTool != null){
tool = lastTool;
lastTool = null;
}
if(ui.editor.hasPane()) return;
zoom += Core.input.axis(KeyCode.SCROLL) / 10f * zoom;
clampZoom();
}
private void clampZoom(){
zoom = Mathf.clamp(zoom, 0.2f, 20f);
}
private Point2 project(float x, float y){
float ratio = 1f / ((float)editor.width() / editor.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.width();
y = (y - getHeight() / 2 + sclheight / 2 - offsety * zoom) / sclheight * editor.height();
if(editor.drawBlock.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.width() / editor.height());
float size = Math.min(width, height);
float sclwidth = size * zoom;
float sclheight = size * zoom * ratio;
float px = ((float)x / editor.width()) * sclwidth + offsetx * zoom - sclwidth / 2 + getWidth() / 2;
float py = ((float)(y) / editor.height()) * sclheight
+ offsety * zoom - sclheight / 2 + getHeight() / 2;
return vec.set(px, py);
}
@Override
public void draw(){
float ratio = 1f / ((float)editor.width() / editor.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.width(), editor.height());
if(!ScissorStack.pushScissors(rect.set(x, y, width, height))){
return;
}
Draw.color(Pal.remove);
Lines.stroke(2f);
Lines.rect(centerx - sclwidth / 2 - 1, centery - sclheight / 2 - 1, sclwidth + 2, sclheight + 2);
if(Core.scene.getKeyboardFocus() != null && isDescendantOf(Core.scene.getKeyboardFocus())){
editor.renderer().draw(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
}
Draw.reset();
if(!ScissorStack.pushScissors(rect.set(x, y, width, height))){
return;
}
if(grid){
Draw.color(Color.GRAY);
image.setBounds(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
image.draw();
Draw.color();
}
int index = 0;
for(int i = 0; i < MapEditor.brushSizes.length; i++){
if(editor.brushSize == MapEditor.brushSizes[i]){
index = i;
break;
}
}
float scaling = zoom * Math.min(width, height) / editor.width();
Draw.color(Pal.accent);
Lines.stroke(Unit.dp.scl(2f));
if((!editor.drawBlock.isMultiblock() || tool == EditorTool.eraser) && tool != EditorTool.fill){
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 || (tool == EditorTool.line && !drawing)) && (!mobile || drawing)){
Point2 p = project(mousex, mousey);
Vector2 v = unproject(p.x, p.y).add(x, y);
//pencil square outline
if(tool == EditorTool.pencil && tool.mode == 1){
Lines.square(v.x + scaling/2f, v.y + scaling/2f, scaling * (editor.brushSize + 0.5f));
}else{
Lines.poly(brushPolygons[index], v.x, v.y, scaling);
}
}
}else{
if((tool.edit || tool == EditorTool.line) && (!mobile || drawing)){
Point2 p = project(mousex, mousey);
Vector2 v = unproject(p.x, p.y).add(x, y);
float offset = (editor.drawBlock.size % 2 == 0 ? scaling / 2f : 0f);
Lines.square(
v.x + scaling / 2f + offset,
v.y + scaling / 2f + offset,
scaling * editor.drawBlock.size / 2f);
}
}
Draw.color(Pal.accent);
Lines.stroke(Unit.dp.scl(3f));
Lines.rect(x, y, width, height);
Draw.reset();
ScissorStack.popScissors();
ScissorStack.popScissors();
}
private boolean active(){
return Core.scene.getKeyboardFocus() != null
&& Core.scene.getKeyboardFocus().isDescendantOf(ui.editor)
&& ui.editor.isShown() && tool == EditorTool.zoom &&
Core.scene.hit(Core.input.mouse().x, Core.input.mouse().y, true) == this;
}
@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 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 io.anuke.arc.collection.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.remove(0);
}
}
public boolean canUndo(){
return !(stack.size - 1 + index < 0);
}
public boolean canRedo(){
return !(index > -1 || stack.size + index < 0);
}
public void undo(){
if(!canUndo()) return;
stack.get(stack.size - 1 + index).undo();
index--;
}
public void redo(){
if(!canRedo()) return;
index++;
stack.get(stack.size - 1 + index).redo();
}
}

View File

@@ -0,0 +1,268 @@
package io.anuke.mindustry.editor;
import io.anuke.arc.Core;
import io.anuke.arc.collection.Array;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.input.KeyCode;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.scene.event.Touchable;
import io.anuke.arc.scene.ui.Label;
import io.anuke.arc.scene.ui.TextField.TextFieldFilter;
import io.anuke.arc.scene.ui.layout.Table;
import io.anuke.arc.util.*;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.graphics.Pal;
import io.anuke.mindustry.io.JsonIO;
import io.anuke.mindustry.type.*;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import static io.anuke.mindustry.Vars.*;
import static io.anuke.mindustry.game.SpawnGroup.never;
public class WaveInfoDialog extends FloatingDialog{
private final static int displayed = 20;
private Array<SpawnGroup> groups = new Array<>();
private Table table, preview;
private int start = 0;
private UnitType lastType = UnitTypes.dagger;
private float updateTimer, updatePeriod = 1f;
public WaveInfoDialog(MapEditor editor){
super("$waves.title");
shown(this::setup);
hidden(() -> {
state.rules.spawns = groups;
});
keyDown(key -> {
if(key == KeyCode.ESCAPE || key == KeyCode.BACK){
Core.app.post(this::hide);
}
});
addCloseButton();
buttons.addButton("$waves.edit", () -> {
FloatingDialog dialog = new FloatingDialog("$waves.edit");
dialog.addCloseButton();
dialog.setFillParent(false);
dialog.cont.defaults().size(210f, 64f);
dialog.cont.addButton("$waves.copy", () -> {
ui.showInfoFade("$waves.copied");
Core.app.getClipboard().setContents(world.maps.writeWaves(groups));
dialog.hide();
}).disabled(b -> groups == null);
dialog.cont.row();
dialog.cont.addButton("$waves.load", () -> {
try{
groups = world.maps.readWaves(Core.app.getClipboard().getContents());
buildGroups();
}catch(Exception e){
ui.showError("$waves.invalid");
}
dialog.hide();
}).disabled(b -> Core.app.getClipboard().getContents() == null || Core.app.getClipboard().getContents().isEmpty());
dialog.cont.row();
dialog.cont.addButton("$settings.reset", () -> ui.showConfirm("$confirm", "$settings.clear.confirm", () -> {
groups = JsonIO.copy(defaultWaves.get());
buildGroups();
dialog.hide();
}));
dialog.show();
}).size(270f, 64f);
}
void setup(){
groups = JsonIO.copy(state.rules.spawns.isEmpty() ? defaultWaves.get() : state.rules.spawns);
cont.clear();
cont.stack(new Table("clear", main -> {
main.pane(t -> table = t).growX().growY().get().setScrollingDisabled(true, false);
main.row();
main.addButton("$add", () -> {
if(groups == null) groups = new Array<>();
groups.add(new SpawnGroup(lastType));
buildGroups();
}).growX().height(70f);
}), new Label("$waves.none"){{
visible(groups::isEmpty);
touchable(Touchable.disabled);
setWrap(true);
setAlignment(Align.center, Align.center);
}}).width(390f).growY();
cont.table("clear", m -> {
m.add("$waves.preview").color(Color.LIGHT_GRAY).growX().center().get().setAlignment(Align.center, Align.center);
m.row();
m.addButton("-", () -> {
}).update(t -> {
if(t.getClickListener().isPressed()){
updateTimer += Time.delta();
if(updateTimer >= updatePeriod){
start = Math.max(start - 1, 0);
updateTimer = 0f;
updateWaves();
}
}
}).growX().height(70f);
m.row();
m.pane(t -> preview = t).grow().get().setScrollingDisabled(true, false);
m.row();
m.addButton("+", () -> {
}).update(t -> {
if(t.getClickListener().isPressed()){
updateTimer += Time.delta();
if(updateTimer >= updatePeriod){
start++;
updateTimer = 0f;
updateWaves();
}
}
}).growX().height(70f);
}).growY().width(180f).growY();
buildGroups();
}
void buildGroups(){
table.clear();
table.top();
table.margin(10f);
if(groups != null){
for(SpawnGroup group : groups){
table.table("clear", t -> {
t.margin(6f).defaults().pad(2).padLeft(5f).growX().left();
t.addButton(b -> {
b.left();
b.addImage(group.type.iconRegion).size(30f).padRight(3);
b.add(group.type.localizedName).color(Pal.accent);
}, () -> showUpdate(group)).pad(-6f).padBottom(0f);
t.row();
t.table(spawns -> {
spawns.addField("" + (group.begin + 1), TextFieldFilter.digitsOnly, text -> {
if(Strings.canParsePostiveInt(text)){
group.begin = Strings.parseInt(text) - 1;
updateWaves();
}
}).width(100f);
spawns.add("$waves.to").padLeft(4).padRight(4);
spawns.addField(group.end == never ? "" : (group.end + 1) + "", TextFieldFilter.digitsOnly, text -> {
if(Strings.canParsePostiveInt(text)){
group.end = Strings.parseInt(text) - 1;
updateWaves();
}else if(text.isEmpty()){
group.end = never;
updateWaves();
}
}).width(100f).get().setMessageText(Core.bundle.get("waves.never"));
});
t.row();
t.table(p -> {
p.add("$waves.every").padRight(4);
p.addField(group.spacing + "", TextFieldFilter.digitsOnly, text -> {
if(Strings.canParsePostiveInt(text) && Strings.parseInt(text) > 0){
group.spacing = Strings.parseInt(text);
updateWaves();
}
}).width(100f);
p.add("$waves.waves").padLeft(4);
});
t.row();
t.table(a -> {
a.addField(group.unitAmount + "", TextFieldFilter.digitsOnly, text -> {
if(Strings.canParsePostiveInt(text)){
group.unitAmount = Strings.parseInt(text);
updateWaves();
}
}).width(80f);
a.add(" + ");
a.addField(Strings.fixed(Math.max((Mathf.isZero(group.unitScaling) ? 0 : 1f / group.unitScaling), 0), 2), TextFieldFilter.floatsOnly, text -> {
if(Strings.canParsePositiveFloat(text)){
group.unitScaling = 1f / Strings.parseFloat(text);
updateWaves();
}
}).width(80f);
a.add("$waves.perspawn").padLeft(4);
});
t.row();
t.addCheck("$waves.boss", b -> group.effect = (b ? StatusEffects.boss : null)).padTop(4).update(b -> b.setChecked(group.effect == StatusEffects.boss));
t.row();
t.addButton("$waves.remove", () -> {
groups.remove(group);
table.getCell(t).pad(0f);
t.remove();
updateWaves();
}).growX().pad(-6f).padTop(5);
}).width(340f).pad(5);
table.row();
}
}else{
table.add("$editor.default");
}
updateWaves();
}
void showUpdate(SpawnGroup group){
FloatingDialog dialog = new FloatingDialog("");
dialog.setFillParent(false);
int i = 0;
for(UnitType type : content.units()){
dialog.cont.addButton(t -> {
t.left();
t.addImage(type.iconRegion).size(40f).padRight(2f);
t.add(type.localizedName);
}, () -> {
lastType = type;
group.type = type;
dialog.hide();
buildGroups();
}).pad(2).margin(12f).fillX();
if(++i % 3 == 0) dialog.cont.row();
}
dialog.show();
}
void updateWaves(){
preview.clear();
preview.top();
for(int i = start; i < displayed + start; i++){
int wave = i;
preview.table("underline", table -> {
table.add((wave + 1) + "").color(Pal.accent).center().colspan(2).get().setAlignment(Align.center, Align.center);
table.row();
int[] spawned = new int[Vars.content.getBy(ContentType.unit).size];
for(SpawnGroup spawn : groups){
spawned[spawn.type.id] += spawn.getUnitsSpawned(wave);
}
for(int j = 0; j < spawned.length; j++){
if(spawned[j] > 0){
UnitType type = content.getByID(ContentType.unit, j);
table.addImage(type.iconRegion).size(30f).padRight(4);
table.add(spawned[j] + "x").color(Color.LIGHT_GRAY).padRight(6);
table.row();
}
}
if(table.getChildren().size == 1){
table.add("$none").color(Pal.remove);
}
}).width(110f).pad(2f);
preview.row();
}
}
}

View File

@@ -0,0 +1,46 @@
package io.anuke.mindustry.editor.generation;
import io.anuke.arc.math.Mathf;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
import io.anuke.mindustry.world.Block;
import static io.anuke.mindustry.editor.generation.FilterOption.floorsOnly;
public class BlendFilter extends GenerateFilter{
float radius = 2f;
Block flooronto = Blocks.stone, floor = Blocks.ice;
{
options(
new SliderOption("radius", () -> radius, f -> radius = f, 1f, 10f),
new BlockOption("flooronto", () -> flooronto, b -> flooronto = b, floorsOnly),
new BlockOption("floor", () -> floor, b -> floor = b, floorsOnly)
);
}
@Override
public void apply(){
if(in.floor == flooronto) return;
int rad = (int)radius;
boolean found = false;
outer:
for(int x = -rad; x <= rad; x++){
for(int y = -rad; y <= rad; y++){
if(Mathf.dst2(x, y) > rad*rad) continue;
if(in.tile(in.x + x, in.y + y).floor == flooronto.id){
found = true;
break outer;
}
}
}
if(found){
in.floor = floor;
}
}
}

View File

@@ -0,0 +1,27 @@
package io.anuke.mindustry.editor.generation;
import io.anuke.mindustry.editor.MapGenerateDialog.GenTile;
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
import io.anuke.mindustry.world.blocks.Floor;
import static io.anuke.mindustry.Vars.content;
public class DistortFilter extends GenerateFilter{
float scl = 40, mag = 5;
{
options(
new SliderOption("scale", () -> scl, f -> scl = f, 1f, 400f),
new SliderOption("mag", () -> mag, f -> mag = f, 0.5f, 100f)
);
}
@Override
public void apply(){
GenTile tile = in.tile(in.x / (in.scaling) + (noise(in.x, in.y, scl, mag) - mag / 2f) / in.scaling, in.y / (in.scaling) + (noise(in.x, in.y + o, scl, mag) - mag / 2f) / in.scaling);
in.floor = content.block(tile.floor);
if(!content.block(tile.block).synthetic() && !in.block.synthetic()) in.block = content.block(tile.block);
if(!((Floor)in.floor).isLiquid) in.ore = content.block(tile.ore);
}
}

View File

@@ -0,0 +1,95 @@
package io.anuke.mindustry.editor.generation;
import io.anuke.arc.Core;
import io.anuke.arc.function.*;
import io.anuke.arc.scene.style.TextureRegionDrawable;
import io.anuke.arc.scene.ui.Slider;
import io.anuke.arc.scene.ui.layout.Table;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.ui.dialogs.FloatingDialog;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Block.Icon;
import io.anuke.mindustry.world.blocks.*;
import static io.anuke.mindustry.Vars.updateEditorOnChange;
public abstract class FilterOption{
public static final Predicate<Block> floorsOnly = b -> (b instanceof Floor && !(b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Icon.full));
public static final Predicate<Block> wallsOnly = b -> (!b.synthetic() && !(b instanceof Floor)) && Core.atlas.isFound(b.icon(Icon.full));
public static final Predicate<Block> floorsOptional = b -> b == Blocks.air || ((b instanceof Floor && !(b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Icon.full)));
public static final Predicate<Block> wallsOptional = b -> b == Blocks.air || ((!b.synthetic() && !(b instanceof Floor)) && Core.atlas.isFound(b.icon(Icon.full)));
public static final Predicate<Block> wallsOresOptional = b -> b == Blocks.air || (((!b.synthetic() && !(b instanceof Floor)) || (b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Icon.full)));
public static final Predicate<Block> oresOnly = b -> b instanceof OverlayFloor && Core.atlas.isFound(b.icon(Icon.full));
public abstract void build(Table table);
public Runnable changed = () -> {
};
static class SliderOption extends FilterOption{
final String name;
final FloatProvider getter;
final FloatConsumer setter;
final float min, max;
SliderOption(String name, FloatProvider getter, FloatConsumer setter, float min, float max){
this.name = name;
this.getter = getter;
this.setter = setter;
this.min = min;
this.max = max;
}
@Override
public void build(Table table){
table.add("$filter.option." + name);
table.row();
Slider slider = table.addSlider(min, max, (max - min) / 200f, setter).growX().get();
slider.setValue(getter.get());
if(updateEditorOnChange){
slider.changed(changed);
}else{
slider.released(changed);
}
}
}
static class BlockOption extends FilterOption{
final String name;
final Supplier<Block> supplier;
final Consumer<Block> consumer;
final Predicate<Block> filter;
BlockOption(String name, Supplier<Block> supplier, Consumer<Block> consumer, Predicate<Block> filter){
this.name = name;
this.supplier = supplier;
this.consumer = consumer;
this.filter = filter;
}
@Override
public void build(Table table){
table.addButton(b -> b.addImage(supplier.get().icon(Icon.small)).update(i -> ((TextureRegionDrawable)i.getDrawable())
.setRegion(supplier.get() == Blocks.air ? Core.atlas.find("icon-none") : supplier.get().icon(Icon.small))).size(8 * 3), () -> {
FloatingDialog dialog = new FloatingDialog("");
dialog.setFillParent(false);
int i = 0;
for(Block block : Vars.content.blocks()){
if(!filter.test(block)) continue;
dialog.cont.addImage(block == Blocks.air ? Core.atlas.find("icon-none-small") : block.icon(Icon.medium)).size(8 * 4).pad(3).get().clicked(() -> {
consumer.accept(block);
dialog.hide();
changed.run();
});
if(++i % 10 == 0) dialog.cont.row();
}
dialog.show();
}).pad(4).margin(12f);
table.add("$filter.option." + name);
}
}
}

View File

@@ -0,0 +1,99 @@
package io.anuke.mindustry.editor.generation;
import io.anuke.arc.Core;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Pack;
import io.anuke.arc.util.noise.RidgedPerlin;
import io.anuke.arc.util.noise.Simplex;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.editor.MapEditor;
import io.anuke.mindustry.editor.MapGenerateDialog.GenTile;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.blocks.Floor;
public abstract class GenerateFilter{
protected transient float o = (float)(Math.random() * 10000000.0);
protected transient long seed;
protected transient GenerateInput in;
public FilterOption[] options;
protected abstract void apply();
protected float noise(float x, float y, float scl, float mag){
return (float)in.noise.octaveNoise2D(1f, 0f, 1f / scl, x + o, y + o) * mag;
}
protected float noise(float x, float y, float scl, float mag, float octaves, float persistence){
return (float)in.noise.octaveNoise2D(octaves, persistence, 1f / scl, x + o, y + o) * mag;
}
protected float rnoise(float x, float y, float scl, float mag){
return in.pnoise.getValue((int)(x + o), (int)(y + o), 1f / scl) * mag;
}
public void randomize(){
seed = Mathf.random(99999999);
}
protected float chance(){
return Mathf.randomSeed(Pack.longInt(in.x, in.y + (int)o));
}
public void options(FilterOption... options){
this.options = options;
}
public String name(){
return Core.bundle.get("filter." + getClass().getSimpleName().toLowerCase().replace("filter", ""), getClass().getSimpleName().replace("Filter", ""));
}
public final void apply(GenerateInput in){
this.in = in;
apply();
//remove extra ores on liquids
if(((Floor)in.floor).isLiquid){
in.ore = Blocks.air;
}
}
public static class GenerateInput{
public Floor srcfloor;
public Block srcblock;
public Block srcore;
public int x, y, width, height, scaling;
public MapEditor editor;
public Block floor, block, ore;
Simplex noise = new Simplex();
RidgedPerlin pnoise = new RidgedPerlin(0, 1);
TileProvider buffer;
public void begin(MapEditor editor, int x, int y, Block floor, Block block, Block ore){
this.editor = editor;
this.floor = this.srcfloor = (Floor)floor;
this.block = this.srcblock = block;
this.ore = srcore = ore;
this.x = x;
this.y = y;
}
public void setFilter(GenerateFilter filter, int width, int height, int scaling, TileProvider buffer){
this.buffer = buffer;
this.width = width;
this.height = height;
this.scaling = scaling;
noise.setSeed(filter.seed);
pnoise.setSeed((int)(filter.seed + 1));
}
GenTile tile(float x, float y){
return buffer.get(Mathf.clamp((int)x, 0, width - 1), Mathf.clamp((int)y, 0, height - 1));
}
public interface TileProvider{
GenTile get(int x, int y);
}
}
}

View File

@@ -0,0 +1,46 @@
package io.anuke.mindustry.editor.generation;
import io.anuke.arc.collection.IntArray;
import io.anuke.arc.math.Mathf;
import io.anuke.mindustry.editor.MapGenerateDialog.GenTile;
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
import static io.anuke.mindustry.Vars.content;
public class MedianFilter extends GenerateFilter{
float radius = 2;
float percentile = 0.5f;
IntArray blocks = new IntArray(), floors = new IntArray();
{
options(
new SliderOption("radius", () -> radius, f -> radius = f, 1f, 12f),
new SliderOption("percentile", () -> percentile, f -> percentile = f, 0f, 1f)
);
}
@Override
public void apply(){
int rad = (int)radius;
blocks.clear();
floors.clear();
for(int x = -rad; x <= rad; x++){
for(int y = -rad; y <= rad; y++){
if(Mathf.dst2(x, y) > rad*rad) continue;
GenTile tile = in.tile(in.x + x, in.y + y);
blocks.add(tile.block);
floors.add(tile.floor);
}
}
floors.sort();
blocks.sort();
int index = Math.min((int)(floors.size * percentile), floors.size - 1);
int floor = floors.get(index), block = blocks.get(index);
in.floor = content.block(floor);
if(!content.block(block).synthetic() && !in.block.synthetic()) in.block = content.block(block);
}
}

View File

@@ -0,0 +1,35 @@
package io.anuke.mindustry.editor.generation;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
import io.anuke.mindustry.world.Block;
import static io.anuke.mindustry.editor.generation.FilterOption.floorsOnly;
import static io.anuke.mindustry.editor.generation.FilterOption.wallsOnly;
public class NoiseFilter extends GenerateFilter{
float scl = 40, threshold = 0.5f, octaves = 3f, falloff = 0.5f;
Block floor = Blocks.stone, block = Blocks.rocks;
{
options(
new SliderOption("scale", () -> scl, f -> scl = f, 1f, 500f),
new SliderOption("threshold", () -> threshold, f -> threshold = f, 0f, 1f),
new SliderOption("octaves", () -> octaves, f -> octaves = f, 1f, 10f),
new SliderOption("falloff", () -> falloff, f -> falloff = f, 0f, 1f),
new BlockOption("floor", () -> floor, b -> floor = b, floorsOnly),
new BlockOption("wall", () -> block, b -> block = b, wallsOnly)
);
}
@Override
public void apply(){
float noise = noise(in.x, in.y, scl, 1f, octaves, falloff);
if(noise > threshold){
in.floor = floor;
if(wallsOnly.test(in.srcblock)) in.block = block;
}
}
}

View File

@@ -0,0 +1,32 @@
package io.anuke.mindustry.editor.generation;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
import io.anuke.mindustry.world.Block;
import static io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
import static io.anuke.mindustry.editor.generation.FilterOption.oresOnly;
public class OreFilter extends GenerateFilter{
public float scl = 50, threshold = 0.72f, octaves = 3f, falloff = 0.4f;
public Block ore = Blocks.oreCopper;
{
options(
new SliderOption("scale", () -> scl, f -> scl = f, 1f, 500f),
new SliderOption("threshold", () -> threshold, f -> threshold = f, 0f, 1f),
new SliderOption("octaves", () -> octaves, f -> octaves = f, 1f, 10f),
new SliderOption("falloff", () -> falloff, f -> falloff = f, 0f, 1f),
new BlockOption("ore", () -> ore, b -> ore = b, oresOnly)
);
}
@Override
public void apply(){
float noise = noise(in.x, in.y, scl, 1f, octaves, falloff);
if(noise > threshold){
in.ore = ore;
}
}
}

View File

@@ -0,0 +1,42 @@
package io.anuke.mindustry.editor.generation;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
import io.anuke.mindustry.world.Block;
import static io.anuke.mindustry.editor.generation.FilterOption.floorsOnly;
import static io.anuke.mindustry.editor.generation.FilterOption.wallsOnly;
public class RiverNoiseFilter extends GenerateFilter{
float scl = 40, threshold = 0f, threshold2 = 0.1f;
Block floor = Blocks.water, floor2 = Blocks.deepwater, block = Blocks.sandRocks;
{
options(
new SliderOption("scale", () -> scl, f -> scl = f, 1f, 500f),
new SliderOption("threshold", () -> threshold, f -> threshold = f, 0f, 1f),
new SliderOption("threshold2", () -> threshold2, f -> threshold2 = f, 0f, 1f),
new BlockOption("block", () -> block, b -> block = b, wallsOnly),
new BlockOption("floor", () -> floor, b -> floor = b, floorsOnly),
new BlockOption("floor2", () -> floor2, b -> floor2 = b, floorsOnly)
);
}
@Override
public void apply(){
float noise = rnoise(in.x, in.y, scl, 1f);
if(noise >= threshold){
in.floor = floor;
if(in.srcblock.solid){
in.block = block;
}
if(noise >= threshold2){
in.floor = floor2;
}
}
}
}

View File

@@ -0,0 +1,38 @@
package io.anuke.mindustry.editor.generation;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
import io.anuke.mindustry.world.Block;
import static io.anuke.mindustry.editor.generation.FilterOption.*;
public class ScatterFilter extends GenerateFilter{
float chance = 0.1f;
Block flooronto = Blocks.air, floor = Blocks.air, block = Blocks.air;
{
options(
new SliderOption("chance", () -> chance, f -> chance = f, 0f, 1f),
new BlockOption("flooronto", () -> flooronto, b -> flooronto = b, floorsOptional),
new BlockOption("floor", () -> floor, b -> floor = b, floorsOptional),
new BlockOption("block", () -> block, b -> block = b, wallsOresOptional)
);
}
@Override
public void apply(){
if(block != Blocks.air && (in.srcfloor == flooronto || flooronto == Blocks.air) && in.srcblock == Blocks.air && chance() <= chance){
if(!block.isOverlay()){
in.block = block;
}else{
in.ore = block;
}
}
if(floor != Blocks.air && (in.srcfloor == flooronto || flooronto == Blocks.air) && chance() <= chance){
in.floor = floor;
}
}
}

View File

@@ -0,0 +1,42 @@
package io.anuke.mindustry.editor.generation;
import io.anuke.arc.math.Mathf;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.editor.generation.FilterOption.BlockOption;
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
import io.anuke.mindustry.world.Block;
import static io.anuke.mindustry.editor.generation.FilterOption.floorsOnly;
import static io.anuke.mindustry.editor.generation.FilterOption.wallsOnly;
public class TerrainFilter extends GenerateFilter{
float scl = 40, threshold = 0.9f, octaves = 3f, falloff = 0.5f, magnitude = 1f, circleScl = 2.1f;
Block floor = Blocks.stone, block = Blocks.rocks;
{
options(
new SliderOption("scale", () -> scl, f -> scl = f, 1f, 500f),
new SliderOption("mag", () -> magnitude, f -> magnitude = f, 0f, 2f),
new SliderOption("threshold", () -> threshold, f -> threshold = f, 0f, 1f),
new SliderOption("circle-scale", () -> circleScl, f -> circleScl = f, 0f, 3f),
new SliderOption("octaves", () -> octaves, f -> octaves = f, 1f, 10f),
new SliderOption("falloff", () -> falloff, f -> falloff = f, 0f, 1f),
new BlockOption("floor", () -> floor, b -> floor = b, floorsOnly),
new BlockOption("wall", () -> block, b -> block = b, wallsOnly)
);
}
@Override
public void apply(){
float noise = noise(in.x, in.y, scl, magnitude, octaves, falloff) + Mathf.dst((float)in.x / in.editor.width(), (float)in.y / in.editor.height(), 0.5f, 0.5f) * circleScl;
in.floor = floor;
in.ore = Blocks.air;
if(noise >= threshold){
in.block = block;
}else{
in.block = Blocks.air;
}
}
}

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,278 @@
package io.anuke.mindustry.entities;
import io.anuke.annotations.Annotations.Struct;
import io.anuke.arc.collection.GridBits;
import io.anuke.arc.collection.IntQueue;
import io.anuke.arc.function.*;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.*;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.content.Bullets;
import io.anuke.mindustry.content.Fx;
import io.anuke.mindustry.entities.Effects.Effect;
import io.anuke.mindustry.entities.bullet.Bullet;
import io.anuke.mindustry.entities.effect.Fire;
import io.anuke.mindustry.entities.effect.Lightning;
import io.anuke.mindustry.entities.type.Unit;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.gen.Call;
import io.anuke.mindustry.gen.PropCell;
import io.anuke.mindustry.graphics.Pal;
import io.anuke.mindustry.world.Tile;
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 Vector2 tr = new Vector2();
private static GridBits bits = new GridBits(30, 30);
private static IntQueue propagation = new IntQueue();
/** 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);
Time.run(i * 2f + Mathf.random(4f), () -> Lightning.create(Team.none, Pal.power, 3,
x, y, Mathf.random(360f), branches + Mathf.range(2)));
}
for(int i = 0; i < Mathf.clamp(flammability / 4, 0, 30); i++){
Time.run(i / 2f, () -> Call.createBullet(Bullets.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;
Time.run(i * 2f, () -> {
Damage.damage(x, y, Mathf.clamp(radius + explosiveness, 0, 50f) * ((f + 1f) / waves), explosiveness / 2f);
Effects.effect(Fx.blockExplosionSmoke, x + Mathf.range(radius), y + Mathf.range(radius));
});
}
if(explosiveness > 15f){
Effects.effect(Fx.shockwave, x, y);
}
if(explosiveness > 30f){
Effects.effect(Fx.bigShockwave, x, y);
}
float shake = Math.min(explosiveness / 4f + 3f, 9f);
Effects.shake(shake, shake, x, y);
Effects.effect(Fx.dynamicExplosion, x, y, radius / 8f);
}
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);
}
}
}
public static void collideLine(Bullet hitter, Team team, Effect effect, float x, float y, float angle, float length){
collideLine(hitter, team, effect, x, y, angle, length, false);
}
/**
* Damages entities in a line.
* Only enemies of the specified team are damaged.
*/
public static void collideLine(Bullet hitter, Team team, Effect effect, float x, float y, float angle, float length, boolean large){
tr.trns(angle, length);
IntPositionConsumer collider = (cx, cy) -> {
Tile tile = world.ltile(cx, cy);
if(tile != null && tile.entity != null && tile.getTeamID() != team.ordinal() && tile.entity.collide(hitter)){
tile.entity.collision(hitter);
hitter.getBulletType().hit(hitter, tile.worldx(), tile.worldy());
}
};
world.raycastEachWorld(x, y, x + tr.x, y + tr.y, (cx, cy) -> {
collider.accept(cx, cy);
if(large){
for(Point2 p : Geometry.d4){
collider.accept(cx + p.x, cy + p.y);
}
}
return false;
});
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.hitbox(hitrect);
Rectangle other = hitrect;
other.y -= expand;
other.x -= expand;
other.width += expand * 2;
other.height += expand * 2;
Vector2 vec = Geometry.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.nearbyEnemies(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.hitbox(hitrect);
if(!hitrect.overlaps(rect)){
return;
}
entity.damage(damage);
acceptor.accept(entity);
};
rect.setSize(size * 2).setCenter(x, y);
if(team != null){
Units.nearbyEnemies(team, rect, cons);
}else{
Units.nearby(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, false);
}
/** 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){
damage(team, x, y, radius, damage, false);
}
/** 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, boolean complete){
Consumer<Unit> cons = entity -> {
if(entity.getTeam() == team || entity.dst(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.velocity().add(tr.setLength((1f - dst / radius) * 2f / entity.mass()));
};
rect.setSize(radius * 2).setCenter(x, y);
if(team != null){
Units.nearbyEnemies(team, rect, cons);
}else{
Units.nearby(rect, cons);
}
if(!complete){
int trad = (int)(radius / tilesize);
Tile tile = world.tileWorld(x, y);
if(tile != null){
tileDamage(team, tile.x, tile.y, trad, damage);
}
}else{
completeDamage(team, x, y, radius, damage);
}
}
public static void tileDamage(Team team, int startx, int starty, int radius, float baseDamage){
bits.clear();
propagation.clear();
int bitOffset = bits.width() / 2;
propagation.addFirst(PropCell.get((byte)0, (byte)0, (short)baseDamage));
//clamp radius to fit bits
radius = Math.min(radius, bits.width() / 2);
while(!propagation.isEmpty()){
int prop = propagation.removeLast();
int x = PropCell.x(prop);
int y = PropCell.y(prop);
int damage = PropCell.damage(prop);
//manhattan distance used for calculating falloff, results in a diamond pattern
int dst = Math.abs(x) + Math.abs(y);
int scaledDamage = (int)(damage * (1f - (float)dst / radius));
bits.set(bitOffset + x, bitOffset + y);
Tile tile = world.ltile(startx + x, starty + y);
if(scaledDamage <= 0 || tile == null) continue;
//apply damage to entity if needed
if(tile.entity != null && tile.getTeam() != team){
int health = (int)tile.entity.health;
if(tile.entity.health > 0){
tile.entity.damage(scaledDamage);
scaledDamage -= health;
if(scaledDamage <= 0) continue;
}
}
for(Point2 p : Geometry.d4){
if(!bits.get(bitOffset + x + p.x, bitOffset + y + p.y)){
propagation.addFirst(PropCell.get((byte)(x + p.x), (byte)(y + p.y), (short)scaledDamage));
}
}
}
}
private static void completeDamage(Team team, float x, float y, float radius, float damage){
int trad = (int)(radius / tilesize);
for(int dx = -trad; dx <= trad; dx++){
for(int dy = -trad; dy <= trad; dy++){
Tile tile = world.tile(Math.round(x / tilesize) + dx, Math.round(y / tilesize) + dy);
if(tile != null && tile.entity != null && (team == null || state.teams.areEnemies(team, tile.getTeam())) && Mathf.dst(dx, dy) <= trad){
tile.entity.damage(damage);
}
}
}
}
private static float calculateDamage(float x, float y, float tx, float ty, float radius, float damage){
float dist = Mathf.dst(x, y, tx, ty);
float falloff = 0.4f;
float scaled = Mathf.lerp(1f - dist / radius, 1f, falloff);
return damage * scaled;
}
@Struct
class PropCellStruct{
byte x;
byte y;
short damage;
}
}

View File

@@ -0,0 +1,166 @@
package io.anuke.mindustry.entities;
import io.anuke.arc.Core;
import io.anuke.arc.collection.Array;
import io.anuke.arc.function.Consumer;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Position;
import io.anuke.arc.util.pooling.Pools;
import io.anuke.mindustry.entities.impl.EffectEntity;
import io.anuke.mindustry.entities.traits.ScaleTrait;
public class Effects{
private static final EffectContainer container = new EffectContainer();
private static Array<Effect> effects = new Array<>();
private static ScreenshakeProvider shakeProvider;
private static float shakeFalloff = 1000f;
private static EffectProvider provider = (effect, color, x, y, rotation, data) -> {
EffectEntity entity = Pools.obtain(EffectEntity.class, EffectEntity::new);
entity.effect = effect;
entity.color = color;
entity.rotation = rotation;
entity.data = data;
entity.set(x, y);
entity.add();
};
public static void setEffectProvider(EffectProvider prov){
provider = prov;
}
public static void setScreenShakeProvider(ScreenshakeProvider provider){
shakeProvider = provider;
}
public static void renderEffect(int id, Effect render, Color color, float life, float rotation, float x, float y, Object data){
container.set(id, color, life, render.lifetime, rotation, x, y, data);
render.draw.render(container);
}
public static Effect getEffect(int id){
if(id >= effects.size || id < 0)
throw new IllegalArgumentException("The effect with ID \"" + id + "\" does not exist!");
return effects.get(id);
}
public static Array<Effect> all(){
return effects;
}
public static void effect(Effect effect, float x, float y, float rotation){
provider.createEffect(effect, Color.WHITE, x, y, rotation, null);
}
public static void effect(Effect effect, float x, float y){
effect(effect, x, y, 0);
}
public static void effect(Effect effect, Color color, float x, float y){
provider.createEffect(effect, color, x, y, 0f, null);
}
public static void effect(Effect effect, Position loc){
provider.createEffect(effect, Color.WHITE, loc.getX(), loc.getY(), 0f, null);
}
public static void effect(Effect effect, Color color, float x, float y, float rotation){
provider.createEffect(effect, color, x, y, rotation, null);
}
public static void effect(Effect effect, Color color, float x, float y, float rotation, Object data){
provider.createEffect(effect, color, x, y, rotation, data);
}
public static void effect(Effect effect, float x, float y, float rotation, Object data){
provider.createEffect(effect, Color.WHITE, x, y, rotation, data);
}
/** Default value is 1000. Higher numbers mean more powerful shake (less falloff). */
public static void setShakeFalloff(float falloff){
shakeFalloff = falloff;
}
private static void shake(float intensity, float duration){
if(shakeProvider == null) throw new RuntimeException("Screenshake provider is null! Set it first.");
shakeProvider.accept(intensity, duration);
}
public static void shake(float intensity, float duration, float x, float y){
if(Core.camera == null) return;
float distance = Core.camera.position.dst(x, y);
if(distance < 1) distance = 1;
shake(Mathf.clamp(1f / (distance * distance / shakeFalloff)) * intensity, duration);
}
public static void shake(float intensity, float duration, Position loc){
shake(intensity, duration, loc.getX(), loc.getY());
}
public interface ScreenshakeProvider{
void accept(float intensity, float duration);
}
public static class Effect{
private static int lastid = 0;
public final int id;
public final EffectRenderer draw;
public final float lifetime;
/** Clip size. */
public float size;
public Effect(float life, float clipsize, EffectRenderer draw){
this.id = lastid++;
this.lifetime = life;
this.draw = draw;
this.size = clipsize;
effects.add(this);
}
public Effect(float life, EffectRenderer draw){
this(life, 28f, draw);
}
}
public static class EffectContainer implements ScaleTrait{
public float x, y, time, lifetime, rotation;
public Color color;
public int id;
public Object data;
private EffectContainer innerContainer;
public void set(int id, Color color, float life, float lifetime, float rotation, float x, float y, Object data){
this.x = x;
this.y = y;
this.color = color;
this.time = life;
this.lifetime = lifetime;
this.id = id;
this.rotation = rotation;
this.data = data;
}
public void scaled(float lifetime, Consumer<EffectContainer> cons){
if(innerContainer == null) innerContainer = new EffectContainer();
if(time <= lifetime){
innerContainer.set(id, color, time, lifetime, rotation, x, y, data);
cons.accept(innerContainer);
}
}
@Override
public float fin(){
return time / lifetime;
}
}
public interface EffectProvider{
void createEffect(Effect effect, Color color, float x, float y, float rotation, Object data);
}
public interface EffectRenderer{
void render(EffectContainer effect);
}
}

View File

@@ -0,0 +1,89 @@
package io.anuke.mindustry.entities;
import io.anuke.arc.Core;
import io.anuke.arc.collection.Array;
import io.anuke.arc.collection.IntMap;
import io.anuke.arc.function.Consumer;
import io.anuke.arc.function.Predicate;
import io.anuke.arc.graphics.Camera;
import io.anuke.arc.math.geom.Rectangle;
import io.anuke.mindustry.entities.traits.DrawTrait;
import io.anuke.mindustry.entities.traits.Entity;
import static io.anuke.mindustry.Vars.collisions;
public class Entities{
private static final Array<EntityGroup<?>> groupArray = new Array<>();
private static final IntMap<EntityGroup<?>> groups = new IntMap<>();
private static final Rectangle viewport = new Rectangle();
private static final boolean clip = true;
private static int count = 0;
public static void clear(){
for(EntityGroup group : groupArray){
group.clear();
}
}
public static EntityGroup<?> getGroup(int id){
return groups.get(id);
}
public static Array<EntityGroup<?>> getAllGroups(){
return groupArray;
}
public static <T extends Entity> EntityGroup<T> addGroup(Class<T> type){
return addGroup(type, true);
}
public static <T extends Entity> EntityGroup<T> addGroup(Class<T> type, boolean useTree){
EntityGroup<T> group = new EntityGroup<>(type, useTree);
groups.put(group.getID(), group);
groupArray.add(group);
return group;
}
public static void update(EntityGroup<?> group){
group.updateEvents();
if(group.useTree()){
collisions.updatePhysics(group);
}
for(Entity e : group.all()){
e.update();
}
}
public static int countInBounds(EntityGroup<?> group){
count = 0;
draw(group, e -> true, e -> count++);
return count;
}
public static void draw(EntityGroup<?> group){
draw(group, e -> true);
}
public static <T extends DrawTrait> void draw(EntityGroup<?> group, Predicate<T> toDraw){
draw(group, toDraw, DrawTrait::draw);
}
@SuppressWarnings("unchecked")
public static <T extends DrawTrait> void draw(EntityGroup<?> group, Predicate<T> toDraw, Consumer<T> cons){
if(clip){
Camera cam = Core.camera;
viewport.set(cam.position.x - cam.width / 2, cam.position.y - cam.height / 2, cam.width, cam.height);
}
for(Entity e : group.all()){
if(!(e instanceof DrawTrait) || !toDraw.test((T)e) || !e.isAdded()) continue;
DrawTrait draw = (DrawTrait)e;
if(!clip || viewport.overlaps(draw.getX() - draw.drawSize()/2f, draw.getY() - draw.drawSize()/2f, draw.drawSize(), draw.drawSize())){
cons.accept((T)e);
}
}
}
}

View File

@@ -0,0 +1,236 @@
package io.anuke.mindustry.entities;
import io.anuke.arc.collection.Array;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.*;
import io.anuke.mindustry.entities.traits.Entity;
import io.anuke.mindustry.entities.traits.SolidTrait;
import io.anuke.mindustry.world.Tile;
import static io.anuke.mindustry.Vars.tilesize;
import static io.anuke.mindustry.Vars.world;
public class EntityCollisions{
//range for tile collision scanning
private static final int r = 1;
//move in 1-unit chunks
private static final float seg = 1f;
//tile collisions
private Rectangle tmp = new Rectangle();
private Vector2 vector = new Vector2();
private Vector2 l1 = new Vector2();
private Rectangle r1 = new Rectangle();
private Rectangle r2 = new Rectangle();
//entity collisions
private Array<SolidTrait> arrOut = new Array<>();
public void move(SolidTrait entity, float deltax, float deltay){
boolean movedx = false;
while(Math.abs(deltax) > 0 || !movedx){
movedx = true;
moveDelta(entity, Math.min(Math.abs(deltax), seg) * Mathf.sign(deltax), 0, true);
if(Math.abs(deltax) >= seg){
deltax -= seg * Mathf.sign(deltax);
}else{
deltax = 0f;
}
}
boolean movedy = false;
while(Math.abs(deltay) > 0 || !movedy){
movedy = true;
moveDelta(entity, 0, Math.min(Math.abs(deltay), seg) * Mathf.sign(deltay), false);
if(Math.abs(deltay) >= seg){
deltay -= seg * Mathf.sign(deltay);
}else{
deltay = 0f;
}
}
}
public void moveDelta(SolidTrait entity, float deltax, float deltay, boolean x){
Rectangle rect = r1;
entity.hitboxTile(rect);
entity.hitboxTile(r2);
rect.x += deltax;
rect.y += deltay;
int tilex = Math.round((rect.x + rect.width / 2) / tilesize), tiley = Math.round((rect.y + rect.height / 2) / tilesize);
for(int dx = -r; dx <= r; dx++){
for(int dy = -r; dy <= r; dy++){
int wx = dx + tilex, wy = dy + tiley;
if(solid(wx, wy) && entity.collidesGrid(wx, wy)){
tmp.setSize(tilesize).setCenter(wx * tilesize, wy * tilesize);
if(tmp.overlaps(rect)){
Vector2 v = Geometry.overlap(rect, tmp, x);
rect.x += v.x;
rect.y += v.y;
}
}
}
}
entity.setX(entity.getX() + rect.x - r2.x);
entity.setY(entity.getY() + rect.y - r2.y);
}
public boolean overlapsTile(Rectangle rect){
rect.getCenter(vector);
int r = 1;
//assumes tiles are centered
int tilex = Math.round(vector.x / tilesize);
int tiley = Math.round(vector.y / tilesize);
for(int dx = -r; dx <= r; dx++){
for(int dy = -r; dy <= r; dy++){
int wx = dx + tilex, wy = dy + tiley;
if(solid(wx, wy)){
r2.setSize(tilesize).setCenter(wx * tilesize, wy * tilesize);
if(r2.overlaps(rect)){
return true;
}
}
}
}
return false;
}
@SuppressWarnings("unchecked")
public <T extends Entity> void updatePhysics(EntityGroup<T> group){
QuadTree tree = group.tree();
tree.clear();
for(Entity entity : group.all()){
if(entity instanceof SolidTrait){
SolidTrait s = (SolidTrait)entity;
s.lastPosition().set(s.getX(), s.getY());
tree.insert(s);
}
}
}
private static boolean solid(int x, int y){
Tile tile = world.tile(x, y);
return tile != null && tile.solid();
}
private void checkCollide(Entity entity, Entity other){
SolidTrait a = (SolidTrait)entity;
SolidTrait b = (SolidTrait)other;
a.hitbox(this.r1);
b.hitbox(this.r2);
r1.x += (a.lastPosition().x - a.getX());
r1.y += (a.lastPosition().y - a.getY());
r2.x += (b.lastPosition().x - b.getX());
r2.y += (b.lastPosition().y - b.getY());
float vax = a.getX() - a.lastPosition().x;
float vay = a.getY() - a.lastPosition().y;
float vbx = b.getX() - b.lastPosition().x;
float vby = b.getY() - b.lastPosition().y;
if(a != b && a.collides(b) && b.collides(a)){
l1.set(a.getX(), a.getY());
boolean collide = r1.overlaps(r2) || collide(r1.x, r1.y, r1.width, r1.height, vax, vay,
r2.x, r2.y, r2.width, r2.height, vbx, vby, l1);
if(collide){
a.collision(b, l1.x, l1.y);
b.collision(a, l1.x, l1.y);
}
}
}
private boolean collide(float x1, float y1, float w1, float h1, float vx1, float vy1,
float x2, float y2, float w2, float h2, float vx2, float vy2, Vector2 out){
float px = vx1, py = vy1;
vx1 -= vx2;
vy1 -= vy2;
float xInvEntry, yInvEntry;
float xInvExit, yInvExit;
if(vx1 > 0.0f){
xInvEntry = x2 - (x1 + w1);
xInvExit = (x2 + w2) - x1;
}else{
xInvEntry = (x2 + w2) - x1;
xInvExit = x2 - (x1 + w1);
}
if(vy1 > 0.0f){
yInvEntry = y2 - (y1 + h1);
yInvExit = (y2 + h2) - y1;
}else{
yInvEntry = (y2 + h2) - y1;
yInvExit = y2 - (y1 + h1);
}
float xEntry, yEntry;
float xExit, yExit;
xEntry = xInvEntry / vx1;
xExit = xInvExit / vx1;
yEntry = yInvEntry / vy1;
yExit = yInvExit / vy1;
float entryTime = Math.max(xEntry, yEntry);
float exitTime = Math.min(xExit, yExit);
if(entryTime > exitTime || xExit < 0.0f || yExit < 0.0f || xEntry > 1.0f || yEntry > 1.0f){
return false;
}else{
float dx = x1 + w1 / 2f + px * entryTime;
float dy = y1 + h1 / 2f + py * entryTime;
out.set(dx, dy);
return true;
}
}
@SuppressWarnings("unchecked")
public void collideGroups(EntityGroup<?> groupa, EntityGroup<?> groupb){
for(Entity entity : groupa.all()){
if(!(entity instanceof SolidTrait))
continue;
SolidTrait solid = (SolidTrait)entity;
solid.hitbox(r1);
r1.x += (solid.lastPosition().x - solid.getX());
r1.y += (solid.lastPosition().y - solid.getY());
solid.hitbox(r2);
r2.merge(r1);
arrOut.clear();
groupb.tree().getIntersect(arrOut, r2);
for(SolidTrait sc : arrOut){
sc.hitbox(r1);
if(r2.overlaps(r1)){
checkCollide(entity, sc);
}
}
}
}
}

View File

@@ -0,0 +1,202 @@
package io.anuke.mindustry.entities;
import io.anuke.arc.collection.Array;
import io.anuke.arc.collection.IntMap;
import io.anuke.arc.function.Consumer;
import io.anuke.arc.function.Predicate;
import io.anuke.arc.math.geom.QuadTree;
import io.anuke.arc.math.geom.Rectangle;
import io.anuke.mindustry.entities.traits.Entity;
public class EntityGroup<T extends Entity>{
private static int lastid;
private final boolean useTree;
private final int id;
private final Class<T> type;
private final Array<T> entityArray = new Array<>(false, 16);
private final Array<T> entitiesToRemove = new Array<>(false, 16);
private final Array<T> entitiesToAdd = new Array<>(false, 16);
private IntMap<T> map;
private QuadTree tree;
private Consumer<T> removeListener;
private Consumer<T> addListener;
public EntityGroup(Class<T> type, boolean useTree){
this.useTree = useTree;
this.id = lastid++;
this.type = type;
if(useTree){
tree = new QuadTree<>(new Rectangle(0, 0, 0, 0));
}
}
public boolean useTree(){
return useTree;
}
public void setRemoveListener(Consumer<T> removeListener){
this.removeListener = removeListener;
}
public void setAddListener(Consumer<T> addListener){
this.addListener = addListener;
}
public EntityGroup<T> enableMapping(){
map = new IntMap<>();
return this;
}
public boolean mappingEnabled(){
return map != null;
}
public Class<T> getType(){
return type;
}
public int getID(){
return id;
}
public void updateEvents(){
for(T e : entitiesToAdd){
if(e == null)
continue;
entityArray.add(e);
e.added();
if(map != null){
map.put(e.getID(), e);
}
}
entitiesToAdd.clear();
for(T e : entitiesToRemove){
entityArray.removeValue(e, true);
if(map != null){
map.remove(e.getID());
}
e.removed();
}
entitiesToRemove.clear();
}
public T getByID(int id){
if(map == null) throw new RuntimeException("Mapping is not enabled for group " + id + "!");
return map.get(id);
}
public void removeByID(int id){
if(map == null) throw new RuntimeException("Mapping is not enabled for group " + id + "!");
T t = map.get(id);
if(t != null){ //remove if present in map already
remove(t);
}else{ //maybe it's being queued?
for(T check : entitiesToAdd){
if(check.getID() == id){ //if it is indeed queued, remove it
entitiesToAdd.removeValue(check, true);
if(removeListener != null){
removeListener.accept(check);
}
break;
}
}
}
}
@SuppressWarnings("unchecked")
public void intersect(float x, float y, float width, float height, Consumer<? super T> out){
//don't waste time for empty groups
if(isEmpty()) return;
tree().getIntersect(out, x, y, width, height);
}
public QuadTree tree(){
if(!useTree) throw new RuntimeException("This group does not support quadtrees! Enable quadtrees when creating it.");
return tree;
}
/** Resizes the internal quadtree, if it is enabled.*/
public void resize(float x, float y, float w, float h){
if(useTree){
tree = new QuadTree<>(new Rectangle(x, y, w, h));
}
}
public boolean isEmpty(){
return entityArray.size == 0;
}
public int size(){
return entityArray.size;
}
public int count(Predicate<T> pred){
int count = 0;
for(int i = 0; i < entityArray.size; i++){
if(pred.test(entityArray.get(i))) count++;
}
return count;
}
public void add(T type){
if(type == null) throw new RuntimeException("Cannot add a null entity!");
if(type.getGroup() != null) return;
type.setGroup(this);
entitiesToAdd.add(type);
if(mappingEnabled()){
map.put(type.getID(), type);
}
if(addListener != null){
addListener.accept(type);
}
}
public void remove(T type){
if(type == null) throw new RuntimeException("Cannot remove a null entity!");
type.setGroup(null);
entitiesToRemove.add(type);
if(removeListener != null){
removeListener.accept(type);
}
}
public void clear(){
for(T entity : entityArray)
entity.setGroup(null);
for(T entity : entitiesToAdd)
entity.setGroup(null);
for(T entity : entitiesToRemove)
entity.setGroup(null);
entitiesToAdd.clear();
entitiesToRemove.clear();
entityArray.clear();
if(map != null)
map.clear();
}
public T find(Predicate<T> pred){
for(int i = 0; i < entityArray.size; i++){
if(pred.test(entityArray.get(i))) return entityArray.get(i);
}
return null;
}
/** Returns the logic-only array for iteration. */
public Array<T> all(){
return entityArray;
}
}

View File

@@ -1,309 +0,0 @@
package io.anuke.mindustry.entities;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Vector2;
import io.anuke.mindustry.graphics.Fx;
import io.anuke.mindustry.graphics.Shaders;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.net.NetEvents;
import io.anuke.mindustry.resource.Mech;
import io.anuke.mindustry.resource.Upgrade;
import io.anuke.mindustry.resource.Weapon;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.Blocks;
import io.anuke.ucore.core.*;
import io.anuke.ucore.entities.SolidEntity;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.util.Angles;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Timer;
import io.anuke.ucore.util.Translator;
import java.nio.ByteBuffer;
import static io.anuke.mindustry.Vars.*;
public class Player extends SyncEntity{
static final float speed = 1.1f;
static final float dashSpeed = 1.8f;
static final int timerDash = 0;
static final int timerShootLeft = 1;
static final int timerShootRight = 2;
static final int timerRegen = 3;
public String name = "name";
public boolean isAndroid;
public boolean isAdmin;
public Color color = new Color();
public Weapon weaponLeft = Weapon.blaster;
public Weapon weaponRight = Weapon.blaster;
public Mech mech = Mech.standard;
public float targetAngle = 0f;
public boolean dashing = false;
public int clientid = -1;
public boolean isLocal = false;
public Timer timer = new Timer(4);
private Vector2 movement = new Vector2();
private Translator tr = new Translator();
public Player(){
hitbox.setSize(5);
hitboxTile.setSize(4f);
maxhealth = 200;
heal();
}
@Override
public void damage(float amount){
if(debug || isAndroid) return;
health -= amount;
if(health <= 0 && !dead && isLocal){ //remote players don't die normally
onDeath();
dead = true;
}
}
@Override
public boolean collides(SolidEntity other){
if(other instanceof Bullet){
Bullet b = (Bullet)other;
if(!state.friendlyFire && b.owner instanceof Player){
return false;
}
}
return !isDead() && super.collides(other) && !isAndroid;
}
@Override
public void onDeath(){
dead = true;
if(Net.active()){
NetEvents.handlePlayerDeath();
}
Effects.effect(Fx.explosion, this);
Effects.shake(4f, 5f, this);
Effects.sound("die", this);
control.setRespawnTime(respawnduration);
ui.hudfrag.fadeRespawn(true);
}
/**called when a remote player death event is recieved*/
public void doRespawn(){
dead = true;
Effects.effect(Fx.explosion, this);
Effects.shake(4f, 5f, this);
Effects.sound("die", this);
Timers.run(respawnduration + 5f, () -> {
heal();
set(world.getSpawnX(), world.getSpawnY());
interpolator.target.set(x, y);
});
}
@Override
public void drawSmooth(){
if((debug && (!showPlayer || !showUI)) || (isAndroid && isLocal) || dead) return;
boolean snap = snapCamera && Settings.getBool("smoothcam") && Settings.getBool("pixelate") && isLocal;
String part = isAndroid ? "ship" : "mech";
Shaders.outline.color.set(getColor());
Shaders.outline.lighten = 0f;
Shaders.outline.region = Draw.region(part + "-" + mech.name);
Shaders.outline.apply();
if(!isAndroid) {
for (int i : Mathf.signs) {
Weapon weapon = i < 0 ? weaponLeft : weaponRight;
tr.trns(angle - 90, 3*i, 2);
float w = i > 0 ? -8 : 8;
if(snap){
Draw.rect(weapon.name + "-equip", (int)x + tr.x, (int)y + tr.y, w, 8, angle - 90);
}else{
Draw.rect(weapon.name + "-equip", x + tr.x, y + tr.y, w, 8, angle - 90);
}
}
}
if(snap){
Draw.rect(part + "-" + mech.name, (int)x, (int)y, angle-90);
}else{
Draw.rect(part + "-" + mech.name, x, y, angle-90);
}
Graphics.flush();
}
@Override
public void update(){
if(!isLocal || isAndroid){
if(isAndroid && isLocal){
angle = Mathf.slerpDelta(angle, targetAngle, 0.2f);
}
if(!isLocal) interpolate();
return;
}
if(isDead()) return;
Tile tile = world.tileWorld(x, y);
//if player is in solid block
if(tile != null && ((tile.floor().liquid && tile.block() == Blocks.air) || tile.solid())) {
damage(health + 1); //die instantly
}
if(ui.chatfrag.chatOpen()) return;
dashing = Inputs.keyDown("dash");
float speed = dashing ? (debug ? Player.dashSpeed * 5f : Player.dashSpeed) : Player.speed;
if(health < maxhealth && timer.get(timerRegen, 20))
health ++;
health = Mathf.clamp(health, -1, maxhealth);
movement.set(0, 0);
float xa = Inputs.getAxis("move_x");
float ya = Inputs.getAxis("move_y");
if(Math.abs(xa) < 0.3) xa = 0;
if(Math.abs(ya) < 0.3) ya = 0;
movement.y += ya*speed;
movement.x += xa*speed;
boolean shooting = !Inputs.keyDown("dash") && Inputs.keyDown("shoot") && control.input().recipe == null
&& !ui.hasMouse() && !control.input().onConfigurable();
if(shooting){
weaponLeft.update(player, true);
weaponRight.update(player, false);
}
if(dashing && timer.get(timerDash, 3) && movement.len() > 0){
Effects.effect(Fx.dashsmoke, x + Angles.trnsx(angle + 180f, 3f), y + Angles.trnsy(angle + 180f, 3f));
}
movement.limit(speed);
if(!noclip){
move(movement.x*Timers.delta(), movement.y*Timers.delta());
}else{
x += movement.x*Timers.delta();
y += movement.y*Timers.delta();
}
if(!shooting){
if(!movement.isZero())
angle = Mathf.slerpDelta(angle, movement.angle(), 0.13f);
}else{
float angle = Angles.mouseAngle(x, y);
this.angle = Mathf.slerpDelta(this.angle, angle, 0.1f);
}
x = Mathf.clamp(x, 0, world.width() * tilesize);
y = Mathf.clamp(y, 0, world.height() * tilesize);
}
@Override
public Player add(){
return add(playerGroup);
}
@Override
public String toString() {
return "Player{" + id + ", android=" + isAndroid + ", local=" + isLocal + ", " + x + ", " + y + "}\n";
}
@Override
public void writeSpawn(ByteBuffer buffer) {
buffer.put((byte)name.getBytes().length);
buffer.put(name.getBytes());
buffer.put(weaponLeft.id);
buffer.put(weaponRight.id);
buffer.put(isAndroid ? 1 : (byte)0);
buffer.put(isAdmin ? 1 : (byte)0);
buffer.putInt(Color.rgba8888(color));
buffer.putFloat(x);
buffer.putFloat(y);
}
@Override
public void readSpawn(ByteBuffer buffer) {
byte nlength = buffer.get();
byte[] n = new byte[nlength];
buffer.get(n);
name = new String(n);
weaponLeft = (Weapon) Upgrade.getByID(buffer.get());
weaponRight = (Weapon) Upgrade.getByID(buffer.get());
isAndroid = buffer.get() == 1;
isAdmin = buffer.get() == 1;
color.set(buffer.getInt());
x = buffer.getFloat();
y = buffer.getFloat();
setNet(x, y);
}
@Override
public void write(ByteBuffer data) {
if(Net.client() || isLocal) {
data.putFloat(x);
data.putFloat(y);
}else{
data.putFloat(interpolator.target.x);
data.putFloat(interpolator.target.y);
}
data.putFloat(angle);
data.putShort((short)health);
data.put((byte)(dashing ? 1 : 0));
}
@Override
public void read(ByteBuffer data, long time) {
float x = data.getFloat();
float y = data.getFloat();
float angle = data.getFloat();
short health = data.getShort();
byte dashing = data.get();
this.health = health;
this.dashing = dashing == 1;
interpolator.read(this.x, this.y, x, y, angle, time);
}
@Override
public void interpolate() {
super.interpolate();
Interpolator i = interpolator;
float tx = x + Angles.trnsx(angle + 180f, 4f);
float ty = y + Angles.trnsy(angle + 180f, 4f);
if(isAndroid && i.target.dst(i.last) > 2f && timer.get(timerDash, 1)){
Effects.effect(Fx.dashsmoke, tx, ty);
}
if(dashing && !dead && timer.get(timerDash, 3) && i.target.dst(i.last) > 1f){
Effects.effect(Fx.dashsmoke, tx, ty);
}
}
public Color getColor(){
return color;
}
}

View File

@@ -0,0 +1,81 @@
package io.anuke.mindustry.entities;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Vector2;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.entities.traits.TargetTrait;
/**
* 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){
dstvx /= Time.delta();
dstvy /= Time.delta();
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(dstx, dsty);
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.getTargetVelocityX() - src.getTargetVelocityX()/2f,
dst.getTargetVelocityY() - src.getTargetVelocityY()/2f, 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

@@ -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,153 +0,0 @@
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 io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.types.Wall;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.Entity;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Timer;
import java.io.DataInputStream;
import java.io.DataOutputStream;
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();
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 void readNetwork(DataInputStream stream, float elapsed) throws IOException{
read(stream);
}
public void onDeath(){
onDeath(false);
}
public void onDeath(boolean force){
if(Net.server()){
NetEvents.handleBlockDestroyed(this);
}
if(!Net.active() || Net.server() || force){
if(!dead) {
dead = true;
Block block = tile.block();
block.onDestroyed(tile);
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();
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))) {
Effects.effect(Fx.smoke, x + Mathf.range(4), y + Mathf.range(4));
}
if (health <= 0) {
onDeath();
}
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);
}
}

View File

@@ -0,0 +1,221 @@
package io.anuke.mindustry.entities;
import io.anuke.arc.collection.EnumSet;
import io.anuke.arc.function.Consumer;
import io.anuke.arc.function.Predicate;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Geometry;
import io.anuke.arc.math.geom.Rectangle;
import io.anuke.mindustry.entities.traits.TargetTrait;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.world.Tile;
import static io.anuke.mindustry.Vars.*;
/** Utility class for unit and team interactions.*/
public class Units{
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.withinDst(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.getTeam(), targeter.x, targeter.y, targeter.getWeapon().bullet.range());
}
/** Returns whether there are any entities on this tile. */
public static boolean anyEntities(Tile tile){
float size = tile.block().size * tilesize;
return anyEntities(tile.drawx() - size/2f, tile.drawy() - size/2f, size, size);
}
public static boolean anyEntities(float x, float y, float width, float height){
boolResult = false;
nearby(x, y, width, height, unit -> {
if(boolResult) return;
if(!unit.isFlying()){
unit.hitbox(hitrect);
if(hitrect.overlaps(x, y, width, height)){
boolResult = true;
}
}
});
return boolResult;
}
/** Returns the neareset damaged tile. */
public static TileEntity findDamagedTile(Team team, float x, float y){
Tile tile = Geometry.findClosest(x, y, world.indexer.getDamaged(team));
return tile == null ? null : tile.entity;
}
/** Returns the neareset ally tile in a range. */
public static TileEntity findAllyTile(Team team, float x, float y, float range, Predicate<Tile> pred){
return world.indexer.findTile(team, x, y, range, pred);
}
/** Returns the neareset enemy tile in a range. */
public static TileEntity findEnemyTile(Team team, float x, float y, float range, Predicate<Tile> pred){
if(team == Team.none) return null;
for(Team enemy : state.teams.enemiesOf(team)){
TileEntity entity = world.indexer.findTile(enemy, x, y, range, pred);
if(entity != null){
return entity;
}
}
return null;
}
/** Returns the closest target enemy. First, units are checked, then tile entities. */
public static TargetTrait closestTarget(Team team, float x, float y, float range){
return closestTarget(team, x, y, range, Unit::isValid);
}
/** Returns the closest target enemy. First, units are checked, then tile entities. */
public static TargetTrait closestTarget(Team team, float x, float y, float range, Predicate<Unit> unitPred){
return closestTarget(team, x, y, range, unitPred, t -> true);
}
/** Returns the closest target enemy. First, units are checked, then tile entities. */
public static TargetTrait closestTarget(Team team, float x, float y, float range, Predicate<Unit> unitPred, Predicate<Tile> tilePred){
if(team == Team.none) return null;
Unit unit = closestEnemy(team, x, y, range, unitPred);
if(unit != null){
return unit;
}else{
return findEnemyTile(team, x, y, range, tilePred);
}
}
/** Returns the closest enemy of this team. Filter by predicate. */
public static Unit closestEnemy(Team team, float x, float y, float range, Predicate<Unit> predicate){
if(team == Team.none) return null;
result = null;
cdist = 0f;
nearbyEnemies(team, x - range, y - range, range*2f, range*2f, e -> {
if(e.isDead() || !predicate.test(e)) return;
float dst2 = Mathf.dst2(e.x, e.y, x, y);
if(dst2 < range*range && (result == null || dst2 < cdist)){
result = e;
cdist = dst2;
}
});
return result;
}
/** Returns the closest ally of this team. Filter by predicate. */
public static Unit closest(Team team, float x, float y, float range, Predicate<Unit> predicate){
result = null;
cdist = 0f;
nearby(team, x, y, range, e -> {
if(!predicate.test(e)) return;
float dist = Mathf.dst2(e.x, e.y, x, y);
if(result == null || dist < cdist){
result = e;
cdist = dist;
}
});
return result;
}
/** Iterates over all units in a rectangle. */
public static void nearby(Team team, float x, float y, float width, float height, Consumer<Unit> cons){
unitGroups[team.ordinal()].intersect(x, y, width, height, cons);
playerGroup.intersect(x, y, width, height, player -> {
if(player.getTeam() == team){
cons.accept(player);
}
});
}
/** Iterates over all units in a circle around this position. */
public static void nearby(Team team, float x, float y, float radius, Consumer<Unit> cons){
unitGroups[team.ordinal()].intersect(x - radius, y - radius, radius*2f, radius*2f, unit -> {
if(unit.withinDst(x, y, radius)){
cons.accept(unit);
}
});
playerGroup.intersect(x - radius, y - radius, radius*2f, radius*2f, unit -> {
if(unit.getTeam() == team && unit.withinDst(x, y, radius)){
cons.accept(unit);
}
});
}
/** Iterates over all units in a rectangle. */
public static void nearby(float x, float y, float width, float height, Consumer<Unit> cons){
for(Team team : Team.all){
unitGroups[team.ordinal()].intersect(x, y, width, height, cons);
}
playerGroup.intersect(x, y, width, height, cons);
}
/** Iterates over all units in a rectangle. */
public static void nearby(Rectangle rect, Consumer<Unit> cons){
nearby(rect.x, rect.y, rect.width, rect.height, cons);
}
/** Iterates over all units that are enemies of this team. */
public static void nearbyEnemies(Team team, float x, float y, float width, float height, Consumer<Unit> cons){
EnumSet<Team> targets = state.teams.enemiesOf(team);
for(Team other : targets){
unitGroups[other.ordinal()].intersect(x, y, width, height, cons);
}
playerGroup.intersect(x, y, width, height, player -> {
if(targets.contains(player.getTeam())){
cons.accept(player);
}
});
}
/** Iterates over all units that are enemies of this team. */
public static void nearbyEnemies(Team team, Rectangle rect, Consumer<Unit> cons){
nearbyEnemies(team, rect.x, rect.y, rect.width, rect.height, cons);
}
/** Iterates over all units. */
public static void all(Consumer<Unit> cons){
for(Team team : Team.all){
unitGroups[team.ordinal()].all().each(cons);
}
playerGroup.all().each(cons);
}
}

View File

@@ -0,0 +1,42 @@
package io.anuke.mindustry.entities.bullet;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.mindustry.content.Fx;
import io.anuke.mindustry.entities.Effects;
import io.anuke.mindustry.entities.Effects.Effect;
//TODO scale velocity depending on fslope()
public class ArtilleryBulletType extends BasicBulletType{
protected Effect trailEffect = Fx.artilleryTrail;
public ArtilleryBulletType(float speed, float damage, String bulletSprite){
super(speed, damage, bulletSprite);
collidesTiles = false;
collides = false;
collidesAir = 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.rot() - 90);
Draw.color(frontColor);
Draw.rect(frontRegion, b.x, b.y, bulletWidth * scale, height * scale, b.rot() - 90);
Draw.color();
}
}

View File

@@ -0,0 +1,40 @@
package io.anuke.mindustry.entities.bullet;
import io.anuke.arc.Core;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.mindustry.graphics.Pal;
/** An extended BulletType for most ammo-based bullets shot from turrets and units. */
public class BasicBulletType extends BulletType{
public Color backColor = Pal.bulletYellowBack, frontColor = Pal.bulletYellow;
public float bulletWidth = 5f, bulletHeight = 7f;
public float bulletShrink = 0.5f;
public String bulletSprite;
public TextureRegion backRegion;
public TextureRegion frontRegion;
public BasicBulletType(float speed, float damage, String bulletSprite){
super(speed, damage);
this.bulletSprite = bulletSprite;
}
@Override
public void load(){
backRegion = Core.atlas.find(bulletSprite + "-back");
frontRegion = Core.atlas.find(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.rot() - 90);
Draw.color(frontColor);
Draw.rect(frontRegion, b.x, b.y, bulletWidth, height, b.rot() - 90);
Draw.color();
}
}

View File

@@ -0,0 +1,17 @@
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;
collidesAir = false;
}
}

View File

@@ -0,0 +1,319 @@
package io.anuke.mindustry.entities.bullet;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Rectangle;
import io.anuke.arc.math.geom.Vector2;
import io.anuke.arc.util.*;
import io.anuke.arc.util.pooling.Pool.Poolable;
import io.anuke.arc.util.pooling.Pools;
import io.anuke.mindustry.entities.EntityGroup;
import io.anuke.mindustry.entities.effect.Lightning;
import io.anuke.mindustry.entities.impl.SolidEntity;
import io.anuke.mindustry.entities.traits.*;
import io.anuke.mindustry.entities.type.Unit;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.world.Tile;
import static io.anuke.mindustry.Vars.bulletGroup;
import static io.anuke.mindustry.Vars.world;
public class Bullet extends SolidEntity implements DamageTrait, ScaleTrait, Poolable, DrawTrait, VelocityTrait, TimeTrait, TeamTrait, AbsorbTrait{
public Interval timer = new Interval(3);
private float lifeScl;
private Team team;
private Object data;
private boolean supressCollision, supressOnce, initialized;
protected BulletType type;
protected Entity owner;
protected float time;
/** Internal use only! */
public Bullet(){
}
public static Bullet create(BulletType type, TeamTrait owner, float x, float y, float angle){
return create(type, owner, owner.getTeam(), x, y, angle);
}
public static Bullet create(BulletType type, Entity owner, Team team, float x, float y, float angle){
return create(type, owner, team, x, y, angle, 1f);
}
public static Bullet create(BulletType type, Entity owner, Team team, float x, float y, float angle, float velocityScl){
return create(type, owner, team, x, y, angle, velocityScl, 1f, null);
}
public static Bullet create(BulletType type, Entity owner, Team team, float x, float y, float angle, float velocityScl, float lifetimeScl){
return create(type, owner, team, x, y, angle, velocityScl, lifetimeScl, null);
}
public static Bullet create(BulletType type, Entity owner, Team team, float x, float y, float angle, float velocityScl, float lifetimeScl, Object data){
Bullet bullet = Pools.obtain(Bullet.class, Bullet::new);
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).velocity() : Vector2.ZERO);
}
bullet.team = team;
bullet.type = type;
bullet.lifeScl = lifetimeScl;
bullet.set(x - bullet.velocity.x * Time.delta(), y - bullet.velocity.y * Time.delta());
bullet.add();
return bullet;
}
public static Bullet create(BulletType type, Bullet parent, float x, float y, float angle){
return create(type, parent.owner, parent.team, x, y, angle);
}
public static Bullet create(BulletType type, Bullet parent, float x, float y, float angle, float velocityScl){
return create(type, parent.owner, parent.team, x, y, angle, velocityScl);
}
/** Internal use only. */
@Remote(called = Loc.server, unreliable = true)
public static void createBullet(BulletType type, float x, float y, float angle){
create(type, null, Team.none, x, y, angle);
}
/** ok */
@Remote(called = Loc.server, unreliable = true)
public static void createBullet(BulletType type, Team team, float x, float y, float angle){
create(type, null, team, x, y, angle);
}
public Entity getOwner(){
return owner;
}
public boolean collidesTiles(){
return type.collidesTiles;
}
public void supress(){
supressCollision = true;
supressOnce = true;
}
public BulletType getBulletType(){
return type;
}
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;
}
public void setData(Object data){
this.data = data;
}
public float damageMultiplier(){
if(owner instanceof Unit){
return ((Unit)owner).getDamageMultipler();
}
return 1f;
}
@Override
public void absorb(){
supressCollision = true;
remove();
}
@Override
public float drawSize(){
return type.drawSize;
}
@Override
public float damage(){
if(owner instanceof Lightning && data instanceof Float){
return (Float)data;
}
return type.damage * damageMultiplier();
}
@Override
public Team getTeam(){
return team;
}
@Override
public float getShieldDamage(){
return Math.max(damage(), type.splashDamage);
}
@Override
public boolean collides(SolidTrait other){
return type.collides && (other != owner && !(other instanceof DamageTrait)) && !supressCollision && !(other instanceof Unit && ((Unit)other).isFlying() && !type.collidesAir);
}
@Override
public void collision(SolidTrait other, float x, float y){
if(!type.pierce) remove();
type.hit(this, x, y);
if(other instanceof Unit){
Unit unit = (Unit)other;
unit.velocity().add(Tmp.v3.set(other.getX(), other.getY()).sub(x, y).setLength(type.knockback / unit.mass()));
unit.applyEffect(type.status, type.statusDuration);
}
}
@Override
public void update(){
type.update(this);
x += velocity.x * Time.delta();
y += velocity.y * Time.delta();
velocity.scl(Mathf.clamp(1f - type.drag * Time.delta()));
time += Time.delta() * 1f / (lifeScl);
time = Mathf.clamp(time, 0, type.lifetime);
if(time >= type.lifetime){
if(!supressCollision) type.despawned(this);
remove();
}
if(type.hitTiles && collidesTiles() && !supressCollision && initialized){
world.raycastEach(world.toTile(lastPosition().x), world.toTile(lastPosition().y), world.toTile(x), world.toTile(y), (x, y) -> {
Tile tile = world.ltile(x, y);
if(tile == null) return false;
if(tile.entity != null && tile.entity.collide(this) && type.collides(this, tile) && !tile.entity.isDead() && (type.collidesTeam || tile.getTeam() != team)){
if(tile.getTeam() != team){
tile.entity.collision(this);
}
if(!supressCollision){
type.hitTile(this, tile);
remove();
}
return true;
}
return false;
});
}
if(supressOnce){
supressCollision = false;
supressOnce = false;
}
initialized = true;
}
@Override
public void reset(){
type = null;
owner = null;
velocity.setZero();
time = 0f;
timer.clear();
lifeScl = 1f;
team = null;
data = null;
supressCollision = false;
supressOnce = false;
initialized = false;
}
@Override
public void hitbox(Rectangle rectangle){
rectangle.setSize(type.hitSize).setCenter(x, y);
}
@Override
public void hitboxTile(Rectangle rectangle){
rectangle.setSize(type.hitSize).setCenter(x, y);
}
@Override
public float lifetime(){
return type.lifetime;
}
@Override
public void time(float time){
this.time = time;
}
@Override
public float time(){
return time;
}
@Override
public void removed(){
Pools.free(this);
}
@Override
public EntityGroup targetGroup(){
return bulletGroup;
}
@Override
public void added(){
type.init(this);
}
@Override
public void draw(){
type.draw(this);
}
@Override
public float fin(){
return time / type.lifetime;
}
@Override
public Vector2 velocity(){
return velocity;
}
public void velocity(float speed, float angle){
velocity.set(0, speed).setAngle(angle);
}
public void limit(float f){
velocity.limit(f);
}
/** Sets the bullet's rotation in degrees. */
public void rot(float angle){
velocity.setAngle(angle);
}
/** @return the bullet's rotation. */
public float rot(){
float angle = Mathf.atan2(velocity.x, velocity.y) * Mathf.radiansToDegrees;
if(angle < 0) angle += 360;
return angle;
}
}

View File

@@ -0,0 +1,160 @@
package io.anuke.mindustry.entities.bullet;
import io.anuke.arc.math.Angles;
import io.anuke.arc.math.Mathf;
import io.anuke.mindustry.content.Fx;
import io.anuke.mindustry.content.StatusEffects;
import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.Effects.Effect;
import io.anuke.mindustry.entities.effect.Lightning;
import io.anuke.mindustry.entities.traits.TargetTrait;
import io.anuke.mindustry.game.Content;
import io.anuke.mindustry.graphics.Pal;
import io.anuke.mindustry.type.ContentType;
import io.anuke.mindustry.type.StatusEffect;
import io.anuke.mindustry.world.Tile;
public abstract class BulletType extends Content{
public float lifetime;
public float speed;
public float damage;
public float hitSize = 4;
public float drawSize = 40f;
public float drag = 0f;
public boolean pierce;
public Effect hitEffect, despawnEffect;
/** Effect created when shooting. */
public Effect shootEffect = Fx.shootSmall;
/** Extra smoke effect created when shooting. */
public Effect smokeEffect = Fx.shootSmallSmoke;
/** Extra inaccuracy when firing. */
public float inaccuracy = 0f;
/** How many bullets get created per ammo item/liquid. */
public float ammoMultiplier = 1f;
/** Multiplied by turret reload speed to get final shoot speed. */
public float reloadMultiplier = 1f;
/** Recoil from shooter entities. */
public float recoil;
public float splashDamage = 0f;
/** 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 statusDuration = 60 * 1f;
/** Whether this bullet type collides with tiles. */
public boolean collidesTiles = true;
/** Whether this bullet type collides with tiles that are of the same team. */
public boolean collidesTeam = false;
/** Whether this bullet type collides with air units. */
public boolean collidesAir = 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;
//additional effects
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 int incendAmount = 0;
public float incendSpread = 8f;
public float incendChance = 1f;
public float homingPower = 0f;
public float homingRange = 50f;
public int lightining;
public int lightningLength = 5;
public float hitShake = 0f;
public BulletType(float speed, float damage){
this.speed = speed;
this.damage = damage;
lifetime = 40f;
hitEffect = Fx.hitBulletSmall;
despawnEffect = Fx.hitBulletSmall;
}
/** Returns maximum distance the bullet this bullet type has can travel. */
public float range(){
return speed * lifetime * (1f - drag);
}
public boolean collides(Bullet bullet, Tile tile){
return true;
}
public void hitTile(Bullet b, Tile tile){
hit(b);
}
public void hit(Bullet b){
hit(b, b.x, b.y);
}
public void hit(Bullet b, float x, float y){
Effects.effect(hitEffect, x, y, b.rot());
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 * b.damageMultiplier());
}
}
public void despawned(Bullet b){
Effects.effect(despawnEffect, b.x, b.y, b.rot());
if(fragBullet != null || splashDamageRadius > 0){
hit(b);
}
for(int i = 0; i < lightining; i++){
Lightning.create(b.getTeam(), Pal.surge, damage, b.x, b.y, Mathf.random(360f), lightningLength);
}
}
public void draw(Bullet b){
}
public void init(Bullet b){
}
public void update(Bullet b){
if(homingPower > 0.0001f){
TargetTrait target = Units.closestTarget(b.getTeam(), b.x, b.y, homingRange);
if(target != null){
b.velocity().setAngle(Mathf.slerpDelta(b.velocity().angle(), b.angleTo(target), 0.08f));
}
}
}
@Override
public ContentType getContentType(){
return ContentType.bullet;
}
}

View File

@@ -0,0 +1,41 @@
package io.anuke.mindustry.entities.bullet;
import io.anuke.arc.math.geom.Rectangle;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.content.Fx;
import io.anuke.mindustry.entities.Units;
public abstract class FlakBulletType extends BasicBulletType{
protected static Rectangle rect = new Rectangle();
protected float explodeRange = 30f;
public FlakBulletType(float speed, float damage){
super(speed, damage, "shell");
splashDamage = 15f;
splashDamageRadius = 34f;
hitEffect = Fx.flakExplosionBig;
bulletWidth = 8f;
bulletHeight = 10f;
}
@Override
public void update(Bullet b){
super.update(b);
if(b.getData() instanceof Integer) return;
if(b.timer.get(2, 6)){
Units.nearbyEnemies(b.getTeam(), rect.setSize(explodeRange * 2f).setCenter(b.x, b.y), unit -> {
if(b.getData() instanceof Float) return;
if(unit.dst(b) < explodeRange){
b.setData(0);
Time.run(5f, () -> {
if(b.getData() instanceof Integer){
b.time(b.lifetime());
}
});
}
});
}
}
}

View File

@@ -0,0 +1,76 @@
package io.anuke.mindustry.entities.bullet;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.Fill;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Geometry;
import io.anuke.arc.math.geom.Point2;
import io.anuke.mindustry.content.Fx;
import io.anuke.mindustry.entities.Effects;
import io.anuke.mindustry.entities.effect.Fire;
import io.anuke.mindustry.entities.effect.Puddle;
import io.anuke.mindustry.type.Liquid;
import io.anuke.mindustry.world.Tile;
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(3.5f, 0);
this.liquid = liquid;
lifetime = 74f;
status = liquid.effect;
statusDuration = 90f;
despawnEffect = Fx.none;
hitEffect = Fx.hitLiquid;
smokeEffect = Fx.none;
shootEffect = Fx.none;
drag = 0.009f;
knockback = 0.55f;
}
@Override
public float range(){
return speed * lifetime / 2f;
}
@Override
public void update(Bullet b){
super.update(b);
if(liquid.canExtinguish()){
Tile tile = world.tileWorld(b.x, b.y);
if(tile != null && Fire.has(tile.x, tile.y)){
Fire.extinguish(tile, 100f);
b.remove();
hit(b);
}
}
}
@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(Point2 p : Geometry.d4){
Fire.extinguish(world.tileWorld(hitx + p.x * tilesize, hity + p.y * tilesize), intensity);
}
}
}
}

View File

@@ -0,0 +1,106 @@
package io.anuke.mindustry.entities.bullet;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.math.Angles;
import io.anuke.arc.math.Mathf;
import io.anuke.mindustry.content.Fx;
import io.anuke.mindustry.entities.Effects;
import io.anuke.mindustry.graphics.Pal;
import io.anuke.mindustry.world.blocks.distribution.MassDriver.DriverBulletData;
import static io.anuke.mindustry.Vars.content;
public class MassDriverBolt extends BulletType{
public MassDriverBolt(){
super(5.3f, 50);
collidesTiles = false;
lifetime = 200f;
despawnEffect = Fx.smeltsmoke;
hitEffect = Fx.hitBulletBig;
drag = 0.005f;
}
@Override
public void draw(Bullet b){
float w = 11f, h = 13f;
Draw.color(Pal.bulletYellowBack);
Draw.rect("shell-back", b.x, b.y, w, h, b.rot() + 90);
Draw.color(Pal.bulletYellow);
Draw.rect("shell", b.x, b.y, w, h, b.rot() + 90);
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.dst(data.to);
float dst1 = b.dst(data.from);
float dst2 = b.dst(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(Angles.near(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();
for(int i = 0; i < data.items.length; i++){
int amountDropped = Mathf.random(0, data.items[i]);
if(amountDropped > 0){
float angle = b.rot() + Mathf.range(100f);
Effects.effect(Fx.dropItem, Color.WHITE, b.x, b.y, angle, content.item(i));
}
}
}
@Override
public void hit(Bullet b, float hitx, float hity){
super.hit(b, hitx, hity);
despawned(b);
}
}

View File

@@ -0,0 +1,35 @@
package io.anuke.mindustry.entities.bullet;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.content.Fx;
import io.anuke.mindustry.entities.Effects;
import io.anuke.mindustry.graphics.Pal;
public class MissileBulletType extends BasicBulletType{
protected Color trailColor = Pal.missileYellowBack;
protected float weaveScale = 0f;
protected float weaveMag = -1f;
public MissileBulletType(float speed, float damage, String bulletSprite){
super(speed, damage, bulletSprite);
backColor = Pal.missileYellowBack;
frontColor = Pal.missileYellow;
homingPower = 7f;
}
@Override
public void update(Bullet b){
super.update(b);
if(Mathf.chance(Time.delta() * 0.2)){
Effects.effect(Fx.missileTrail, trailColor, b.x, b.y, 2f);
}
if(weaveMag > 0){
b.velocity().rotate(Mathf.sin(Time.time() + b.id * 4422, weaveScale, weaveMag));
}
}
}

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,36 @@
package io.anuke.mindustry.entities.effect;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.math.Mathf;
import io.anuke.mindustry.entities.EntityGroup;
import io.anuke.mindustry.entities.impl.TimedEntity;
import io.anuke.mindustry.entities.traits.BelowLiquidTrait;
import io.anuke.mindustry.entities.traits.DrawTrait;
import io.anuke.mindustry.graphics.Pal;
import static io.anuke.mindustry.Vars.groundEffectGroup;
/**
* Class for creating block rubble on the ground.
*/
public abstract class Decal extends TimedEntity implements BelowLiquidTrait, DrawTrait{
@Override
public float lifetime(){
return 8200f;
}
@Override
public void draw(){
Draw.color(Pal.rubble.r, Pal.rubble.g, Pal.rubble.b, 1f - Mathf.curve(fin(), 0.98f));
drawDecal();
Draw.color();
}
@Override
public EntityGroup targetGroup(){
return groundEffectGroup;
}
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,216 @@
package io.anuke.mindustry.entities.effect;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.arc.collection.IntMap;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Geometry;
import io.anuke.arc.math.geom.Point2;
import io.anuke.arc.util.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.impl.TimedEntity;
import io.anuke.mindustry.entities.traits.SaveTrait;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.entities.type.TileEntity;
import io.anuke.mindustry.gen.Call;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.world.*;
import java.io.*;
import static io.anuke.mindustry.Vars.*;
public class Fire extends TimedEntity implements SaveTrait, SyncTrait{
private static final IntMap<Fire> map = new IntMap<>();
private static final float baseLifetime = 1000f, spreadChance = 0.05f, fireballChance = 0.07f;
private int loadedPosition = -1;
private Tile tile;
private Block block;
private float baseFlammability = -1, puddleFlammability;
private float lifetime;
/** Deserialization use only! */
public Fire(){
}
@Remote
public static void onRemoveFire(int fid){
fireGroup.removeByID(fid);
}
/** 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.pos());
if(fire == null){
fire = new Fire();
fire.tile = tile;
fire.lifetime = baseLifetime;
fire.set(tile.worldx(), tile.worldy());
fire.add();
map.put(tile.pos(), fire);
}else{
fire.lifetime = baseLifetime;
fire.time = 0f;
}
}
public static boolean has(int x, int y){
if(!Structs.inBounds(x, y, world.width(), world.height()) || !map.containsKey(Pos.get(x, y))){
return false;
}
Fire fire = map.get(Pos.get(x, y));
return fire.isAdded() && fire.fin() < 1f && fire.tile != null && fire.tile.x == x && fire.tile.y == y;
}
/**
* 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.pos())){
map.get(tile.pos()).time += intensity * Time.delta();
}
}
@Override
public byte version(){
return 0;
}
@Override
public float lifetime(){
return lifetime;
}
@Override
public void update(){
if(Mathf.chance(0.1 * Time.delta())){
Effects.effect(Fx.fire, x + Mathf.range(4f), y + Mathf.range(4f));
}
if(Mathf.chance(0.05 * Time.delta())){
Effects.effect(Fx.fireSmoke, x + Mathf.range(4f), y + Mathf.range(4f));
}
time = Mathf.clamp(time + Time.delta(), 0, lifetime());
map.put(tile.pos(), this);
if(Net.client()){
return;
}
if(time >= lifetime() || tile == null){
remove();
return;
}
TileEntity entity = tile.link().entity;
boolean damage = entity != null;
float flammability = baseFlammability + puddleFlammability;
if(!damage && flammability <= 0){
time += Time.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) * Time.delta();
}
if(flammability > 1f && Mathf.chance(spreadChance * Time.delta() * Mathf.clamp(flammability / 5f, 0.3f, 2f))){
Point2 p = Geometry.d4[Mathf.random(3)];
Tile other = world.tile(tile.x + p.x, tile.y + p.y);
create(other);
if(Mathf.chance(fireballChance * Time.delta() * Mathf.clamp(flammability / 10f))){
Call.createBullet(Bullets.fireball, x, y, Mathf.random(360f));
}
}
if(Mathf.chance(0.1 * Time.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.isImmune(StatusEffects.burning),
unit -> unit.applyEffect(StatusEffects.burning, 60 * 5));
}
}
@Override
public void writeSave(DataOutput stream) throws IOException{
stream.writeInt(tile.pos());
stream.writeFloat(lifetime);
stream.writeFloat(time);
}
@Override
public void readSave(DataInput stream, byte version) throws IOException{
this.loadedPosition = stream.readInt();
this.lifetime = stream.readFloat();
this.time = stream.readFloat();
add();
}
@Override
public void write(DataOutput data) throws IOException{
data.writeInt(tile.pos());
data.writeFloat(lifetime);
}
@Override
public void read(DataInput data) throws IOException{
int pos = data.readInt();
this.lifetime = data.readFloat();
x = Pos.x(pos) * tilesize;
y = Pos.y(pos) * tilesize;
tile = world.tile(pos);
}
@Override
public void reset(){
loadedPosition = -1;
tile = null;
baseFlammability = -1;
puddleFlammability = 0f;
incrementID();
}
@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){
Call.onRemoveFire(id);
map.remove(tile.pos());
}
}
@Override
public EntityGroup targetGroup(){
return fireGroup;
}
}

View File

@@ -0,0 +1,90 @@
package io.anuke.mindustry.entities.effect;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.util.Time;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.entities.Effects;
import io.anuke.mindustry.entities.Effects.Effect;
import io.anuke.mindustry.entities.Effects.EffectRenderer;
import io.anuke.mindustry.entities.impl.EffectEntity;
import io.anuke.mindustry.world.Tile;
/**
* 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 += Time.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,120 @@
package io.anuke.mindustry.entities.effect;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.math.Interpolation;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Position;
import io.anuke.arc.math.geom.Vector2;
import io.anuke.arc.util.Time;
import io.anuke.arc.util.pooling.Pools;
import io.anuke.mindustry.entities.EntityGroup;
import io.anuke.mindustry.entities.impl.TimedEntity;
import io.anuke.mindustry.entities.traits.DrawTrait;
import io.anuke.mindustry.entities.type.Unit;
import io.anuke.mindustry.graphics.Pal;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.world.Tile;
import static io.anuke.mindustry.Vars.effectGroup;
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 Position to;
private Runnable done;
public ItemTransfer(){
}
@Remote(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(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.addItem(item));
}
@Remote(called = Loc.server)
public static void transferItemTo(Item item, int amount, float x, float y, Tile tile){
if(tile == null || tile.entity == null || tile.entity.items == null) return;
for(int i = 0; i < Mathf.clamp(amount / 3, 1, 8); i++){
Time.run(i * 3, () -> create(item, x, y, tile, () -> {
}));
}
tile.entity.items.add(item, amount);
}
public static void create(Item item, float fromx, float fromy, Position to, Runnable done){
ItemTransfer tr = Pools.obtain(ItemTransfer.class, ItemTransfer::new);
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){
done.run();
}
Pools.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(){
Lines.stroke(fslope() * 2f, Pal.accent);
Lines.circle(x, y, fslope() * 2f);
Draw.color(item.color);
Fill.circle(x, y, fslope() * 1.5f);
Draw.reset();
}
@Override
public EntityGroup targetGroup(){
return effectGroup;
}
}

View File

@@ -0,0 +1,134 @@
package io.anuke.mindustry.entities.effect;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.arc.collection.Array;
import io.anuke.arc.collection.IntSet;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.math.*;
import io.anuke.arc.math.geom.*;
import io.anuke.arc.util.pooling.Pools;
import io.anuke.mindustry.content.Bullets;
import io.anuke.mindustry.entities.EntityGroup;
import io.anuke.mindustry.entities.Units;
import io.anuke.mindustry.entities.bullet.Bullet;
import io.anuke.mindustry.entities.impl.TimedEntity;
import io.anuke.mindustry.entities.traits.DrawTrait;
import io.anuke.mindustry.entities.traits.TimeTrait;
import io.anuke.mindustry.entities.type.Unit;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.gen.Call;
import io.anuke.mindustry.graphics.Pal;
import static io.anuke.mindustry.Vars.bulletGroup;
public class Lightning extends TimedEntity implements DrawTrait, TimeTrait{
public static final float lifetime = 10f;
private static final RandomXS128 random = new RandomXS128();
private static final Rectangle rect = new Rectangle();
private static final Array<Unit> entities = new Array<>();
private static final IntSet hit = new IntSet();
private static final int maxChain = 8;
private static final float hitRange = 30f;
private static int lastSeed = 0;
private Array<Position> lines = new Array<>();
private Color color = Pal.lancerLaser;
/** 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, Color color, float damage, float x, float y, float targetAngle, int length){
Call.createLighting(lastSeed++, team, color, damage, x, y, targetAngle, length);
}
/** Do not invoke! */
@Remote(called = Loc.server)
public static void createLighting(int seed, Team team, Color color, float damage, float x, float y, float rotation, int length){
Lightning l = Pools.obtain(Lightning.class, Lightning::new);
Float dmg = damage;
l.x = x;
l.y = y;
l.color = color;
l.add();
random.setSeed(seed);
hit.clear();
for(int i = 0; i < length / 2; i++){
Bullet.create(Bullets.damageLightning, l, team, x, y, 0f, 1f, 1f, dmg);
l.lines.add(new Vector2(x + Mathf.range(3f), y + Mathf.range(3f)));
rect.setSize(hitRange).setCenter(x, y);
entities.clear();
if(hit.size < maxChain){
Units.nearbyEnemies(team, rect, u -> {
if(!hit.contains(u.getID())){
entities.add(u);
}
});
}
Unit furthest = Geometry.findFurthest(x, y, entities);
if(furthest != null){
hit.add(furthest.getID());
x = furthest.x;
y = furthest.y;
}else{
rotation += random.range(20f);
x += Angles.trnsx(rotation, hitRange / 2f);
y += Angles.trnsy(rotation, hitRange / 2f);
}
}
}
@Override
public float lifetime(){
return lifetime;
}
@Override
public void reset(){
super.reset();
color = Pal.lancerLaser;
lines.clear();
}
@Override
public void removed(){
super.removed();
Pools.free(this);
}
@Override
public void draw(){
Lines.stroke(3f * fout());
Draw.color(color, Color.WHITE, fin());
Lines.beginLine();
Lines.linePoint(x, y);
for(Position p : lines){
Lines.linePoint(p.getX(), p.getY());
}
Lines.endLine();
int i = 0;
for(Position p : lines){
Fill.square(p.getX(), p.getY(), (5f - (float)i++ / lines.size * 2f) * fout(), 45);
}
Draw.reset();
}
@Override
public EntityGroup targetGroup(){
return bulletGroup;
}
}

View File

@@ -0,0 +1,312 @@
package io.anuke.mindustry.entities.effect;
import io.anuke.annotations.Annotations.Loc;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.arc.collection.IntMap;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.Fill;
import io.anuke.arc.math.Angles;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.*;
import io.anuke.arc.util.Time;
import io.anuke.arc.util.pooling.Pool.Poolable;
import io.anuke.arc.util.pooling.Pools;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.impl.SolidEntity;
import io.anuke.mindustry.entities.traits.*;
import io.anuke.mindustry.gen.Call;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.type.Liquid;
import io.anuke.mindustry.world.Tile;
import java.io.*;
import static io.anuke.mindustry.Vars.*;
public class Puddle extends SolidEntity 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 float lastRipple;
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.pos());
}
private static void deposit(Tile tile, Tile source, Liquid liquid, float amount, int generation){
if(tile == null) return;
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);
Puddle p = map.get(tile.pos());
if(generation == 0 && p != null && p.lastRipple <= Time.time() - 40f){
Effects.effect(Fx.ripple, tile.floor().liquidDrop.color,
(tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f);
p.lastRipple = Time.time();
}
return;
}
Puddle p = map.get(tile.pos());
if(p == null){
if(Net.client()) return; //not clientside.
Puddle puddle = Pools.obtain(Puddle.class, Puddle::new);
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.pos(), puddle);
}else if(p.liquid == liquid){
p.accepting = Math.max(amount, p.accepting);
if(generation == 0 && p.lastRipple <= Time.time() - 40f && p.amount >= maxLiquid / 2f){
Effects.effect(Fx.ripple, p.liquid.color, (tile.worldx() + source.worldx()) / 2f, (tile.worldy() + source.worldy()) / 2f);
p.lastRipple = Time.time();
}
}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)){
Call.createBullet(Bullets.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(Fx.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(Fx.steam, x, y);
}
return -0.4f * amount;
}
return 0f;
}
@Remote(called = Loc.server)
public static void onPuddleRemoved(int puddleid){
puddleGroup.removeByID(puddleid);
}
public float getFlammability(){
return liquid.flammability * amount;
}
@Override
public byte version(){
return 0;
}
@Override
public void hitbox(Rectangle rectangle){
rectangle.setCenter(x, y).setSize(tilesize);
}
@Override
public void hitboxTile(Rectangle rectangle){
rectangle.setCenter(x, y).setSize(0f);
}
@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 -= Time.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) * Time.delta();
for(Point2 point : Geometry.d4){
Tile other = world.tile(tile.x + point.x, tile.y + point.y);
if(other != null && other.block() == Blocks.air){
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){
Call.onPuddleRemoved(getID());
}
}
//effects-only code
if(amount >= maxLiquid / 2f && updateTime <= 0f){
Units.nearby(rect.setSize(Mathf.clamp(amount / (maxLiquid / 1.5f)) * 10f).setCenter(x, y), unit -> {
if(unit.isFlying()) return;
unit.hitbox(rect2);
if(!rect.overlaps(rect2)) return;
unit.applyEffect(liquid.effect, 60 * 2);
if(unit.velocity().len() > 0.1){
Effects.effect(Fx.ripple, liquid.color, unit.x, unit.y);
}
});
if(liquid.temperature > 0.7f && (tile.link().entity != null) && Mathf.chance(0.3 * Time.delta())){
Fire.create(tile);
}
updateTime = 20f;
}
updateTime -= Time.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(tmp.set(liquid.color).shiftValue(-0.05f));
Fill.circle(x + Mathf.sin(Time.time() + seeds * 532, sscl, smag), y + Mathf.sin(Time.time() + seeds * 53, sscl, smag), f * 8f);
Angles.randLenVectors(id, 3, f * 6f, (ex, ey) -> {
Fill.circle(x + ex + Mathf.sin(Time.time() + seeds * 532, sscl, smag),
y + ey + Mathf.sin(Time.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.pos());
stream.writeFloat(x);
stream.writeFloat(y);
stream.writeByte(liquid.id);
stream.writeFloat(amount);
stream.writeByte(generation);
}
@Override
public void readSave(DataInput stream, byte version) throws IOException{
this.loadedPosition = stream.readInt();
this.x = stream.readFloat();
this.y = stream.readFloat();
this.liquid = content.liquid(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.pos());
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.pos());
}
@Override
public void read(DataInput data) throws IOException{
x = data.readFloat();
y = data.readFloat();
liquid = content.liquid(data.readByte());
targetAmount = data.readShort() / 4f;
int pos = data.readInt();
tile = world.tile(pos);
map.put(pos, this);
}
@Override
public EntityGroup targetGroup(){
return puddleGroup;
}
}

View File

@@ -0,0 +1,44 @@
package io.anuke.mindustry.entities.effect;
import io.anuke.arc.Core;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.arc.math.Mathf;
import static io.anuke.mindustry.Vars.headless;
public class RubbleDecal extends Decal{
private static final TextureRegion[][] regions = new TextureRegion[16][0];
private TextureRegion region;
/** Creates a rubble effect at a position. Provide a block size to use. */
public static void create(float x, float y, int size){
if(headless) return;
if(regions[size].length == 0 || regions[size][0].getTexture().isDisposed()){
regions[size] = new TextureRegion[2];
for(int j = 0; j < 2; j++){
regions[size][j] = Core.atlas.find("rubble-" + size + "-" + j);
}
}
RubbleDecal decal = new RubbleDecal();
decal.region = regions[size][Mathf.clamp(Mathf.randomSeed(decal.id, 0, 1), 0, regions[size].length - 1)];
if(!Core.atlas.isFound(decal.region)){
return;
}
decal.set(x, y);
decal.add();
}
@Override
public void drawDecal(){
if(!Core.atlas.isFound(region)){
remove();
return;
}
Draw.rect(region, x, y, Mathf.randomSeed(id, 0, 4) * 90);
}
}

View File

@@ -0,0 +1,49 @@
package io.anuke.mindustry.entities.effect;
import io.anuke.arc.Core;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.arc.math.Angles;
import io.anuke.arc.math.Mathf;
import io.anuke.mindustry.world.Tile;
import static io.anuke.mindustry.Vars.headless;
import static io.anuke.mindustry.Vars.world;
public class ScorchDecal extends Decal{
private static final int scorches = 5;
private static final TextureRegion[] regions = new TextureRegion[scorches];
public static void create(float x, float y){
if(headless) return;
if(regions[0] == null || regions[0].getTexture().isDisposed()){
for(int i = 0; i < regions.length; i++){
regions[i] = Core.atlas.find("scorch" + (i + 1));
}
}
Tile tile = world.tileWorld(x, y);
if(tile == null || tile.floor().liquidDrop != null) return;
ScorchDecal decal = new ScorchDecal();
decal.set(x, y);
decal.add();
}
@Override
public void drawDecal(){
for(int i = 0; i < 5; i++){
TextureRegion region = regions[Mathf.randomSeed(id - i, 0, scorches - 1)];
float rotation = Mathf.randomSeed(id + i, 0, 360);
float space = 1.5f + Mathf.randomSeed(id + i + 1, 0, 20) / 10f;
Draw.rect(region,
x + Angles.trnsx(rotation, space),
y + Angles.trnsy(rotation, space) + region.getHeight() / 2f * Draw.scl,
region.getWidth() * Draw.scl,
region.getHeight() * Draw.scl,
region.getWidth() / 2f * Draw.scl, 0, rotation - 90);
}
}
}

View File

@@ -1,103 +0,0 @@
package io.anuke.mindustry.entities.effect;
import com.badlogic.gdx.math.Interpolation;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.types.defense.ShieldBlock;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.BulletEntity;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.Entity;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.bulletGroup;
import static io.anuke.mindustry.Vars.shieldGroup;
public class Shield extends Entity{
public boolean active;
public boolean hitPlayers = false;
public float radius = 0f;
private float uptime = 0f;
private final Tile tile;
public Shield(Tile tile){
this.tile = tile;
this.x = tile.worldx();
this.y = tile.worldy();
}
public float drawSize(){
return 150;
}
@Override
public void update(){
float alpha = 0.1f;
Interpolation interp = Interpolation.fade;
if(active){
uptime = interp.apply(uptime, 1f, alpha * Timers.delta());
}else{
uptime = interp.apply(uptime, 0f, alpha * Timers.delta());
if(uptime <= 0.05f)
remove();
}
uptime = Mathf.clamp(uptime);
if(!(tile.block() instanceof ShieldBlock)){
remove();
return;
}
ShieldBlock block = (ShieldBlock)tile.block();
Entities.getNearby(bulletGroup, x, y, block.shieldRadius * 2*uptime + 10, entity->{
BulletEntity bullet = (BulletEntity)entity;
if((bullet.owner instanceof Enemy || hitPlayers)){
float dst = entity.distanceTo(this);
if(dst < drawRadius()/2f){
((ShieldBlock)tile.block()).handleBullet(tile, bullet);
}
}
});
}
@Override
public void draw(){
if(!(tile.block() instanceof ShieldBlock) || radius <= 1f){
return;
}
float rad = drawRadius();
Draw.rect("circle2", x, y, rad, rad);
}
float drawRadius(){
return (radius*2 + Mathf.sin(Timers.time(), 25f, 2f));
}
public void removeDelay(){
active = false;
}
@Override
public Shield add(){
return super.add(shieldGroup);
}
@Override
public void added(){
active = true;
}
@Override
public void removed(){
active = false;
uptime = 0f;
}
}

View File

@@ -1,135 +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 com.badlogic.gdx.utils.ObjectSet;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.mindustry.graphics.Fx;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.Entity;
import io.anuke.ucore.entities.SolidEntity;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Mathf;
import static io.anuke.mindustry.Vars.enemyGroup;
public class TeslaOrb extends Entity{
private Array<Vector2> points = new Array<>();
private ObjectSet<Enemy> hit = new ObjectSet<>();
private int damage = 0;
private float range = 0;
private float lifetime = 30f;
private float life = 0f;
private Vector2 vector = new Vector2();
public TeslaOrb(float x, float y, float range, int damage){
set(x, y);
this.damage = damage;
this.range = range;
}
void shock(){
float stopchance = 0.1f;
float curx = x, cury = y;
float shake = 3f;
int max = 7;
while(points.size < max){
if(Mathf.chance(stopchance)){
break;
}
Array<SolidEntity> enemies = Entities.getNearby(enemyGroup, curx, cury, range*2f);
synchronized (Entities.entityLock) {
for (SolidEntity entity : enemies) {
if (entity != null && entity.distanceTo(curx, cury) < range && !hit.contains((Enemy) entity)) {
hit.add((Enemy) entity);
points.add(new Vector2(entity.x + Mathf.range(shake), entity.y + Mathf.range(shake)));
damageEnemy((Enemy) entity);
curx = entity.x;
cury = entity.y;
break;
}
}
}
}
if(points.size == 0){
remove();
}
}
void damageEnemy(Enemy enemy){
enemy.damage(damage);
Effects.effect(Fx.laserhit, enemy.x + Mathf.range(2f), enemy.y + Mathf.range(2f));
}
@Override
public void update(){
life += Timers.delta();
if(life >= lifetime){
remove();
}
}
@Override
public void drawOver(){
if(points.size == 0) return;
float range = 1f;
Vector2 previous = vector.set(x, y);
for(Vector2 enemy : points){
float x1 = previous.x + Mathf.range(range),
y1 = previous.y + Mathf.range(range),
x2 = enemy.x + Mathf.range(range),
y2 = enemy.y + Mathf.range(range);
Draw.color(Color.WHITE);
Draw.alpha(1f-life/lifetime);
Lines.stroke(3f - life/lifetime*2f);
Lines.line(x1, y1, x2, y2);
float rad = 7f - life/lifetime*5f;
Draw.rect("circle", x2, y2, rad, rad);
if(previous.epsilonEquals(x, y, 0.001f)){
Draw.rect("circle", x, y, rad, rad);
}
//Draw.color(Color.WHITE);
//Draw.stroke(2f - life/lifetime*2f);
//Draw.line(x1, y1, x2, y2);
Draw.reset();
previous = enemy;
}
}
@Override
public void added(){
Timers.run(1f, ()->{
shock();
});
}
@Override
public float drawSize(){
return 200;
}
}

View File

@@ -1,164 +0,0 @@
package io.anuke.mindustry.entities.enemies;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import io.anuke.mindustry.entities.Bullet;
import io.anuke.mindustry.entities.BulletType;
import io.anuke.mindustry.entities.SyncEntity;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.net.NetEvents;
import io.anuke.ucore.entities.Entity;
import io.anuke.ucore.entities.SolidEntity;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Timer;
import io.anuke.ucore.util.Translator;
import java.nio.ByteBuffer;
import static io.anuke.mindustry.Vars.enemyGroup;
public class Enemy extends SyncEntity {
public EnemyType type;
public Timer timer = new Timer(5);
public float idletime = 0f;
public int lane;
public int node = -1;
public Enemy spawner;
public int spawned;
public Vector2 velocity = new Vector2();
public Vector2 totalMove = new Vector2();
public Vector2 tpos = new Vector2(-999, -999);
public Entity target;
public float hitTime;
public int tier = 1;
public TextureRegion region;
public Translator tr = new Translator();
public Enemy(EnemyType type){
this.type = type;
}
/**internal constructor used for deserialization, DO NOT USE*/
public Enemy(){}
@Override
public void update(){
type.update(this);
}
@Override
public void drawSmooth(){
type.draw(this);
}
@Override
public void drawOver(){
type.drawOver(this);
}
@Override
public float drawSize(){
return 14;
}
@Override
public boolean collides(SolidEntity other){
return (other instanceof Bullet) && !(((Bullet) other).owner instanceof Enemy);
}
@Override
public void damage(float amount){
super.damage(amount);
hitTime = EnemyType.hitDuration;
}
@Override
public void onDeath(){
type.onDeath(this, false);
}
@Override
public void removed(){
type.removed(this);
}
@Override
public void added(){
hitbox.setSize(type.hitsize);
hitboxTile.setSize(type.hitsizeTile);
maxhealth = type.health * tier;
region = Draw.region(type.name + "-t" + Mathf.clamp(tier, 1, 3));
heal();
}
@Override
public Enemy add(){
return add(enemyGroup);
}
@Override
public void writeSpawn(ByteBuffer buffer) {
buffer.put(type.id);
buffer.put((byte)lane);
buffer.put((byte)tier);
buffer.putFloat(x);
buffer.putFloat(y);
buffer.putShort((short)health);
}
@Override
public void readSpawn(ByteBuffer buffer) {
type = EnemyType.getByID(buffer.get());
lane = buffer.get();
tier = buffer.get();
x = buffer.getFloat();
y = buffer.getFloat();
health = buffer.getShort();
setNet(x, y);
}
@Override
public void write(ByteBuffer data) {
data.putFloat(x);
data.putFloat(y);
data.putShort((short)(angle*2));
data.putShort((short)health);
}
@Override
public void read(ByteBuffer data, long time) {
float x = data.getFloat();
float y = data.getFloat();
short angle = data.getShort();
short health = data.getShort();
this.health = health;
interpolator.read(this.x, this.y, x, y, angle/2f, time);
}
public void shoot(BulletType bullet){
shoot(bullet, 0);
}
public void shoot(BulletType bullet, float rotation){
if(!(Net.client())) {
tr.trns(angle + rotation, type.length);
Bullet out = new Bullet(bullet, this, x + tr.x, y + tr.y, this.angle + rotation).add();
out.damage = (int) ((bullet.damage * (1 + (tier - 1) * 1f)));
type.onShoot(this, bullet, rotation);
if(Net.server()){
NetEvents.handleBullet(bullet, this, x + tr.x, y + tr.y, this.angle + rotation, (short)out.damage);
}
}
}
}

View File

@@ -1,289 +0,0 @@
package io.anuke.mindustry.entities.enemies;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import io.anuke.mindustry.entities.BulletType;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.graphics.Fx;
import io.anuke.mindustry.graphics.Shaders;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.net.NetEvents;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.Blocks;
import io.anuke.ucore.core.Effects;
import io.anuke.ucore.core.Graphics;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.graphics.Draw;
import io.anuke.ucore.graphics.Lines;
import io.anuke.ucore.util.Mathf;
import io.anuke.ucore.util.Strings;
import static io.anuke.mindustry.Vars.*;
public class EnemyType {
//TODO documentation, comments
private static byte lastid = 0;
private static Array<EnemyType> types = new Array<>();
public final static Color[] tierColors = {
Color.valueOf("ffe451"), Color.valueOf("f48e20"), Color.valueOf("ff6757"),
Color.valueOf("ff2d86"), Color.valueOf("cb2dff"), Color.valueOf("362020") };
public final static int maxtier = tierColors.length;
public final static float maxIdleLife = 60f*2f; //2 seconds idle = death
public final static float hitDuration = 5f;
public final String name;
public final byte id;
protected int timeid;
protected int health = 60;
protected float hitsize = 5f;
protected float hitsizeTile = 4f;
protected float speed = 0.4f;
protected float reload = 32;
protected float range = 60;
protected float length = 4;
protected float rotatespeed = 0.1f;
protected float turretrotatespeed = 0.2f;
protected boolean alwaysRotate = false;
protected BulletType bullet = BulletType.small;
protected String shootsound = "enemyshoot";
protected boolean targetCore = false;
protected boolean stopNearCore = true;
protected boolean targetClient = false;
protected float mass = 1f;
protected final int timerTarget = timeid ++;
protected final int timerReload = timeid ++;
protected final int timerReset = timeid ++;
protected final Vector2 shift = new Vector2();
protected final Vector2 move = new Vector2();
protected final Vector2 calc = new Vector2();
public EnemyType(String name){
this.id = lastid++;
this.name = name;
types.add(this);
}
public void draw(Enemy enemy){
Shaders.outline.color.set(tierColors[enemy.tier - 1]);
Shaders.outline.lighten = Mathf.clamp(enemy.hitTime/hitDuration);
Shaders.outline.region = enemy.region;
Shaders.outline.apply();
Draw.rect(enemy.region, enemy.x, enemy.y, enemy.angle - 90);
Draw.color();
Graphics.flush();
if(isCalculating(enemy)){
Draw.color(Color.SKY);
Lines.polySeg(20, 0, 4, enemy.x, enemy.y, 11f, Timers.time() * 2f + enemy.id*52f);
Lines.polySeg(20, 0, 4, enemy.x, enemy.y, 11f, Timers.time() * 2f + enemy.id*52f + 180f);
Draw.color();
}
if(showPaths){
Draw.tscl(0.25f);
Draw.text((int)enemy.idletime + " " + enemy.node + " " + enemy.id + "\n" + Strings.toFixed(enemy.totalMove.x, 2) + ", "
+ Strings.toFixed(enemy.totalMove.x, 2), enemy.x, enemy.y);
Draw.tscl(fontscale);
}
Shaders.outline.lighten = 0f;
}
public void drawOver(Enemy enemy){ }
public void update(Enemy enemy){
float lastx = enemy.x, lasty = enemy.y;
if(enemy.hitTime > 0){
enemy.hitTime -= Timers.delta();
}
if(enemy.lane >= world.getSpawns().size || enemy.lane < 0) enemy.lane = 0;
boolean waiting = enemy.lane >= world.getSpawns().size || enemy.lane < 0
|| world.getSpawns().get(enemy.lane).pathTiles == null || enemy.node <= 0;
move(enemy);
enemy.velocity.set(enemy.x - lastx, enemy.y - lasty).scl(1f / Timers.delta());
enemy.totalMove.add(enemy.velocity);
float minv = 0.07f;
if(enemy.timer.get(timerReset, 80)){
enemy.totalMove.setZero();
}
if(enemy.velocity.len() < minv && !waiting && enemy.target == null){
enemy.idletime += Timers.delta();
}else{
enemy.idletime = 0;
}
if(enemy.timer.getTime(timerReset) > 50 && enemy.totalMove.len() < 0.2f && !waiting && enemy.target == null){
enemy.idletime = 999999f;
}
Tile tile = world.tileWorld(enemy.x, enemy.y);
if(tile != null && tile.floor().liquid && tile.block() == Blocks.air){
enemy.damage(enemy.health+1); //drown
}
if(Float.isNaN(enemy.angle)){
enemy.angle = 0;
}
if(enemy.target == null || alwaysRotate){
enemy.angle = Mathf.slerpDelta(enemy.angle, enemy.velocity.angle(), rotatespeed);
}else{
enemy.angle = Mathf.slerpDelta(enemy.angle, enemy.angleTo(enemy.target), turretrotatespeed);
}
enemy.x = Mathf.clamp(enemy.x, 0, world.width() * tilesize);
enemy.y = Mathf.clamp(enemy.y, 0, world.height() * tilesize);
}
public void move(Enemy enemy){
if(Net.client()){
enemy.interpolate();
if(targetClient) updateTargeting(enemy, false);
return;
}
float speed = this.speed + 0.04f * enemy.tier;
float range = this.range + enemy.tier * 5;
Tile core = world.getCore();
if(core == null) return;
if(enemy.idletime > maxIdleLife && enemy.node > 0){
enemy.onDeath();
return;
}
boolean nearCore = enemy.distanceTo(core.worldx(), core.worldy()) <= range - 18f && stopNearCore;
Vector2 vec;
if(nearCore){
vec = move.setZero();
if(targetCore) enemy.target = core.entity;
}else{
vec = world.pathfinder().find(enemy);
vec.sub(enemy.x, enemy.y).limit(speed);
}
shift.setZero();
float shiftRange = enemy.hitbox.width + 2f;
float avoidRange = shiftRange + 4f;
float attractRange = avoidRange + 7f;
float avoidSpeed = this.speed/2.7f;
Entities.getNearby(enemyGroup, enemy.x, enemy.y, range, en -> {
Enemy other = (Enemy)en;
if(other == enemy) return;
float dst = other.distanceTo(enemy);
if(dst < shiftRange){
float scl = Mathf.clamp(1.4f - dst / shiftRange) * mass * 1f/mass;
shift.add((enemy.x - other.x) * scl, (enemy.y - other.y) * scl);
}else if(dst < avoidRange){
calc.set((enemy.x - other.x), (enemy.y - other.y)).setLength(avoidSpeed);
shift.add(calc.scl(1.1f));
}else if(dst < attractRange && !nearCore && !isCalculating(enemy)){
calc.set((enemy.x - other.x), (enemy.y - other.y)).setLength(avoidSpeed);
shift.add(calc.scl(-1));
}
});
shift.limit(1f);
vec.add(shift.scl(0.5f));
enemy.move(vec.x * Timers.delta(), vec.y * Timers.delta());
updateTargeting(enemy, nearCore);
behavior(enemy);
}
public void behavior(Enemy enemy){}
public void updateTargeting(Enemy enemy, boolean nearCore){
if(enemy.target != null && enemy.target instanceof TileEntity && ((TileEntity)enemy.target).dead){
enemy.target = null;
}
if(enemy.timer.get(timerTarget, 15) && !nearCore){
enemy.target = world.findTileTarget(enemy.x, enemy.y, null, range, false);
//no tile found
if(enemy.target == null){
enemy.target = Entities.getClosest(playerGroup, enemy.x, enemy.y, range, e -> !((Player)e).isAndroid &&
!((Player)e).isDead());
}
}else if(nearCore){
enemy.target = world.getCore().entity;
}
if(enemy.target != null && bullet != null){
updateShooting(enemy);
}
}
public void updateShooting(Enemy enemy){
float reload = this.reload / Math.max(enemy.tier / 1.5f, 1f);
if(enemy.timer.get(timerReload, reload)){
shoot(enemy);
}
}
public void shoot(Enemy enemy){
enemy.shoot(bullet);
if(shootsound != null) Effects.sound(shootsound, enemy);
}
public void onShoot(Enemy enemy, BulletType type, float rotation){}
public void onDeath(Enemy enemy, boolean force){
if(Net.server()){
NetEvents.handleEnemyDeath(enemy);
}
if(!Net.client() || force) {
Effects.effect(Fx.explosion, enemy);
Effects.shake(3f, 4f, enemy);
Effects.sound("bang2", enemy);
enemy.remove();
enemy.dead = true;
}
}
public void removed(Enemy enemy){
if(!enemy.dead){
if(enemy.spawner != null){
enemy.spawner.spawned --;
}else{
state.enemies --;
}
}
}
public boolean isCalculating(Enemy enemy){
return enemy.node < 0 && !Net.client();
}
public static EnemyType getByID(byte id){
return types.get(id);
}
}

View File

@@ -1,32 +0,0 @@
package io.anuke.mindustry.entities.enemies;
import io.anuke.mindustry.entities.enemies.types.BlastType;
import io.anuke.mindustry.entities.enemies.types.EmpType;
import io.anuke.mindustry.entities.enemies.types.FastType;
import io.anuke.mindustry.entities.enemies.types.FlamerType;
import io.anuke.mindustry.entities.enemies.types.FortressType;
import io.anuke.mindustry.entities.enemies.types.HealerType;
import io.anuke.mindustry.entities.enemies.types.MortarType;
import io.anuke.mindustry.entities.enemies.types.RapidType;
import io.anuke.mindustry.entities.enemies.types.*;
import io.anuke.mindustry.entities.enemies.types.TankType;
import io.anuke.mindustry.entities.enemies.types.TargetType;
import io.anuke.mindustry.entities.enemies.types.TitanType;
public class EnemyTypes {
public static final EnemyType
standard = new StandardType(),
fast = new FastType(),
rapid = new RapidType(),
flamer = new FlamerType(),
tank = new TankType(),
blast = new BlastType(),
mortar = new MortarType(),
healer = new HealerType(),
titan = new TitanType(),
emp = new EmpType(),
fortress = new FortressType(),
target = new TargetType();
}

View File

@@ -1,54 +0,0 @@
package io.anuke.mindustry.entities.enemies.types;
import io.anuke.mindustry.entities.Bullet;
import io.anuke.mindustry.entities.BulletType;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.entities.enemies.Enemy;
import io.anuke.mindustry.entities.enemies.EnemyType;
import static io.anuke.mindustry.Vars.tilesize;
public class BlastType extends EnemyType {
public BlastType() {
super("blastenemy");
health = 30;
speed = 0.8f;
bullet = null;
turretrotatespeed = 0f;
mass = 0.8f;
stopNearCore = false;
}
@Override
public void behavior(Enemy enemy){
float range = 10f;
float ox = 0, oy = 0;
if(enemy.target instanceof TileEntity){
TileEntity e = (TileEntity)enemy.target;
range = (e.tile.block().width * tilesize) /2f + 8f;
ox = e.tile.block().getPlaceOffset().x;
oy = e.tile.block().getPlaceOffset().y;
}
if(enemy.target != null && enemy.target.distanceTo(enemy.x - ox, enemy.y - oy) < range){
explode(enemy);
}
}
@Override
public void onDeath(Enemy enemy, boolean force){
if(force) explode(enemy);
super.onDeath(enemy, force);
}
void explode(Enemy enemy){
Bullet b = new Bullet(BulletType.blast, enemy, enemy.x, enemy.y, 0).add();
b.damage = BulletType.blast.damage + (enemy.tier-1) * 40;
enemy.damage(999);
enemy.remove();
}
}

View File

@@ -1,19 +0,0 @@
package io.anuke.mindustry.entities.enemies.types;
import io.anuke.mindustry.entities.BulletType;
import io.anuke.mindustry.entities.enemies.EnemyType;
public class EmpType extends EnemyType {
public EmpType() {
super("empenemy");
speed = 0.3f;
reload = 70;
health = 210;
range = 80f;
bullet = BulletType.emp;
turretrotatespeed = 0.1f;
}
}

View File

@@ -1,17 +0,0 @@
package io.anuke.mindustry.entities.enemies.types;
import io.anuke.mindustry.entities.enemies.EnemyType;
public class FastType extends EnemyType {
public FastType() {
super("fastenemy");
speed = 0.73f;
reload = 20;
mass = 0.2f;
health = 50;
}
}

View File

@@ -1,20 +0,0 @@
package io.anuke.mindustry.entities.enemies.types;
import io.anuke.mindustry.entities.BulletType;
import io.anuke.mindustry.entities.enemies.EnemyType;
public class FlamerType extends EnemyType {
public FlamerType() {
super("flamerenemy");
speed = 0.35f;
health = 150;
reload = 6;
bullet = BulletType.flameshot;
shootsound = "flame";
mass = 1.5f;
range = 40;
}
}

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